UNPKG

@timesheet/sdk

Version:
1 lines 82.2 kB
{"version":3,"sources":["../src/http/ApiClient.ts","../src/exceptions/TimesheetApiError.ts","../src/exceptions/TimesheetAuthError.ts","../src/exceptions/TimesheetRateLimitError.ts","../src/auth/ApiKeyAuth.ts","../src/auth/OAuth2Auth.ts","../src/config/RetryConfig.ts","../src/models/Page.ts","../src/resources/Resource.ts","../src/resources/OrganizationResource.ts","../src/resources/TeamResource.ts","../src/resources/ProjectResource.ts","../src/utils/date.ts","../src/resources/TaskResource.ts","../src/resources/RateResource.ts","../src/resources/TagResource.ts","../src/resources/ExpenseResource.ts","../src/resources/NoteResource.ts","../src/resources/PauseResource.ts","../src/resources/ProfileResource.ts","../src/resources/SettingsResource.ts","../src/resources/AutomationResource.ts","../src/resources/DocumentResource.ts","../src/resources/TimerResource.ts","../src/resources/TodoResource.ts","../src/resources/WebhookResource.ts","../src/index.ts"],"sourcesContent":["import type { AxiosInstance, AxiosRequestConfig } from 'axios';\nimport axios from 'axios';\nimport type { ClientConfig } from '../config';\nimport { TimesheetApiError, TimesheetAuthError, TimesheetRateLimitError } from '../exceptions';\n\ninterface ErrorResponseData {\n message?: string;\n code?: string;\n}\n\n/**\n * HTTP client for making API requests.\n */\nexport class ApiClient {\n private readonly config: ClientConfig;\n private readonly httpClient: AxiosInstance;\n\n constructor(config: ClientConfig) {\n this.config = config;\n this.httpClient =\n config.httpClient ||\n axios.create({\n baseURL: config.baseUrl,\n timeout: 30000,\n headers: {\n 'User-Agent': 'Timesheet-TypeScript-SDK/1.0.0',\n 'Content-Type': 'application/json',\n },\n });\n\n // Request interceptor for authentication\n this.httpClient.interceptors.request.use(async (config) => {\n // Add authentication headers if configured\n if (config.url !== '/oauth/token' && this.config.authentication) {\n const authHeaders = await this.config.authentication.getAuthHeaders();\n if (authHeaders) {\n // Set headers directly to avoid type issues\n for (const [key, value] of Object.entries(authHeaders)) {\n config.headers.set(key, value);\n }\n }\n }\n\n return config;\n });\n }\n\n /**\n * Makes an HTTP request with retry support.\n */\n async request<T>(config: AxiosRequestConfig): Promise<T> {\n let lastError: Error | null = null;\n const retryConfig = this.config.retryConfig;\n\n for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {\n try {\n const response = await this.httpClient.request<T>(config);\n return response.data;\n } catch (error) {\n lastError = error as Error;\n\n // Handle authentication errors\n if (axios.isAxiosError<ErrorResponseData>(error) && error.response?.status === 401) {\n const errorData = error.response.data;\n throw new TimesheetAuthError(\n errorData?.message || 'Authentication failed',\n 401,\n JSON.stringify(errorData),\n );\n }\n\n // Handle rate limit errors\n if (axios.isAxiosError(error) && error.response?.status === 429) {\n const retryAfter = error.response.headers['retry-after'] as string | undefined;\n throw new TimesheetRateLimitError('Rate limit exceeded', retryAfter);\n }\n\n // Check if we should retry\n const status = axios.isAxiosError(error) ? error.response?.status : undefined;\n if (\n attempt < retryConfig.maxRetries &&\n status &&\n retryConfig.retryableStatusCodes.includes(status)\n ) {\n // Calculate delay with exponential backoff\n const delay = Math.min(\n retryConfig.initialDelay * Math.pow(retryConfig.backoffMultiplier, attempt),\n retryConfig.maxDelay,\n );\n\n await this.sleep(delay);\n continue;\n }\n\n // Non-retryable error\n if (axios.isAxiosError<ErrorResponseData>(error)) {\n const errorData = error.response?.data;\n throw new TimesheetApiError(\n errorData?.message || error.message,\n error.response?.status,\n JSON.stringify(errorData),\n errorData?.code,\n );\n } else if (error instanceof Error) {\n throw new TimesheetApiError(error.message);\n } else {\n throw new TimesheetApiError('Unknown error occurred');\n }\n }\n }\n\n // This should never happen, but TypeScript needs it\n throw lastError || new Error('Unknown error');\n }\n\n /**\n * GET request.\n */\n async get<T, P = Record<string, unknown>>(path: string, params?: P): Promise<T> {\n return this.request<T>({\n method: 'GET',\n url: path,\n params: params as Record<string, unknown>,\n });\n }\n\n /**\n * POST request.\n */\n async post<T, P = Record<string, unknown>>(path: string, data?: unknown, params?: P): Promise<T> {\n return this.request<T>({\n method: 'POST',\n url: path,\n data,\n params: params as Record<string, unknown>,\n });\n }\n\n /**\n * PUT request.\n */\n async put<T, P = Record<string, unknown>>(path: string, data?: unknown, params?: P): Promise<T> {\n return this.request<T>({\n method: 'PUT',\n url: path,\n data,\n params: params as Record<string, unknown>,\n });\n }\n\n /**\n * DELETE request.\n */\n async delete<T, P = Record<string, unknown>>(path: string, params?: P): Promise<T> {\n return this.request<T>({\n method: 'DELETE',\n url: path,\n params: params as Record<string, unknown>,\n });\n }\n\n /**\n * Sleep for the specified number of milliseconds.\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","/**\n * Base exception for all Timesheet API errors.\n */\nexport class TimesheetApiError extends Error {\n public readonly statusCode?: number;\n public readonly responseBody?: string;\n public readonly errorCode?: string;\n\n /**\n * Creates a new TimesheetApiError.\n *\n * @param message The error message\n * @param statusCode Optional HTTP status code\n * @param responseBody Optional response body\n * @param errorCode Optional API error code\n */\n constructor(message: string, statusCode?: number, responseBody?: string, errorCode?: string) {\n const fullMessage = statusCode\n ? errorCode\n ? `${message} (HTTP ${statusCode}, Code: ${errorCode})`\n : `${message} (HTTP ${statusCode})`\n : message;\n\n super(fullMessage);\n\n this.name = 'TimesheetApiError';\n this.statusCode = statusCode;\n this.responseBody = responseBody;\n this.errorCode = errorCode;\n\n // Maintains proper stack trace for where our error was thrown\n if (typeof Error.captureStackTrace === 'function') {\n Error.captureStackTrace(this, TimesheetApiError);\n }\n }\n}\n","import { TimesheetApiError } from './TimesheetApiError';\n\n/**\n * Exception thrown when authentication fails.\n */\nexport class TimesheetAuthError extends TimesheetApiError {\n /**\n * Creates a new authentication exception.\n *\n * @param message The error message\n * @param statusCode The HTTP status code (usually 401)\n * @param responseBody The response body\n */\n constructor(message: string, statusCode: number = 401, responseBody?: string) {\n super(message, statusCode, responseBody, 'authentication_error');\n this.name = 'TimesheetAuthError';\n }\n}\n","import { TimesheetApiError } from './TimesheetApiError';\n\n/**\n * Exception thrown when rate limit is exceeded.\n */\nexport class TimesheetRateLimitError extends TimesheetApiError {\n public readonly retryAfter?: string;\n\n /**\n * Creates a new rate limit exception.\n *\n * @param message The error message\n * @param retryAfter The retry-after header value\n */\n constructor(message: string, retryAfter?: string) {\n super(message, 429, undefined, 'rate_limit_exceeded');\n this.name = 'TimesheetRateLimitError';\n this.retryAfter = retryAfter;\n }\n\n /**\n * Parses the retry-after value as a Date.\n *\n * @returns The retry-after time as a Date, or null if parsing fails\n */\n getRetryAfterDate(): Date | null {\n if (!this.retryAfter) {\n return null;\n }\n\n // Try to parse as epoch seconds\n const epochSeconds = Number(this.retryAfter);\n if (!isNaN(epochSeconds)) {\n return new Date(epochSeconds * 1000);\n }\n\n // Try to parse as ISO string\n const date = new Date(this.retryAfter);\n if (!isNaN(date.getTime())) {\n return date;\n }\n\n return null;\n }\n}\n","import type { AxiosRequestConfig } from 'axios';\nimport type { Authentication } from './Authentication';\n\n/**\n * API Key authentication implementation.\n *\n * Uses the ApiKey scheme in the Authorization header.\n * Format: `Authorization: ApiKey {api_key}`\n */\nexport class ApiKeyAuth implements Authentication {\n constructor(private readonly apiKey: string) {\n if (apiKey === null) {\n throw new Error('API key cannot be null');\n }\n if (apiKey === undefined) {\n throw new Error('API key cannot be undefined');\n }\n if (!apiKey) {\n throw new Error('API key cannot be empty');\n }\n if (typeof apiKey !== 'string') {\n throw new Error('API key must be a string');\n }\n if (apiKey.trim().length === 0) {\n throw new Error('API key cannot be empty or whitespace');\n }\n\n // Validate API key format: should be ts_{prefix}.{secret}\n if (!this.isValidFormat(apiKey)) {\n throw new Error('Invalid API key format');\n }\n }\n\n private isValidFormat(apiKey: string): boolean {\n // API key format: ts_{prefix}.{secret}\n const apiKeyPattern = /^ts_[a-zA-Z0-9]+\\.[a-zA-Z0-9]+$/;\n return apiKeyPattern.test(apiKey);\n }\n\n applyAuth(config: AxiosRequestConfig): void {\n if (!config.headers) {\n config.headers = {};\n }\n config.headers['Authorization'] = `ApiKey ${this.apiKey}`;\n }\n\n needsRefresh(): boolean {\n // API keys don't need refresh\n return false;\n }\n\n // eslint-disable-next-line @typescript-eslint/require-await\n async refresh(): Promise<void> {\n throw new Error('API keys cannot be refreshed');\n }\n\n // eslint-disable-next-line @typescript-eslint/require-await\n async getAuthHeaders(): Promise<Record<string, string>> {\n return {\n Authorization: `ApiKey ${this.apiKey}`,\n };\n }\n\n isValid(): boolean {\n return (\n typeof this.apiKey === 'string' &&\n this.apiKey.trim().length > 0 &&\n this.isValidFormat(this.apiKey)\n );\n }\n}\n","import type { AxiosRequestConfig } from 'axios';\nimport axios from 'axios';\nimport jwt from 'jsonwebtoken';\nimport type { Authentication } from './Authentication';\n\n/**\n * OAuth2 authentication implementation with automatic token refresh.\n *\n * Supports both simple bearer token authentication and full OAuth2 flow with refresh tokens.\n */\nexport class OAuth2Auth implements Authentication {\n private static readonly TOKEN_ENDPOINT = 'https://api.timesheet.io/oauth2/token';\n\n private accessToken: string;\n private refreshToken?: string;\n private readonly clientId?: string;\n private readonly clientSecret?: string;\n private tokenExpiry?: Date;\n private refreshPromise?: Promise<void>;\n\n /**\n * Creates OAuth2 authentication with a simple bearer token.\n *\n * @param accessToken The OAuth2 access token\n */\n constructor(accessToken: string);\n\n /**\n * Creates OAuth2 authentication with refresh capability.\n *\n * @param clientId The OAuth2 client ID\n * @param clientSecret The OAuth2 client secret\n * @param refreshToken The OAuth2 refresh token\n */\n constructor(clientId: string, clientSecret: string, refreshToken: string);\n\n constructor(accessTokenOrClientId: string, clientSecret?: string, refreshToken?: string) {\n if (clientSecret && refreshToken) {\n // OAuth2 with refresh\n this.clientId = accessTokenOrClientId;\n this.clientSecret = clientSecret;\n this.refreshToken = refreshToken;\n this.accessToken = ''; // Will be set by refresh()\n // Don't call refresh here - let it happen lazily when needed\n } else {\n // Simple bearer token\n this.accessToken = accessTokenOrClientId;\n this.parseTokenExpiry();\n }\n }\n\n applyAuth(config: AxiosRequestConfig): void {\n if (!config.headers) {\n config.headers = {};\n }\n config.headers['Authorization'] = `Bearer ${this.accessToken}`;\n }\n\n needsRefresh(): boolean {\n // If we have refresh capabilities but no access token, we need to refresh\n if (this.refreshToken && !this.accessToken) {\n return true;\n }\n\n if (!this.refreshToken || !this.tokenExpiry) {\n return false;\n }\n // Refresh if token expires in less than 5 minutes\n const fiveMinutesFromNow = new Date(Date.now() + 5 * 60 * 1000);\n return fiveMinutesFromNow >= this.tokenExpiry;\n }\n\n async refresh(): Promise<void> {\n if (!this.refreshToken) {\n throw new Error('Cannot refresh without refresh token');\n }\n\n // Prevent multiple concurrent refresh attempts\n if (this.refreshPromise) {\n return this.refreshPromise;\n }\n\n this.refreshPromise = this.performRefresh();\n try {\n await this.refreshPromise;\n } finally {\n this.refreshPromise = undefined;\n }\n }\n\n private async performRefresh(): Promise<void> {\n interface TokenResponse {\n access_token: string;\n refresh_token?: string;\n token_type?: string;\n expires_in?: number;\n }\n\n try {\n const response = await axios.post<TokenResponse>(\n OAuth2Auth.TOKEN_ENDPOINT,\n new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: this.refreshToken!,\n client_id: this.clientId!,\n client_secret: this.clientSecret!,\n }),\n {\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n },\n );\n\n this.accessToken = response.data.access_token;\n\n if (response.data.refresh_token) {\n this.refreshToken = response.data.refresh_token;\n }\n\n this.parseTokenExpiry();\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n throw new Error(`Failed to refresh OAuth2 token: ${message}`);\n }\n }\n\n async getAuthHeaders(): Promise<Record<string, string>> {\n // Ensure we have a valid token before returning headers\n if (this.needsRefresh()) {\n await this.refresh();\n }\n\n return {\n Authorization: `Bearer ${this.accessToken}`,\n };\n }\n\n /**\n * Performs the OAuth2 authorization code flow.\n *\n * @param clientId The OAuth2 client ID\n * @param clientSecret The OAuth2 client secret\n * @param authorizationCode The authorization code from the OAuth2 flow\n * @param redirectUri The redirect URI used in the authorization request\n * @returns A new OAuth2Auth instance with the obtained tokens\n */\n static async fromAuthorizationCode(\n clientId: string,\n clientSecret: string,\n authorizationCode: string,\n redirectUri: string,\n ): Promise<OAuth2Auth> {\n interface TokenResponse {\n access_token: string;\n refresh_token?: string;\n token_type?: string;\n expires_in?: number;\n }\n\n try {\n const response = await axios.post<TokenResponse>(\n OAuth2Auth.TOKEN_ENDPOINT,\n new URLSearchParams({\n grant_type: 'authorization_code',\n code: authorizationCode,\n redirect_uri: redirectUri,\n client_id: clientId,\n client_secret: clientSecret,\n }),\n {\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n },\n );\n\n const accessToken = response.data.access_token;\n const refreshToken = response.data.refresh_token;\n\n if (refreshToken) {\n return new OAuth2Auth(clientId, clientSecret, refreshToken);\n } else {\n return new OAuth2Auth(accessToken);\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n throw new Error(`Failed to exchange authorization code: ${message}`);\n }\n }\n\n /**\n * Builds the OAuth2 authorization URL.\n *\n * @param clientId The OAuth2 client ID\n * @param redirectUri The redirect URI for the OAuth2 flow\n * @param state Optional state parameter for CSRF protection\n * @returns The authorization URL\n */\n static buildAuthorizationUrl(clientId: string, redirectUri: string, state?: string): string {\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n });\n\n if (state) {\n params.append('state', state);\n }\n\n return `https://api.timesheet.io/oauth2/auth?${params.toString()}`;\n }\n\n private parseTokenExpiry(): void {\n interface JWTPayload {\n exp?: number;\n iat?: number;\n sub?: string;\n [key: string]: unknown;\n }\n\n try {\n const decoded = jwt.decode(this.accessToken) as JWTPayload | null;\n if (decoded && decoded.exp) {\n this.tokenExpiry = new Date(decoded.exp * 1000);\n }\n } catch {\n // If we can't parse the JWT, assume it expires in 1 hour\n this.tokenExpiry = new Date(Date.now() + 60 * 60 * 1000);\n }\n }\n}\n","/**\n * Configuration for automatic retry behavior.\n */\nexport class RetryConfig {\n public readonly maxRetries: number;\n public readonly initialDelay: number;\n public readonly maxDelay: number;\n public readonly backoffMultiplier: number;\n public readonly retryableStatusCodes: number[];\n\n constructor(options: Partial<RetryConfigOptions> = {}) {\n this.maxRetries = options.maxRetries ?? 3;\n this.initialDelay = options.initialDelay ?? 100;\n this.maxDelay = options.maxDelay ?? 10000;\n this.backoffMultiplier = options.backoffMultiplier ?? 2.0;\n this.retryableStatusCodes = options.retryableStatusCodes ?? [429, 502, 503, 504];\n }\n\n /**\n * Returns the default retry configuration.\n */\n static default(): RetryConfig {\n return new RetryConfig();\n }\n}\n\n/**\n * Options for RetryConfig constructor.\n */\nexport interface RetryConfigOptions {\n /**\n * Maximum number of retry attempts.\n * @default 3\n */\n maxRetries: number;\n\n /**\n * Initial delay before the first retry in milliseconds.\n * @default 100\n */\n initialDelay: number;\n\n /**\n * Maximum delay between retries in milliseconds.\n * @default 10000\n */\n maxDelay: number;\n\n /**\n * Backoff multiplier for exponential backoff.\n * @default 2.0\n */\n backoffMultiplier: number;\n\n /**\n * HTTP status codes that should trigger a retry.\n * @default [429, 502, 503, 504]\n */\n retryableStatusCodes: number[];\n}\n","import type { ListParams } from './common';\n\n/**\n * Represents a page of results with pagination support.\n */\nexport interface Page<T> {\n /**\n * The items in this page.\n */\n items: T[];\n\n /**\n * Pagination and sorting parameters.\n */\n params: ListParams;\n}\n\n/**\n * Extended page with navigation methods.\n */\nexport class NavigablePage<T> implements Page<T> {\n items: T[];\n params: ListParams;\n\n private readonly nextPageLoader?: (page: number) => Promise<NavigablePage<T>>;\n\n constructor(data: Page<T>, nextPageLoader?: (page: number) => Promise<NavigablePage<T>>) {\n this.items = data.items;\n this.params = data.params;\n this.nextPageLoader = nextPageLoader;\n }\n\n /**\n * Gets the total number of pages.\n */\n get totalPages(): number {\n return Math.ceil((this.params?.count || 0) / (this.params?.limit || 25));\n }\n\n /**\n * Checks if there is a next page.\n */\n get hasNextPage(): boolean {\n return (this.params?.page || 1) < this.totalPages;\n }\n\n /**\n * Loads the next page.\n */\n async nextPage(): Promise<NavigablePage<T>> {\n if (!this.hasNextPage) {\n throw new Error('No more pages available');\n }\n if (!this.nextPageLoader) {\n throw new Error('Next page loader not configured');\n }\n return this.nextPageLoader((this.params.page || 1) + 1);\n }\n\n /**\n * Returns an async iterator for auto-pagination.\n */\n async *[Symbol.asyncIterator](): AsyncIterator<T> {\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n let currentPage: NavigablePage<T> = this;\n\n while (true) {\n for (const item of currentPage.items) {\n yield item;\n }\n\n if (!currentPage.hasNextPage) {\n break;\n }\n\n currentPage = await currentPage.nextPage();\n }\n }\n\n /**\n * Converts all pages to an array (loads all pages).\n */\n async toArray(): Promise<T[]> {\n const allItems: T[] = [];\n\n for await (const item of this) {\n allItems.push(item);\n }\n\n return allItems;\n }\n}\n","import type { ApiClient } from '../http';\nimport type { Page } from '../models';\nimport { NavigablePage } from '../models';\n\n/**\n * Configuration for resource endpoints\n */\nexport interface ResourceConfig {\n basePath: string;\n}\n\n/**\n * Base resource class that provides common functionality for all API resources.\n */\nexport abstract class Resource {\n protected readonly basePath: string;\n\n protected constructor(\n protected readonly http: ApiClient,\n config: ResourceConfig | string,\n ) {\n if (typeof config === 'string') {\n this.basePath = config;\n } else {\n this.basePath = config.basePath;\n }\n }\n\n /**\n * Creates a NavigablePage from a Page response\n * @param page The page response\n * @param nextPageLoader Function to load the next page\n */\n protected createNavigablePage<R>(\n page: Page<R>,\n nextPageLoader: (page: number) => Promise<NavigablePage<R>>,\n ): NavigablePage<R> {\n return new NavigablePage(page, nextPageLoader);\n }\n}\n","import type { ApiClient } from '../http';\nimport type {\n Organization,\n OrganizationCreateRequest,\n OrganizationListParams,\n OrganizationUpdateRequest,\n Page,\n} from '../models';\nimport { NavigablePage } from '../models';\nimport { Resource } from './Resource';\n\n/**\n * Resource for managing organizations.\n */\nexport class OrganizationResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/organizations');\n }\n\n async list(params?: OrganizationListParams): Promise<NavigablePage<Organization>> {\n const response = await this.http.get<Page<Organization>, OrganizationListParams>(\n this.basePath,\n params,\n );\n return new NavigablePage(response, (page) => this.list({ ...params, page }));\n }\n\n async create(data: OrganizationCreateRequest): Promise<Organization> {\n return this.http.post<Organization>(this.basePath, data);\n }\n\n async update(id: string, data: OrganizationUpdateRequest): Promise<Organization> {\n return this.http.put<Organization>(`${this.basePath}/${encodeURIComponent(id)}`, data);\n }\n\n async get(id: string): Promise<Organization> {\n return this.http.get<Organization>(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n async delete(id: string): Promise<void> {\n return this.http.delete(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Search organizations with parameters using POST\n * @param params Search parameters\n */\n async search(params: OrganizationListParams): Promise<NavigablePage<Organization>> {\n const response = await this.http.post<Page<Organization>>(`${this.basePath}/search`, params);\n return this.createNavigablePage(response, (page) => this.search({ ...params, page }));\n }\n}\n","import type { ApiClient } from '../http';\nimport type {\n Page,\n Team,\n TeamCreateRequest,\n TeamListParams,\n TeamMember,\n TeamMemberListParams,\n TeamUpdateRequest,\n} from '../models';\nimport { NavigablePage } from '../models';\nimport { Resource } from './Resource';\n\nexport class TeamResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/teams');\n }\n\n async list(params?: TeamListParams): Promise<NavigablePage<Team>> {\n const response = await this.http.get<Page<Team>, TeamListParams>(this.basePath, params);\n return new NavigablePage(response, (page) => this.list({ ...params, page }));\n }\n\n async create(data: TeamCreateRequest): Promise<Team> {\n return this.http.post<Team>(this.basePath, data);\n }\n\n async update(id: string, data: TeamUpdateRequest): Promise<Team> {\n return this.http.put<Team>(`${this.basePath}/${encodeURIComponent(id)}`, data);\n }\n\n async get(id: string): Promise<Team> {\n return this.http.get<Team>(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n async delete(id: string): Promise<void> {\n return this.http.delete(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Search teams with parameters using POST\n * @param params Search parameters\n */\n async search(params: TeamListParams): Promise<NavigablePage<Team>> {\n const response = await this.http.post<Page<Team>>(`${this.basePath}/search`, params);\n return this.createNavigablePage(response, (page) => this.search({ ...params, page }));\n }\n\n /**\n * List team members\n * @param teamId Team identifier\n * @param params List parameters\n */\n async listMembers(\n teamId: string,\n params: TeamMemberListParams,\n ): Promise<NavigablePage<TeamMember>> {\n const response = await this.http.post<Page<TeamMember>>(\n `${this.basePath}/${teamId}/members/list`,\n params,\n );\n return this.createNavigablePage(response, (page) =>\n this.listMembers(teamId, { ...params, page }),\n );\n }\n\n /**\n * Get team member\n * @param teamId Team identifier\n * @param memberId Member identifier\n */\n async getMember(teamId: string, memberId: string): Promise<TeamMember> {\n return this.http.get<TeamMember>(`${this.basePath}/${teamId}/members/${memberId}`);\n }\n\n /**\n * Get colleagues\n * @param params Parameters for filtering colleagues\n */\n async getColleagues(params?: TeamMemberListParams): Promise<NavigablePage<TeamMember>> {\n const response = await this.http.post<Page<TeamMember>>(\n `${this.basePath}/getColleagues`,\n params,\n );\n return this.createNavigablePage(response, (page) => this.getColleagues({ ...params, page }));\n }\n}\n","import type { ApiClient } from '../http';\nimport type {\n Page,\n Project,\n ProjectCreateRequest,\n ProjectListParams,\n ProjectUpdateRequest,\n} from '../models';\nimport { NavigablePage } from '../models';\nimport { Resource } from './Resource';\n\nexport class ProjectResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/projects');\n }\n\n async list(params?: ProjectListParams): Promise<NavigablePage<Project>> {\n const response = await this.http.get<Page<Project>, ProjectListParams>(this.basePath, params);\n return new NavigablePage(response, (page) => this.list({ ...params, page }));\n }\n\n async create(data: ProjectCreateRequest): Promise<Project> {\n return this.http.post<Project>(this.basePath, data);\n }\n\n async update(id: string, data: ProjectUpdateRequest): Promise<Project> {\n return this.http.put<Project>(`${this.basePath}/${encodeURIComponent(id)}`, data);\n }\n\n async get(id: string): Promise<Project> {\n return this.http.get<Project>(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n async delete(id: string): Promise<void> {\n return this.http.delete(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Search projects with parameters using POST\n * @param params Search parameters\n */\n async search(params: ProjectListParams): Promise<NavigablePage<Project>> {\n const response = await this.http.post<Page<Project>>(`${this.basePath}/search`, params);\n return this.createNavigablePage(response, (page) => this.search({ ...params, page }));\n }\n}\n","import dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\nimport timezone from 'dayjs/plugin/timezone';\nimport customParseFormat from 'dayjs/plugin/customParseFormat';\n\n// Extend dayjs with necessary plugins\ndayjs.extend(utc);\ndayjs.extend(timezone);\ndayjs.extend(customParseFormat);\n\n// The expected format for Timesheet API: YYYY-MM-DDTHH:mm:ss±HH:mm\nconst TIMESTAMP_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ';\nconst TIMESTAMP_REGEX = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}$/;\n\nexport const DateUtils = {\n /**\n * Validates if a timestamp string is in the correct format with timezone offset\n * (e.g. 2025-05-31T16:45:51+02:00)\n */\n isValidTimestampFormat(timestamp: string): boolean {\n return TIMESTAMP_REGEX.test(timestamp);\n },\n\n /**\n * Formats a date or timestamp string into the ISO 8601 format with timezone offset\n * that the Timesheet API expects (e.g. 2025-05-31T16:45:51+02:00)\n *\n * @param input Date object, timestamp string, or undefined for current time\n * @returns Formatted date string without milliseconds\n */\n formatTimestamp(input?: Date | string | dayjs.Dayjs): string {\n // If input is a string and already in correct format, return it\n if (typeof input === 'string' && this.isValidTimestampFormat(input)) {\n return input;\n }\n\n // Convert to dayjs object\n const dayjsDate = dayjs(input || new Date());\n\n // Format with timezone offset (no milliseconds)\n // dayjs's format 'Z' gives ±HH:mm format\n return dayjsDate.format(TIMESTAMP_FORMAT);\n },\n\n /**\n * Parses a timestamp string into a Date object.\n * Accepts both UTC ('Z') and timezone offset formats.\n *\n * @param timestamp Timestamp string to parse\n * @returns Date object\n */\n parseTimestamp(timestamp: string): Date {\n return dayjs(timestamp).toDate();\n },\n\n /**\n * Formats a date string to YYYY-MM-DD format\n *\n * @param input Date object, timestamp string, or undefined for current date\n * @returns Date string in YYYY-MM-DD format\n */\n formatDate(input?: Date | string | dayjs.Dayjs): string {\n return dayjs(input || new Date()).format('YYYY-MM-DD');\n },\n\n /**\n * Gets the current timestamp in the correct format\n *\n * @returns Current timestamp with timezone offset\n */\n now(): string {\n return this.formatTimestamp();\n },\n};\n","import type { ApiClient } from '../http';\nimport type {\n NavigablePage,\n Page,\n Task,\n TaskCreateRequest,\n TaskListParams,\n TaskStatusUpdateRequest,\n TaskTimesUpdateRequest,\n TaskUpdateRequest,\n} from '../models';\nimport { DateUtils } from '../utils/date';\nimport { Resource } from './Resource';\n\nexport class TaskResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/tasks');\n }\n\n async create(data: TaskCreateRequest): Promise<Task> {\n const formattedData = {\n ...data,\n startDateTime: DateUtils.formatTimestamp(data.startDateTime),\n endDateTime: data.endDateTime ? DateUtils.formatTimestamp(data.endDateTime) : undefined,\n };\n return this.http.post<Task>(this.basePath, formattedData);\n }\n\n async update(id: string, data: TaskUpdateRequest): Promise<Task> {\n const formattedData = {\n ...data,\n startDateTime: data.startDateTime ? DateUtils.formatTimestamp(data.startDateTime) : undefined,\n endDateTime: data.endDateTime ? DateUtils.formatTimestamp(data.endDateTime) : undefined,\n };\n return this.http.put<Task>(`${this.basePath}/${encodeURIComponent(id)}`, formattedData);\n }\n\n async get(id: string): Promise<Task> {\n return this.http.get<Task>(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n async delete(id: string): Promise<void> {\n return this.http.delete(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Search tasks with parameters using POST\n * @param params Search parameters\n */\n async search(params: TaskListParams): Promise<NavigablePage<Task>> {\n const response = await this.http.post<Page<Task>>(`${this.basePath}/search`, params);\n return this.createNavigablePage(response, (page) => this.search({ ...params, page }));\n }\n\n /**\n * Update task status (billable, paid, billed)\n * @param data Status update data\n */\n async updateStatus(data: TaskStatusUpdateRequest): Promise<Task> {\n return this.http.put<Task>(`${this.basePath}/updateStatus`, data);\n }\n\n /**\n * Update task times\n * @param data Times update data\n */\n async updateTimes(data: TaskTimesUpdateRequest): Promise<Task> {\n const formattedData = {\n ...data,\n startDateTime: DateUtils.formatTimestamp(data.startDateTime),\n endDateTime: DateUtils.formatTimestamp(data.endDateTime),\n };\n return this.http.put<Task>(`${this.basePath}/updateTimes`, formattedData);\n }\n}\n","import type { ApiClient } from '../http';\nimport type { Page, Rate, RateCreateRequest, RateListParams, RateUpdateRequest } from '../models';\nimport { NavigablePage } from '../models';\nimport { Resource } from './Resource';\n\nexport class RateResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/rates');\n }\n\n async list(params?: RateListParams): Promise<NavigablePage<Rate>> {\n const response = await this.http.get<Page<Rate>, RateListParams>(this.basePath, params);\n return new NavigablePage(response, (page) => this.list({ ...params, page }));\n }\n\n async create(data: RateCreateRequest): Promise<Rate> {\n return this.http.post<Rate>(this.basePath, data);\n }\n\n async update(id: string, data: RateUpdateRequest): Promise<Rate> {\n return this.http.put<Rate>(`${this.basePath}/${encodeURIComponent(id)}`, data);\n }\n\n async get(id: string): Promise<Rate> {\n return this.http.get<Rate>(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n async delete(id: string): Promise<void> {\n return this.http.delete(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Search rates with parameters using POST\n * @param params Search parameters\n */\n async search(params: RateListParams): Promise<NavigablePage<Rate>> {\n const response = await this.http.post<Page<Rate>>(`${this.basePath}/search`, params);\n return this.createNavigablePage(response, (page) => this.search({ ...params, page }));\n }\n}\n","import type { ApiClient } from '../http';\nimport type { Page, Tag, TagCreateRequest, TagListParams, TagUpdateRequest } from '../models';\nimport { NavigablePage } from '../models';\nimport { Resource } from './Resource';\n\nexport class TagResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/tags');\n }\n\n async list(params?: TagListParams): Promise<NavigablePage<Tag>> {\n const response = await this.http.get<Page<Tag>, TagListParams>(this.basePath, params);\n return new NavigablePage(response, (page) => this.list({ ...params, page }));\n }\n\n async create(data: TagCreateRequest): Promise<Tag> {\n return this.http.post<Tag>(this.basePath, data);\n }\n\n async update(id: string, data: TagUpdateRequest): Promise<Tag> {\n return this.http.put<Tag>(`${this.basePath}/${encodeURIComponent(id)}`, data);\n }\n\n async get(id: string): Promise<Tag> {\n return this.http.get<Tag>(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n async delete(id: string): Promise<void> {\n return this.http.delete(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Search tags with parameters using POST\n * @param params Search parameters\n */\n async search(params: TagListParams): Promise<NavigablePage<Tag>> {\n const response = await this.http.post<Page<Tag>>(`${this.basePath}/search`, params);\n return this.createNavigablePage(response, (page) => this.search({ ...params, page }));\n }\n}\n","import type { ApiClient } from '../http';\nimport type {\n Expense,\n ExpenseCreateRequest,\n ExpenseListParams,\n ExpenseStatus,\n ExpenseUpdateRequest,\n Page,\n} from '../models';\nimport { NavigablePage } from '../models';\nimport { DateUtils } from '../utils/date';\nimport { Resource } from './Resource';\n\nexport class ExpenseResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/expenses');\n }\n\n async list(params?: ExpenseListParams): Promise<NavigablePage<Expense>> {\n const response = await this.http.get<Page<Expense>, ExpenseListParams>(this.basePath, params);\n return new NavigablePage(response, (page) => this.list({ ...params, page }));\n }\n\n async create(data: ExpenseCreateRequest): Promise<Expense> {\n const formattedData = {\n ...data,\n dateTime: DateUtils.formatTimestamp(data.dateTime),\n };\n return this.http.post<Expense>(this.basePath, formattedData);\n }\n\n async update(id: string, data: ExpenseUpdateRequest): Promise<Expense> {\n const formattedData = {\n ...data,\n dateTime: data.dateTime ? DateUtils.formatTimestamp(data.dateTime) : undefined,\n };\n return this.http.put<Expense>(`${this.basePath}/${encodeURIComponent(id)}`, formattedData);\n }\n\n async get(id: string): Promise<Expense> {\n return this.http.get<Expense>(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n async delete(id: string): Promise<void> {\n return this.http.delete(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Search expenses with parameters using POST\n * @param params Search parameters\n */\n async search(params: ExpenseListParams): Promise<NavigablePage<Expense>> {\n const response = await this.http.post<Page<Expense>>(`${this.basePath}/search`, params);\n return this.createNavigablePage(response, (page) => this.search({ ...params, page }));\n }\n\n /**\n * Update expense status\n * @param id Expense ID\n * @param data Status update data\n * @returns Updated expense\n */\n async updateStatus(id: string, data: ExpenseStatus): Promise<Expense> {\n return this.http.put<Expense>(`${this.basePath}/${id}/status`, data);\n }\n}\n","import type { ApiClient } from '../http';\nimport type { Note, NoteCreateRequest, NoteListParams, NoteUpdateRequest, Page } from '../models';\nimport { NavigablePage } from '../models';\nimport { DateUtils } from '../utils/date';\nimport { Resource } from './Resource';\n\nexport class NoteResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/notes');\n }\n\n async list(params?: NoteListParams): Promise<NavigablePage<Note>> {\n const response = await this.http.get<Page<Note>, NoteListParams>(this.basePath, params);\n return new NavigablePage(response, (page) => this.list({ ...params, page }));\n }\n\n async create(data: NoteCreateRequest): Promise<Note> {\n const formattedData = {\n ...data,\n dateTime: DateUtils.formatTimestamp(data.dateTime),\n };\n return this.http.post<Note>(this.basePath, formattedData);\n }\n\n async update(id: string, data: NoteUpdateRequest): Promise<Note> {\n const formattedData = {\n ...data,\n dateTime: DateUtils.formatTimestamp(data.dateTime),\n };\n return this.http.put<Note>(`${this.basePath}/${encodeURIComponent(id)}`, formattedData);\n }\n\n async get(id: string): Promise<Note> {\n return this.http.get<Note>(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n async delete(id: string): Promise<void> {\n return this.http.delete(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Search notes with parameters using POST\n * @param params Search parameters\n */\n async search(params: NoteListParams): Promise<NavigablePage<Note>> {\n const response = await this.http.post<Page<Note>>(`${this.basePath}/search`, params);\n return this.createNavigablePage(response, (page) => this.search({ ...params, page }));\n }\n}\n","import type { ApiClient } from '../http';\nimport type {\n Page,\n Pause,\n PauseCreateRequest,\n PauseListParams,\n PauseUpdateRequest,\n} from '../models';\nimport { NavigablePage } from '../models';\nimport { DateUtils } from '../utils/date';\nimport { Resource } from './Resource';\n\nexport class PauseResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/pauses');\n }\n\n async list(params?: PauseListParams): Promise<NavigablePage<Pause>> {\n const response = await this.http.get<Page<Pause>, PauseListParams>(this.basePath, params);\n return new NavigablePage(response, (page) => this.list({ ...params, page }));\n }\n\n async create(data: PauseCreateRequest): Promise<Pause> {\n const formattedData = {\n ...data,\n startDateTime: DateUtils.formatTimestamp(data.startDateTime),\n endDateTime: DateUtils.formatTimestamp(data.endDateTime),\n };\n return this.http.post<Pause>(this.basePath, formattedData);\n }\n\n async update(id: string, data: PauseUpdateRequest): Promise<Pause> {\n const formattedData = {\n ...data,\n startDateTime: DateUtils.formatTimestamp(data.startDateTime),\n endDateTime: DateUtils.formatTimestamp(data.endDateTime),\n };\n return this.http.put<Pause>(`${this.basePath}/${encodeURIComponent(id)}`, formattedData);\n }\n\n async get(id: string): Promise<Pause> {\n return this.http.get<Pause>(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n async delete(id: string): Promise<void> {\n return this.http.delete(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Search pauses with parameters using POST\n * @param params Search parameters\n */\n async search(params: PauseListParams): Promise<NavigablePage<Pause>> {\n const response = await this.http.post<Page<Pause>>(`${this.basePath}/search`, params);\n return this.createNavigablePage(response, (page) => this.search({ ...params, page }));\n }\n}\n","import type { ApiClient } from '../http';\nimport type { Profile, ProfileUpdateRequest } from '../models';\n\nexport class ProfileResource {\n constructor(private readonly http: ApiClient) {}\n\n async getProfile(): Promise<Profile> {\n return this.http.get<Profile>('/v1/profiles/me');\n }\n\n async updateProfile(data: ProfileUpdateRequest): Promise<Profile> {\n return this.http.put<Profile>('/v1/profiles/me', data);\n }\n}\n","import type { ApiClient } from '../http';\nimport type { Settings, SettingsUpdateRequest } from '../models';\n\nexport class SettingsResource {\n constructor(private readonly http: ApiClient) {}\n\n async get(): Promise<Settings> {\n return this.http.get<Settings>('/v1/settings');\n }\n\n async update(data: SettingsUpdateRequest): Promise<Settings> {\n return this.http.put<Settings>('/v1/settings', data);\n }\n}\n","import type { ApiClient } from '../http';\nimport type {\n Automation,\n AutomationCreateRequest,\n AutomationListParams,\n AutomationUpdateRequest,\n Page,\n} from '../models';\nimport { NavigablePage } from '../models';\nimport { Resource } from './Resource';\n\nexport class AutomationResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/automations');\n }\n\n async list(params?: AutomationListParams): Promise<NavigablePage<Automation>> {\n const response = await this.http.get<Page<Automation>, AutomationListParams>(\n this.basePath,\n params,\n );\n return new NavigablePage(response, (page) => this.list({ ...params, page }));\n }\n\n async create(data: AutomationCreateRequest): Promise<Automation> {\n return this.http.post<Automation>(this.basePath, data);\n }\n\n async update(id: string, data: AutomationUpdateRequest): Promise<Automation> {\n return this.http.put<Automation>(`${this.basePath}/${encodeURIComponent(id)}`, data);\n }\n\n async get(id: string): Promise<Automation> {\n return this.http.get<Automation>(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n async delete(id: string): Promise<void> {\n return this.http.delete(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Search automations with parameters using POST\n * @param params Search parameters\n */\n async search(params: AutomationListParams): Promise<NavigablePage<Automation>> {\n const response = await this.http.post<Page<Automation>>(`${this.basePath}/search`, params);\n return this.createNavigablePage(response, (page) => this.search({ ...params, page }));\n }\n}\n","import type { ApiClient } from '../http';\nimport type {\n Document,\n DocumentCreateRequest,\n DocumentListParams,\n DocumentUpdateRequest,\n Page,\n} from '../models';\nimport { NavigablePage } from '../models';\nimport { Resource } from './Resource';\n\nexport class DocumentResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/documents');\n }\n\n async list(params?: DocumentListParams): Promise<NavigablePage<Document>> {\n const response = await this.http.get<Page<Document>, DocumentListParams>(this.basePath, params);\n return new NavigablePage(response, (page) => this.list({ ...params, page }));\n }\n\n async create(data: DocumentCreateRequest): Promise<Document> {\n return this.http.post<Document>(this.basePath, data);\n }\n\n async update(id: string, data: DocumentUpdateRequest): Promise<Document> {\n return this.http.put<Document>(`${this.basePath}/${encodeURIComponent(id)}`, data);\n }\n\n async get(id: string): Promise<Document> {\n return this.http.get<Document>(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n async delete(id: string): Promise<void> {\n return this.http.delete(`${this.basePath}/${encodeURIComponent(id)}`);\n }\n\n /**\n * Search documents with parameters using POST\n * @param params Search parameters\n */\n async search(params: DocumentListParams): Promise<NavigablePage<Document>> {\n const response = await this.http.post<Page<Document>>(`${this.basePath}/search`, params);\n return this.createNavigablePage(response, (page) => this.search({ ...params, page }));\n }\n}\n","import type { ApiClient } from '../http';\nimport type {\n Timer,\n TimerPauseRequest,\n TimerResumeRequest,\n TimerStartRequest,\n TimerStopRequest,\n TimerUpdateRequest,\n} from '../models';\nimport { DateUtils } from '../utils/date';\n\nexport class TimerResource {\n constructor(private readonly http: ApiClient) {}\n\n async get(): Promise<Timer> {\n return this.http.get<Timer>('/v1/timer');\n }\n\n async start(data: TimerStartRequest): Promise<Timer> {\n // Format startDateTime if provided\n const formattedData = {\n ...data,\n startDateTime: data.startDateTime\n ? DateUtils.formatTimestamp(data.startDateTime)\n : DateUtils.formatTimestamp(),\n };\n return this.http.post<Timer>('/v1/timer/start', formattedData);\n }\n\n async stop(data?: TimerStopRequest): Promise<Timer> {\n // Format endDateTime if provided\n const formattedData = data\n ? {\n ...data,\n endDateTime: data.endDateTime\n ? DateUtils.formatTimestamp(data.endDateTime)\n : DateUtils.formatTimestamp(),\n }\n : { endDateTime: DateUtils.formatTimestamp() };\n return this.http.post<Timer>('/v1/timer/stop', formattedData);\n }\n\n async pause(data?: TimerPauseRequest): Promise<Timer> {\n // Format startDateTime if provided\n const formattedData = data\n ? {\n ...data,\n startDateTime: data.startDateTime\n ? DateUtils.formatTimestamp(data.startDateTime)\n : DateUtils.formatTimestamp(),\n }\n : { startDateTime: DateUtils.formatTimestamp() };\n return this.http.post<Timer>('/v1/timer/pause', formattedData);\n }\n\n async resume(data?: TimerResumeRequest): Promise<Timer> {\n // Format endDateTime if provided\n const formattedData = data\n ? {\n ...data,\n endDateTime: data.endDateTime\n ? DateUtils.formatTimestamp(data.endDateTime)\n : DateUtils.formatTimestamp(),\n }\n : { endDateTime: DateUtils.formatTimestamp() };\n return this.http.post<Timer>('/v1/timer/resume', formattedData);\n }\n\n async update(data: TimerUpdateRequest): Promise<Timer> {\n // Format startDateTime if provided\n const formattedData = {\n ...data,\n startDateTime: data.startDateTime ? DateUtils.formatTimestamp(data.startDateTime) : undefined,\n };\n return this.http.put<Timer>('/v1/timer/update', formattedData);\n }\n}\n","import type { ApiClient } from '../http';\nimport type { Page, Todo, TodoCreateRequest, TodoListParams, TodoUpdateRequest } from '../models';\nimport { NavigablePage } from '../models';\nimport { DateUtils } from '../utils/date';\nimport { Resource } from './Resource';\n\nexport class TodoResource extends Resource {\n constructor(client: ApiClient) {\n super(client, '/v1/todos');\n }\n\n async list(params?: TodoListParams): Promise<NavigablePage<Todo>> {\n const response = await this.http.get<Page<Todo>, TodoListParams>(this.basePath, params);\n return new NavigablePage(response, (page) => this.list({ ...params, page }));\n }\n\n async create(data: TodoCreateRequest): Promise<Todo> {\n const formattedData = {\n ...data,\n