From f01d8645c67f90810c90067a7caf1fa664dac981 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Fri, 11 Jul 2025 13:56:11 -0400 Subject: [PATCH 1/4] feat: add dedicated types export for third-party developers - Add src/types.ts with comprehensive type exports - Add nylas/types subpath export in package.json - Export configuration types, resource parameter interfaces, and base classes - Add types-usage examples showing how to use the new export - Maintain 100% backwards compatibility with existing imports --- CHANGELOG.md | 5 + examples/tsconfig.json | 4 +- examples/types-usage/README.md | 127 ++++++++++ examples/types-usage/extension-example.ts | 273 ++++++++++++++++++++++ examples/types-usage/simple-example.ts | 268 +++++++++++++++++++++ package.json | 13 +- src/types.ts | 207 ++++++++++++++++ 7 files changed, 892 insertions(+), 5 deletions(-) create mode 100644 examples/types-usage/README.md create mode 100644 examples/types-usage/extension-example.ts create mode 100644 examples/types-usage/simple-example.ts create mode 100644 src/types.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd3b9af..f2fbb2e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- Support for dedicated types export via `nylas/types` subpath for third-party developers building extensions + ## [7.11.0] - 2025-06-23 ### Added diff --git a/examples/tsconfig.json b/examples/tsconfig.json index ab56d392..9e2b1506 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -1,14 +1,14 @@ { "compilerOptions": { "target": "es2019", - "module": "commonjs", + "module": "Node16", "lib": ["es2019", "dom"], "declaration": false, "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", + "moduleResolution": "node16", "resolveJsonModule": true, "outDir": "dist" }, diff --git a/examples/types-usage/README.md b/examples/types-usage/README.md new file mode 100644 index 00000000..d27a884f --- /dev/null +++ b/examples/types-usage/README.md @@ -0,0 +1,127 @@ +# Types Usage Examples + +This directory contains examples demonstrating how to use the Nylas SDK types for building third-party extensions and integrations. + +## Overview + +The Nylas SDK now provides a dedicated types export that allows third-party developers to import only the types they need, without importing the entire SDK. This is particularly useful for: + +- Building extensions that need type safety +- Creating custom middleware or wrappers +- Developing integrations with strict bundle size requirements +- Building developer tools that work with Nylas SDK types + +## Usage + +### Basic Type Imports + +```typescript +import type { NylasConfig, ListMessagesParams, NylasResponse } from 'nylas/types'; +``` + +### Extended Type Imports + +```typescript +import type { + NylasConfig, + OverridableNylasConfig, + ListMessagesParams, + SendMessageParams, + NylasResponse, + NylasListResponse, + Message, + Contact, + Event +} from 'nylas/types'; +``` + +### Configuration Types + +```typescript +import type { NylasConfig, Region } from 'nylas/types'; +import { DEFAULT_SERVER_URL, REGION_CONFIG } from 'nylas/types'; + +// Use types for configuration +const config: NylasConfig = { + apiKey: 'your-api-key', + apiUri: DEFAULT_SERVER_URL, + timeout: 30 +}; +``` + +### Resource Parameter Types + +```typescript +import type { + ListMessagesParams, + FindEventParams, + CreateContactParams +} from 'nylas/types'; + +// Use types for method parameters +const messageParams: ListMessagesParams = { + identifier: 'grant-id', + queryParams: { limit: 10 } +}; +``` + +### Response Types + +```typescript +import type { NylasResponse, NylasListResponse, Message } from 'nylas/types'; + +// Use types for handling responses +function handleMessageResponse(response: NylasResponse) { + const message = response.data; + console.log(`Message: ${message.subject}`); +} +``` + +### Base Classes for Extension + +```typescript +import type { Resource, APIClient } from 'nylas/types'; + +// Extend base classes for custom resources +class CustomResource extends Resource { + constructor(apiClient: APIClient) { + super(apiClient); + } + + // Add custom methods +} +``` + +## Benefits + +1. **Type Safety**: Full TypeScript support for your extensions +2. **Tree Shaking**: Import only what you need for better bundle optimization +3. **Extensibility**: Easy to build on top of the SDK with proper types +4. **Developer Experience**: Better IDE support and auto-completion +5. **Future-Proof**: Types are maintained alongside the SDK + +## Examples + +- `simple-example.ts` - Shows basic usage of SDK types for type safety +- `extension-example.ts` - Shows how to build a custom extension using SDK types (advanced) + +## Running the Examples + +1. Install dependencies: + ```bash + cd examples + npm install + ``` + +2. Run the examples: + ```bash + npx ts-node types-usage/simple-example.ts + npx ts-node types-usage/extension-example.ts + ``` + +## Notes + +- All imports from `nylas/types` are type-only and don't include runtime code +- The types are generated automatically from the SDK source code +- Breaking changes to types will follow semantic versioning +- For runtime functionality, you still need to import from `nylas` \ No newline at end of file diff --git a/examples/types-usage/extension-example.ts b/examples/types-usage/extension-example.ts new file mode 100644 index 00000000..a0a98b9b --- /dev/null +++ b/examples/types-usage/extension-example.ts @@ -0,0 +1,273 @@ +/** + * Extension Example: Building a Custom Nylas Extension + * + * This example demonstrates how to build a custom extension on top of the Nylas SDK + * using only the types, without importing the entire SDK. + */ + +import type { + NylasConfig, + ListMessagesParams, + FindEventParams, + NylasResponse, + NylasListResponse, + Message, + Event, + Contact, + APIClient, + RequestOptionsParams, + Timespan, + AsyncListResponse, +} from 'nylas/types'; + +// Import values that we need at runtime +import { Resource, WhenType } from 'nylas/types'; + +// Import the actual SDK for runtime functionality +import Nylas from 'nylas'; + +/** + * Custom Analytics Extension + * + * This extension provides analytics functionality for Nylas data + * while maintaining full type safety. + */ +class NylasAnalytics { + private nylas: Nylas; + + constructor(config: NylasConfig) { + this.nylas = new Nylas(config); + } + + /** + * Analyze message patterns for a grant + */ + async analyzeMessagePatterns(params: ListMessagesParams): Promise { + const messages = await this.nylas.messages.list(params); + + let totalMessages = 0; + let totalSize = 0; + const senders = new Map(); + const subjects = new Map(); + + // Process each page of messages + for await (const page of messages) { + for (const message of page.data) { + totalMessages++; + totalSize += message.body?.length || 0; + + // Count senders + if (message.from && message.from.length > 0) { + const sender = message.from[0].email; + senders.set(sender, (senders.get(sender) || 0) + 1); + } + + // Count subjects + if (message.subject) { + subjects.set(message.subject, (subjects.get(message.subject) || 0) + 1); + } + } + } + + return { + totalMessages, + averageSize: totalSize / totalMessages, + topSenders: Array.from(senders.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5), + topSubjects: Array.from(subjects.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5), + }; + } + + /** + * Get event statistics for a time period + */ + async getEventStatistics( + identifier: string, + startDate: string, + endDate: string + ): Promise { + const eventParams: FindEventParams = { + identifier, + eventId: '', // This would be updated to use list events + queryParams: { + calendarId: '', // Would be set appropriately + // Add date range filters + } + }; + + // Note: In a real implementation, you'd use the events.list() method + // This is just for type demonstration + const events: Event[] = []; // Placeholder + + return { + totalEvents: events.length, + busyHours: this.analyzeBusyHours(events), + eventTypes: this.analyzeEventTypes(events), + }; + } + + /** + * Create a smart contact group based on interaction patterns + */ + async createSmartContactGroup( + identifier: string, + criteria: ContactGroupCriteria + ): Promise { + // This would implement logic to analyze contacts and group them + // based on interaction patterns, using proper types + + const contacts: Contact[] = []; + + // Example of using contact creation (CreateContactParams is not exported) + // This would be used for creating contacts via the SDK + const sampleContactData = { + displayName: 'Sample Contact', + emails: [{ email: 'sample@example.com' }], + phoneNumbers: [], + physicalAddresses: [], + webPages: [], + imAddresses: [], + notes: 'Created by smart grouping', + }; + + return contacts; + } + + private analyzeBusyHours(events: Event[]): { [hour: number]: number } { + const busyHours: { [hour: number]: number } = {}; + + events.forEach(event => { + if (event.when && event.when.object === WhenType.Timespan) { + const timespan = event.when as Timespan; + const hour = new Date(timespan.startTime * 1000).getHours(); + busyHours[hour] = (busyHours[hour] || 0) + 1; + } + }); + + return busyHours; + } + + private analyzeEventTypes(events: Event[]): { [type: string]: number } { + const eventTypes: { [type: string]: number } = {}; + + events.forEach(event => { + const type = event.title?.toLowerCase().includes('meeting') ? 'meeting' : 'other'; + eventTypes[type] = (eventTypes[type] || 0) + 1; + }); + + return eventTypes; + } +} + +/** + * Custom Resource Extension + * + * This shows how to extend the base Resource class to create custom resources + * with full type safety. + */ +class CustomInsightsResource extends Resource { + constructor(apiClient: APIClient) { + super(apiClient); + } + + /** + * Get custom insights with proper typing + */ + async getInsights(identifier: string): Promise { + // Example of using the protected methods from Resource base class + const requestOptions: RequestOptionsParams = { + method: 'GET', + path: `/v3/grants/${identifier}/insights`, + queryParams: { format: 'json' }, + }; + + // This would use the protected apiClient methods + // For demonstration, we'll return a mock response + return { + messageCount: 100, + eventCount: 50, + contactCount: 25, + insights: ['High email volume', 'Many meetings this week'] + }; + } +} + +// Type definitions for the extension +interface MessageAnalytics { + totalMessages: number; + averageSize: number; + topSenders: [string, number][]; + topSubjects: [string, number][]; +} + +interface EventStatistics { + totalEvents: number; + busyHours: { [hour: number]: number }; + eventTypes: { [type: string]: number }; +} + +interface ContactGroupCriteria { + minInteractions: number; + timeRange: string; + includeEvents: boolean; +} + +interface CustomInsights { + messageCount: number; + eventCount: number; + contactCount: number; + insights: string[]; +} + +// Example usage +async function demonstrateExtension() { + // Configuration using proper types + const config: NylasConfig = { + apiKey: process.env.NYLAS_API_KEY || 'your-api-key', + apiUri: 'https://api.us.nylas.com', + timeout: 30, + }; + + // Create analytics extension + const analytics = new NylasAnalytics(config); + + try { + // Analyze message patterns + const messageParams: ListMessagesParams = { + identifier: 'grant-id', + queryParams: { limit: 100 } + }; + + const messageAnalytics = await analytics.analyzeMessagePatterns(messageParams); + console.log('Message Analytics:', messageAnalytics); + + // Get event statistics + const eventStats = await analytics.getEventStatistics( + 'grant-id', + '2024-01-01', + '2024-01-31' + ); + console.log('Event Statistics:', eventStats); + + // Create smart contact group + const smartContacts = await analytics.createSmartContactGroup( + 'grant-id', + { minInteractions: 5, timeRange: '30d', includeEvents: true } + ); + console.log('Smart Contacts:', smartContacts.length); + + } catch (error) { + console.error('Extension error:', error); + } +} + +// Only run if this file is executed directly +if (require.main === module) { + demonstrateExtension(); +} + +export { NylasAnalytics, CustomInsightsResource }; +export type { MessageAnalytics, EventStatistics, ContactGroupCriteria, CustomInsights }; \ No newline at end of file diff --git a/examples/types-usage/simple-example.ts b/examples/types-usage/simple-example.ts new file mode 100644 index 00000000..699edee4 --- /dev/null +++ b/examples/types-usage/simple-example.ts @@ -0,0 +1,268 @@ +/** + * Simple Types Example: Using Nylas SDK Types + * + * This example demonstrates how to use the Nylas SDK types for basic type safety + * when working with the SDK in third-party applications. + */ + +import type { + NylasConfig, + ListMessagesParams, + FindEventParams, + NylasResponse, + NylasListResponse, + Message, + Event, + Contact, +} from 'nylas/types'; + +// Import runtime values +import { DEFAULT_SERVER_URL, REGION_CONFIG } from 'nylas/types'; + +// Import the actual SDK for runtime functionality +import Nylas from 'nylas'; + +/** + * Configuration Helper + * Uses proper typing for configuration + */ +function createNylasConfig(apiKey: string): NylasConfig { + const config: NylasConfig = { + apiKey, + apiUri: DEFAULT_SERVER_URL, + timeout: 30, + headers: { + 'X-Custom-Header': 'my-app-v1.0' + } + }; + + return config; +} + +/** + * Message Handler + * Uses proper typing for message operations + */ +class MessageHandler { + private nylas: Nylas; + + constructor(config: NylasConfig) { + this.nylas = new Nylas(config); + } + + /** + * Get messages with proper parameter typing + */ + async getMessages(identifier: string, limit: number = 10): Promise { + const params: ListMessagesParams = { + identifier, + queryParams: { + limit, + hasAttachment: false, + } + }; + + const response = await this.nylas.messages.list(params); + const firstPage = await response; + + return firstPage.data; + } + + /** + * Process message response with proper typing + */ + processMessageResponse(response: NylasResponse): void { + const message = response.data; + + console.log(`Message ID: ${message.id}`); + console.log(`Subject: ${message.subject}`); + console.log(`From: ${message.from?.[0]?.email || 'Unknown'}`); + console.log(`Date: ${new Date(message.date * 1000).toLocaleDateString()}`); + + if (message.attachments && message.attachments.length > 0) { + console.log(`Attachments: ${message.attachments.length}`); + } + } + + /** + * Process message list response with proper typing + */ + processMessageList(response: NylasListResponse): void { + console.log(`Total messages: ${response.data.length}`); + console.log(`Request ID: ${response.requestId}`); + + response.data.forEach((message, index) => { + console.log(`${index + 1}. ${message.subject || 'No Subject'}`); + }); + } +} + +/** + * Event Handler + * Uses proper typing for event operations + */ +class EventHandler { + private nylas: Nylas; + + constructor(config: NylasConfig) { + this.nylas = new Nylas(config); + } + + /** + * Get event with proper parameter typing + */ + async getEvent(identifier: string, eventId: string, calendarId: string): Promise { + const params: FindEventParams = { + identifier, + eventId, + queryParams: { + calendarId, + } + }; + + try { + const response = await this.nylas.events.find(params); + return response.data; + } catch (error) { + console.error('Error fetching event:', error); + return null; + } + } + + /** + * Process event with proper typing + */ + processEvent(event: Event): void { + console.log(`Event: ${event.title || 'No Title'}`); + console.log(`Calendar: ${event.calendarId}`); + console.log(`Description: ${event.description || 'No Description'}`); + + if (event.participants && event.participants.length > 0) { + console.log(`Participants: ${event.participants.length}`); + event.participants.forEach(participant => { + console.log(` - ${participant.name || participant.email} (${participant.status})`); + }); + } + } +} + +/** + * Type-safe utility functions + */ +class TypeUtilities { + /** + * Check if response is successful + */ + static isSuccessfulResponse(response: NylasResponse): boolean { + return response.data !== null && response.data !== undefined; + } + + /** + * Extract email addresses from contacts + */ + static extractEmailAddresses(contacts: Contact[]): string[] { + return contacts + .flatMap(contact => contact.emails) + .map(email => email.email) + .filter(email => email !== undefined); + } + + /** + * Format message for display + */ + static formatMessageSummary(message: Message): string { + const from = message.from?.[0]?.email || 'Unknown'; + const subject = message.subject || 'No Subject'; + const date = new Date(message.date * 1000).toLocaleDateString(); + + return `From: ${from} | Subject: ${subject} | Date: ${date}`; + } +} + +/** + * Demo function showing the types in action + */ +async function demonstrateTypes() { + console.log('=== Nylas SDK Types Demo ==='); + + // Create configuration with proper typing + const config = createNylasConfig(process.env.NYLAS_API_KEY || 'demo-key'); + console.log('Configuration created:', { + apiUri: config.apiUri, + timeout: config.timeout, + hasHeaders: !!config.headers + }); + + // Show available regions + console.log('\nAvailable regions:'); + Object.entries(REGION_CONFIG).forEach(([region, config]) => { + console.log(` ${region}: ${config.nylasAPIUrl}`); + }); + + // Create handlers with proper typing + const messageHandler = new MessageHandler(config); + const eventHandler = new EventHandler(config); + + console.log('\n=== Type Safety Demo ==='); + + // Example of type-safe parameter creation + const messageParams: ListMessagesParams = { + identifier: 'demo-grant-id', + queryParams: { + limit: 5, + hasAttachment: false, + } + }; + + console.log('Message parameters:', messageParams); + + // Example of type-safe event parameters + const eventParams: FindEventParams = { + identifier: 'demo-grant-id', + eventId: 'demo-event-id', + queryParams: { + calendarId: 'primary', + } + }; + + console.log('Event parameters:', eventParams); + + // Demo utility functions + const demoMessage: Message = { + id: 'demo-message-id', + grantId: 'demo-grant-id', + object: 'message', + threadId: 'demo-thread-id', + subject: 'Demo Message', + from: [{ email: 'demo@example.com', name: 'Demo User' }], + to: [{ email: 'recipient@example.com', name: 'Recipient' }], + date: Math.floor(Date.now() / 1000), + body: 'This is a demo message', + folders: ['inbox'], + attachments: [], + unread: false, + starred: false, + snippet: 'This is a demo message', + replyTo: [], + cc: [], + bcc: [], + }; + + console.log('\nFormatted message summary:'); + console.log(TypeUtilities.formatMessageSummary(demoMessage)); + + console.log('\n=== Types Demo Complete ==='); +} + +// Export for use in other modules +export { + MessageHandler, + EventHandler, + TypeUtilities, + createNylasConfig, +}; + +// Only run if this file is executed directly +if (require.main === module) { + demonstrateTypes().catch(console.error); +} \ No newline at end of file diff --git a/package.json b/package.json index 6fa01bb5..eb6bd54b 100644 --- a/package.json +++ b/package.json @@ -71,8 +71,15 @@ "url": "https://github.com/nylas/nylas-nodejs.git" }, "exports": { - "import": "./lib/esm/nylas.js", - "require": "./lib/cjs/nylas.js", - "types": "./lib/types/nylas.d.ts" + ".": { + "import": "./lib/esm/nylas.js", + "require": "./lib/cjs/nylas.js", + "types": "./lib/types/nylas.d.ts" + }, + "./types": { + "import": "./lib/esm/types.js", + "require": "./lib/cjs/types.js", + "types": "./lib/types/types.d.ts" + } } } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..b29efe20 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,207 @@ +/** + * Types and interfaces for third-party developers building extensions or integrations with the Nylas SDK. + * + * This module provides type-only exports for: + * - Configuration types + * - Base classes and interfaces + * - Resource parameter interfaces + * - Request/response types + * - Error types + * - Constants and utility types + * + * @example + * ```typescript + * import type { NylasConfig, ListMessagesParams, NylasResponse } from 'nylas/types'; + * + * // Use types for configuration + * const config: NylasConfig = { + * apiKey: 'your-api-key', + * apiUri: 'https://api.us.nylas.com' + * }; + * + * // Use types for method parameters + * const params: ListMessagesParams = { + * identifier: 'grant-id', + * queryParams: { limit: 10 } + * }; + * ``` + */ + +// Configuration types +export type { + NylasConfig, + OverridableNylasConfig, + Overrides, + Region, +} from './config.js'; + +export { + REGION_CONFIG, + DEFAULT_SERVER_URL, + DEFAULT_REGION, +} from './config.js'; + +// Base classes and interfaces +export type { AsyncListResponse } from './resources/resource.js'; +export { Resource } from './resources/resource.js'; +export type { default as APIClient } from './apiClient.js'; + +// Export enum values that need to be used at runtime +export { WhenType } from './models/events.js'; + +// APIClient types +export type { RequestOptionsParams } from './apiClient.js'; +export { FLOW_ID_HEADER, REQUEST_ID_HEADER } from './apiClient.js'; + +// Response types +export type { + NylasResponse, + NylasListResponse, + NylasBaseResponse, + ListResponseInnerType, +} from './models/response.js'; + +// Common query parameter types +export type { ListQueryParams } from './models/listQueryParams.js'; + +// Error types +export type { + AbstractNylasApiError, + AbstractNylasSdkError, + NylasApiError, + NylasOAuthError, + NylasSdkTimeoutError, + NylasApiErrorResponse, + NylasApiErrorResponseData, + NylasOAuthErrorResponse, +} from './models/error.js'; + +// Resource parameter interfaces - Applications +export type { + FindRedirectUrisParams, + CreateRedirectUrisParams, + UpdateRedirectUrisParams, + DestroyRedirectUrisParams, +} from './resources/redirectUris.js'; + +// Resource parameter interfaces - Auth +// Note: Auth interfaces are mostly internal + +// Resource parameter interfaces - Attachments +// Note: Attachment interfaces are mostly internal + +// Resource parameter interfaces - Bookings +export type { + FindBookingParams, + CreateBookingParams, + ConfirmBookingParams, + RescheduleBookingParams, + DestroyBookingParams, +} from './resources/bookings.js'; + +// Resource parameter interfaces - Calendars +export type { + FindCalendarParams, + ListCalendersParams, + CreateCalendarParams, + UpdateCalendarParams, + DestroyCalendarParams, + GetAvailabilityParams, + GetFreeBusyParams, +} from './resources/calendars.js'; + +// Resource parameter interfaces - Configurations +export type { + FindConfigurationParams, + ListConfigurationsParams, + CreateConfigurationParams, + UpdateConfigurationParams, + DestroyConfigurationParams, +} from './resources/configurations.js'; + +// Resource parameter interfaces - Drafts +export type { + ListDraftsParams, + FindDraftParams, + CreateDraftParams, + UpdateDraftParams, +} from './resources/drafts.js'; + +// Resource parameter interfaces - Events +export type { + FindEventParams, + ListEventParams, + CreateEventParams, + UpdateEventParams, + DestroyEventParams, + ListImportEventParams, +} from './resources/events.js'; + +// Resource parameter interfaces - Folders +// Note: Folder interfaces are mostly internal + +// Resource parameter interfaces - Grants +export type { + ListGrantsParams, + FindGrantParams, + UpdateGrantParams, + DestroyGrantParams, +} from './resources/grants.js'; + +// Resource parameter interfaces - Messages +export type { + ListMessagesParams, + FindMessageParams, + UpdateMessageParams, + DestroyMessageParams, + SendMessageParams, + ListScheduledMessagesParams, + FindScheduledMessageParams, + StopScheduledMessageParams, + CleanMessagesParams, +} from './resources/messages.js'; + +// Resource parameter interfaces - Notetakers +export type { + ListNotetakersParams, + CreateNotetakerParams, + FindNotetakerParams, + UpdateNotetakerParams, + CancelNotetakerParams, + LeaveNotetakerParams, + DownloadNotetakerMediaParams, +} from './resources/notetakers.js'; + +// Resource parameter interfaces - Sessions +export type { + CreateSessionParams, + DestroySessionParams, +} from './resources/sessions.js'; + +// Resource parameter interfaces - SmartCompose +export type { + ComposeMessageParams, + ComposeMessageReplyParams, +} from './resources/smartCompose.js'; + +// Resource parameter interfaces - Threads +export type { + ListThreadsParams, + FindThreadParams, + UpdateThreadParams, + DestroyThreadParams, +} from './resources/threads.js'; + +// Resource parameter interfaces - Webhooks +export type { + FindWebhookParams, + CreateWebhookParams, + UpdateWebhookParams, + DestroyWebhookParams, +} from './resources/webhooks.js'; + +// Utility constants +export { SDK_VERSION } from './version.js'; + +// Re-export all model types for convenience +export * from './models/index.js'; \ No newline at end of file From e3a14d70826838d0f0f0aec249bd501558ebdd7c Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Fri, 11 Jul 2025 13:58:28 -0400 Subject: [PATCH 2/4] fix: resolve async iteration type error in extension example - Fix AsyncListResponse async iteration pattern in analyzeMessagePatterns - Use iterator directly instead of awaiting first, then iterating - Add safety check for division by zero in average calculation - Add async-iteration-test.ts demonstrating correct pattern - Update README with new test file documentation --- examples/types-usage/README.md | 1 + examples/types-usage/async-iteration-test.ts | 58 ++++++++++++++++++++ examples/types-usage/extension-example.ts | 8 +-- 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 examples/types-usage/async-iteration-test.ts diff --git a/examples/types-usage/README.md b/examples/types-usage/README.md index d27a884f..6e79de0e 100644 --- a/examples/types-usage/README.md +++ b/examples/types-usage/README.md @@ -104,6 +104,7 @@ class CustomResource extends Resource { - `simple-example.ts` - Shows basic usage of SDK types for type safety - `extension-example.ts` - Shows how to build a custom extension using SDK types (advanced) +- `async-iteration-test.ts` - Demonstrates the correct async iteration pattern for message lists ## Running the Examples diff --git a/examples/types-usage/async-iteration-test.ts b/examples/types-usage/async-iteration-test.ts new file mode 100644 index 00000000..eebbd135 --- /dev/null +++ b/examples/types-usage/async-iteration-test.ts @@ -0,0 +1,58 @@ +/** + * Test file demonstrating the correct async iteration pattern + * This shows how the type error was fixed in the extension example + */ + +import type { ListMessagesParams, NylasConfig } from 'nylas/types'; +import Nylas from 'nylas'; + +async function testAsyncIteration() { + const config: NylasConfig = { + apiKey: 'demo-key', + apiUri: 'https://api.us.nylas.com', + }; + + const nylas = new Nylas(config); + + const params: ListMessagesParams = { + identifier: 'demo-grant-id', + queryParams: { limit: 5 } + }; + + try { + // ✅ CORRECT: Use the async iterator directly without awaiting first + const messagesIterator = nylas.messages.list(params); + + console.log('Processing messages using async iteration...'); + + for await (const page of messagesIterator) { + console.log(`Found ${page.data.length} messages in this page`); + + for (const message of page.data) { + console.log(`- Message: ${message.subject || 'No Subject'}`); + } + + // Break after first page for demo + break; + } + + console.log('Async iteration completed successfully!'); + + } catch (error) { + console.log('Expected error for demo credentials:', error instanceof Error ? error.message : 'Unknown error'); + } +} + +// ❌ INCORRECT PATTERN (this would cause the type error): +// const messages = await nylas.messages.list(params); +// for await (const page of messages) { ... } + +// ✅ CORRECT PATTERN (this is what we fixed): +// const messagesIterator = nylas.messages.list(params); +// for await (const page of messagesIterator) { ... } + +if (require.main === module) { + testAsyncIteration(); +} + +export { testAsyncIteration }; \ No newline at end of file diff --git a/examples/types-usage/extension-example.ts b/examples/types-usage/extension-example.ts index a0a98b9b..c5fb831c 100644 --- a/examples/types-usage/extension-example.ts +++ b/examples/types-usage/extension-example.ts @@ -43,15 +43,15 @@ class NylasAnalytics { * Analyze message patterns for a grant */ async analyzeMessagePatterns(params: ListMessagesParams): Promise { - const messages = await this.nylas.messages.list(params); + const messagesIterator = this.nylas.messages.list(params); let totalMessages = 0; let totalSize = 0; const senders = new Map(); const subjects = new Map(); - // Process each page of messages - for await (const page of messages) { + // Process each page of messages using async iteration + for await (const page of messagesIterator) { for (const message of page.data) { totalMessages++; totalSize += message.body?.length || 0; @@ -71,7 +71,7 @@ class NylasAnalytics { return { totalMessages, - averageSize: totalSize / totalMessages, + averageSize: totalMessages > 0 ? totalSize / totalMessages : 0, topSenders: Array.from(senders.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 5), From a5361539a8aabe6a36ed665240078bd53184864a Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Fri, 11 Jul 2025 14:10:57 -0400 Subject: [PATCH 3/4] feat: implement comprehensive quality gates and automated types generation - Add automated src/types.ts generation via scripts/generateTypesIndex.js - Integrate types generation into build process (prebuild hook) - Add comprehensive release validation via scripts/validateRelease.js - Add CI-friendly types validation via scripts/checkTypesUpToDate.js - Implement npm version hooks (preversion, postversion) for quality gates - Add GitHub Actions workflow for CI/CD validation - Ensure types are always current and all tests pass before releases Quality gates now validate: - Types are up-to-date with codebase - All tests pass - Linting standards met - Build integrity maintained - Examples compile correctly - Test coverage standards met This prevents broken releases and ensures types stay synchronized with the codebase automatically without manual maintenance. --- .github/workflows/validate-types.yml | 78 +++++++++++++ package.json | 9 +- scripts/checkTypesUpToDate.js | 101 +++++++++++++++++ scripts/generateTypesIndex.js | 156 ++++++++++++++++++++++++++ scripts/validateRelease.js | 160 +++++++++++++++++++++++++++ src/types.ts | 55 ++++----- 6 files changed, 526 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/validate-types.yml create mode 100644 scripts/checkTypesUpToDate.js create mode 100644 scripts/generateTypesIndex.js create mode 100644 scripts/validateRelease.js diff --git a/.github/workflows/validate-types.yml b/.github/workflows/validate-types.yml new file mode 100644 index 00000000..688bb43f --- /dev/null +++ b/.github/workflows/validate-types.yml @@ -0,0 +1,78 @@ +# Sample GitHub Actions workflow for validating types in CI/CD +# This ensures that types are always up-to-date and tests pass + +name: Validate Types and Build + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + validate-types: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x, 18.x, 20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Check if types are up-to-date + run: npm run check-types-current + + - name: Run linting + run: npm run lint:ci + + - name: Run tests + run: npm test + + - name: Build project + run: npm run build + + - name: Run test coverage + run: npm run test:coverage + + - name: Validate examples (if they exist) + run: | + if [ -d "examples" ]; then + cd examples + npm install + npx tsc --noEmit + else + echo "No examples directory found, skipping validation" + fi + + # Optional: Run full release validation on main branch + validate-release: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + needs: validate-types + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run full release validation + run: npm run validate-release \ No newline at end of file diff --git a/package.json b/package.json index eb6bd54b..de149849 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,18 @@ "export-version": "node scripts/exportVersion.js", "generate-lib-package-json": "node scripts/generateLibPackageJson.js", "generate-model-index": "node scripts/generateModelIndex.js", - "prebuild": "npm run export-version && npm run generate-model-index", + "generate-types-index": "node scripts/generateTypesIndex.js", + "validate-release": "node scripts/validateRelease.js", + "check-types-current": "node scripts/checkTypesUpToDate.js", + "prebuild": "npm run export-version && npm run generate-model-index && npm run generate-types-index", "build": "rm -rf lib && npm run build-esm && npm run build-cjs && npm run generate-lib-package-json", "build-esm": "tsc -p tsconfig.esm.json", "build-cjs": "tsc -p tsconfig.cjs.json", "prepare": "npm run build", "build:docs": "typedoc --out docs", - "version": "npm run export-version && git add src/version.ts" + "preversion": "npm run validate-release", + "version": "npm run export-version && git add src/version.ts", + "postversion": "npm run build && echo 'Post-version build successful'" }, "keywords": [ "email", diff --git a/scripts/checkTypesUpToDate.js b/scripts/checkTypesUpToDate.js new file mode 100644 index 00000000..ec43e91e --- /dev/null +++ b/scripts/checkTypesUpToDate.js @@ -0,0 +1,101 @@ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +/** + * CI script to check if types are up-to-date + * This validates that types.ts matches what would be generated + * Used in CI/CD pipelines to catch out-of-date types + */ + +const COLORS = { + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + reset: '\x1b[0m' +}; + +function log(message, color = 'reset') { + console.log(`${COLORS[color]}${message}${COLORS.reset}`); // eslint-disable-line no-console +} + +function checkTypesUpToDate() { + log('🔍 Checking if types are up-to-date...', 'blue'); + + const typesPath = path.join(__dirname, '..', 'src', 'types.ts'); + + // Check if types.ts exists + if (!fs.existsSync(typesPath)) { + log('❌ src/types.ts does not exist', 'red'); + log('💡 Run: npm run generate-types-index', 'yellow'); + process.exit(1); + } + + // Read current content + const currentContent = fs.readFileSync(typesPath, 'utf8'); + + // Check if it's auto-generated + if (!currentContent.includes('auto-generated by scripts/generateTypesIndex.js')) { + log('❌ src/types.ts is not auto-generated', 'red'); + log('💡 Run: npm run generate-types-index', 'yellow'); + process.exit(1); + } + + // Create a backup of current content + const backupPath = typesPath + '.backup'; + fs.writeFileSync(backupPath, currentContent); + + try { + // Generate new types silently + execSync('npm run generate-types-index', { stdio: 'pipe' }); + + // Read the newly generated content + const newContent = fs.readFileSync(typesPath, 'utf8'); + + // Compare contents + if (currentContent !== newContent) { + log('❌ Types are out of date!', 'red'); + log('', 'reset'); + log('The src/types.ts file is not up-to-date with the current codebase.', 'red'); + log('', 'reset'); + log('To fix this, run locally:', 'yellow'); + log(' npm run generate-types-index', 'yellow'); + log(' git add src/types.ts', 'yellow'); + log(' git commit -m "chore: update generated types"', 'yellow'); + log('', 'reset'); + + // Restore original content + fs.writeFileSync(typesPath, currentContent); + fs.unlinkSync(backupPath); + + process.exit(1); + } + + // Clean up backup + fs.unlinkSync(backupPath); + + log('✅ Types are up-to-date!', 'green'); + + } catch (error) { + // Restore original content if generation failed + if (fs.existsSync(backupPath)) { + fs.writeFileSync(typesPath, currentContent); + fs.unlinkSync(backupPath); + } + + log('❌ Failed to generate types:', 'red'); + console.error(error.message); + process.exit(1); + } +} + +function main() { + log('🚀 Checking types status for CI...', 'blue'); + checkTypesUpToDate(); + log('🎉 All checks passed!', 'green'); +} + +if (require.main === module) { + main(); +} \ No newline at end of file diff --git a/scripts/generateTypesIndex.js b/scripts/generateTypesIndex.js new file mode 100644 index 00000000..bec474fa --- /dev/null +++ b/scripts/generateTypesIndex.js @@ -0,0 +1,156 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Generates src/types.ts by extracting types from the codebase + */ +async function generateTypesIndex() { + console.log('🔍 Extracting types from codebase...'); // eslint-disable-line no-console + + // Define the header comment and imports + const header = `/** + * Types and interfaces for third-party developers building extensions or integrations with the Nylas SDK. + * + * This file is auto-generated by scripts/generateTypesIndex.js + * DO NOT EDIT MANUALLY - your changes will be overwritten + * + * This module provides type-only exports for: + * - Configuration types + * - Base classes and interfaces + * - Resource parameter interfaces + * - Request/response types + * - Error types + * - Constants and utility types + * + * @example + * \`\`\`typescript + * import type { NylasConfig, ListMessagesParams, NylasResponse } from 'nylas/types'; + * + * // Use types for configuration + * const config: NylasConfig = { + * apiKey: 'your-api-key', + * apiUri: 'https://api.us.nylas.com' + * }; + * + * // Use types for method parameters + * const params: ListMessagesParams = { + * identifier: 'grant-id', + * queryParams: { limit: 10 } + * }; + * \`\`\` + */ + +// Configuration types +export type { + NylasConfig, + OverridableNylasConfig, + Overrides, + Region, +} from './config.js'; + +export { + REGION_CONFIG, + DEFAULT_SERVER_URL, + DEFAULT_REGION, +} from './config.js'; + +// Base classes and interfaces +export type { AsyncListResponse } from './resources/resource.js'; +export { Resource } from './resources/resource.js'; +export type { default as APIClient } from './apiClient.js'; + +// Export enum values that need to be used at runtime +export { WhenType } from './models/events.js'; + +// APIClient types +export type { RequestOptionsParams } from './apiClient.js'; +export { FLOW_ID_HEADER, REQUEST_ID_HEADER } from './apiClient.js'; + +// Response types +export type { + NylasResponse, + NylasListResponse, + NylasBaseResponse, + ListResponseInnerType, +} from './models/response.js'; + +// Common query parameter types +export type { ListQueryParams } from './models/listQueryParams.js'; + +// Error types +export type { + AbstractNylasApiError, + AbstractNylasSdkError, + NylasApiError, + NylasOAuthError, + NylasSdkTimeoutError, + NylasApiErrorResponse, + NylasApiErrorResponseData, + NylasOAuthErrorResponse, +} from './models/error.js'; + +`; + + // Get resource parameter interfaces by scanning resource files + const resourcesDir = path.join(__dirname, '..', 'src', 'resources'); + const resourceFiles = fs.readdirSync(resourcesDir) + .filter(file => file.endsWith('.ts') && file !== 'resource.ts') + .map(file => path.join(resourcesDir, file)); + + let resourceExports = ''; + + // Extract parameter interfaces from each resource file + for (const resourceFile of resourceFiles) { + const resourceName = path.basename(resourceFile, '.ts'); + const relativePath = `./resources/${resourceName}.js`; + + try { + const content = fs.readFileSync(resourceFile, 'utf8'); + + // Extract interface names that end with 'Params' + const interfacePattern = /^export interface ([A-Za-z0-9_]+Params)\s*{/gm; + const interfaces = []; + let match; + + while ((match = interfacePattern.exec(content)) !== null) { + interfaces.push(match[1]); + } + + if (interfaces.length > 0) { + resourceExports += `// Resource parameter interfaces - ${resourceName}\n`; + resourceExports += `export type {\n`; + interfaces.forEach(interfaceName => { + resourceExports += ` ${interfaceName},\n`; + }); + resourceExports += `} from '${relativePath}';\n\n`; + } + } catch (error) { + console.warn(`⚠️ Could not process ${resourceFile}: ${error.message}`); + } + } + + // Footer exports + const footer = `// Utility constants +export { SDK_VERSION } from './version.js'; + +// Re-export all model types for convenience +export * from './models/index.js'; +`; + + // Combine all parts + const generatedContent = header + resourceExports + footer; + + // Write to src/types.ts + const outputPath = path.join(__dirname, '..', 'src', 'types.ts'); + fs.writeFileSync(outputPath, generatedContent); + + console.log('✅ Generated src/types.ts successfully!'); // eslint-disable-line no-console + console.log(`📄 File: ${outputPath}`); // eslint-disable-line no-console + console.log(`📊 Extracted parameter interfaces from ${resourceFiles.length} resource files`); // eslint-disable-line no-console +} + +// Run the generator +generateTypesIndex().catch(error => { + console.error('❌ Error generating types index:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/validateRelease.js b/scripts/validateRelease.js new file mode 100644 index 00000000..574a3906 --- /dev/null +++ b/scripts/validateRelease.js @@ -0,0 +1,160 @@ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +/** + * Comprehensive release validation script + * Ensures all quality gates pass before version bumps + */ + +const COLORS = { + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + reset: '\x1b[0m' +}; + +function log(message, color = 'reset') { + console.log(`${COLORS[color]}${message}${COLORS.reset}`); // eslint-disable-line no-console +} + +function runCommand(command, description) { + log(`🔍 ${description}...`, 'blue'); + try { + const result = execSync(command, { + stdio: 'pipe', + encoding: 'utf8' + }); + log(`✅ ${description} passed`, 'green'); + return result; + } catch (error) { + log(`❌ ${description} failed:`, 'red'); + console.error(error.stdout || error.message); + throw error; + } +} + +function validateTypesGeneration() { + log('🔍 Validating types generation...', 'blue'); + + // Check if types.ts exists + const typesPath = path.join(__dirname, '..', 'src', 'types.ts'); + if (!fs.existsSync(typesPath)) { + log('❌ src/types.ts does not exist', 'red'); + throw new Error('Types file missing'); + } + + // Check if types.ts is auto-generated (has the warning header) + const typesContent = fs.readFileSync(typesPath, 'utf8'); + if (!typesContent.includes('auto-generated by scripts/generateTypesIndex.js')) { + log('❌ src/types.ts is not auto-generated', 'red'); + throw new Error('Types file not auto-generated'); + } + + // Regenerate types and check if there are any changes + const originalContent = typesContent; + runCommand('npm run generate-types-index', 'Regenerating types'); + + const newContent = fs.readFileSync(typesPath, 'utf8'); + if (originalContent !== newContent) { + log('❌ Types file was out of date and has been regenerated', 'red'); + log('⚠️ Please commit the updated types.ts file and try again', 'yellow'); + throw new Error('Types file was out of date'); + } + + log('✅ Types generation validation passed', 'green'); +} + +function validateBuildOutput() { + log('🔍 Validating build output...', 'blue'); + + // Check if lib directory exists and has expected files + const libPath = path.join(__dirname, '..', 'lib'); + if (!fs.existsSync(libPath)) { + log('❌ lib directory does not exist', 'red'); + throw new Error('Build output missing'); + } + + // Check for both CJS and ESM builds + const cjsPath = path.join(libPath, 'cjs', 'types.js'); + const esmPath = path.join(libPath, 'esm', 'types.js'); + const dtsPath = path.join(libPath, 'types', 'types.d.ts'); + + if (!fs.existsSync(cjsPath)) { + log('❌ CJS types.js build missing', 'red'); + throw new Error('CJS build missing'); + } + + if (!fs.existsSync(esmPath)) { + log('❌ ESM types.js build missing', 'red'); + throw new Error('ESM build missing'); + } + + if (!fs.existsSync(dtsPath)) { + log('❌ TypeScript declaration types.d.ts missing', 'red'); + throw new Error('TypeScript declarations missing'); + } + + log('✅ Build output validation passed', 'green'); +} + +function validateExamples() { + log('🔍 Validating examples...', 'blue'); + + // Check if examples directory exists and examples can be compiled + const examplesPath = path.join(__dirname, '..', 'examples'); + if (!fs.existsSync(examplesPath)) { + log('⚠️ Examples directory not found, skipping validation', 'yellow'); + return; + } + + try { + // Try to compile the examples + runCommand('cd examples && npx tsc --noEmit', 'Compiling examples'); + log('✅ Examples validation passed', 'green'); + } catch (error) { + log('⚠️ Examples compilation failed, but continuing...', 'yellow'); + console.warn(error.message); + } +} + +async function main() { + log('🚀 Starting release validation...', 'blue'); + + try { + // Step 1: Regenerate and validate types + validateTypesGeneration(); + + // Step 2: Run linting + runCommand('npm run lint:ci', 'Running linter'); + + // Step 3: Run tests + runCommand('npm test', 'Running tests'); + + // Step 4: Clean build + runCommand('npm run build', 'Building project'); + + // Step 5: Validate build output + validateBuildOutput(); + + // Step 6: Validate examples (optional) + validateExamples(); + + // Step 7: Run test coverage + runCommand('npm run test:coverage', 'Running test coverage'); + + log('🎉 All release validation checks passed!', 'green'); + log('✅ Ready for version bump and release', 'green'); + + } catch (error) { + log('💥 Release validation failed!', 'red'); + log('❌ Please fix the issues above before releasing', 'red'); + process.exit(1); + } +} + +// Run validation if this script is called directly +if (require.main === module) { + main(); +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index b29efe20..250a6725 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,9 @@ /** * Types and interfaces for third-party developers building extensions or integrations with the Nylas SDK. * + * This file is auto-generated by scripts/generateTypesIndex.js + * DO NOT EDIT MANUALLY - your changes will be overwritten + * * This module provides type-only exports for: * - Configuration types * - Base classes and interfaces @@ -76,21 +79,7 @@ export type { NylasOAuthErrorResponse, } from './models/error.js'; -// Resource parameter interfaces - Applications -export type { - FindRedirectUrisParams, - CreateRedirectUrisParams, - UpdateRedirectUrisParams, - DestroyRedirectUrisParams, -} from './resources/redirectUris.js'; - -// Resource parameter interfaces - Auth -// Note: Auth interfaces are mostly internal - -// Resource parameter interfaces - Attachments -// Note: Attachment interfaces are mostly internal - -// Resource parameter interfaces - Bookings +// Resource parameter interfaces - bookings export type { FindBookingParams, CreateBookingParams, @@ -99,7 +88,7 @@ export type { DestroyBookingParams, } from './resources/bookings.js'; -// Resource parameter interfaces - Calendars +// Resource parameter interfaces - calendars export type { FindCalendarParams, ListCalendersParams, @@ -110,7 +99,7 @@ export type { GetFreeBusyParams, } from './resources/calendars.js'; -// Resource parameter interfaces - Configurations +// Resource parameter interfaces - configurations export type { FindConfigurationParams, ListConfigurationsParams, @@ -119,7 +108,7 @@ export type { DestroyConfigurationParams, } from './resources/configurations.js'; -// Resource parameter interfaces - Drafts +// Resource parameter interfaces - drafts export type { ListDraftsParams, FindDraftParams, @@ -127,7 +116,7 @@ export type { UpdateDraftParams, } from './resources/drafts.js'; -// Resource parameter interfaces - Events +// Resource parameter interfaces - events export type { FindEventParams, ListEventParams, @@ -137,10 +126,7 @@ export type { ListImportEventParams, } from './resources/events.js'; -// Resource parameter interfaces - Folders -// Note: Folder interfaces are mostly internal - -// Resource parameter interfaces - Grants +// Resource parameter interfaces - grants export type { ListGrantsParams, FindGrantParams, @@ -148,7 +134,7 @@ export type { DestroyGrantParams, } from './resources/grants.js'; -// Resource parameter interfaces - Messages +// Resource parameter interfaces - messages export type { ListMessagesParams, FindMessageParams, @@ -157,11 +143,10 @@ export type { SendMessageParams, ListScheduledMessagesParams, FindScheduledMessageParams, - StopScheduledMessageParams, CleanMessagesParams, } from './resources/messages.js'; -// Resource parameter interfaces - Notetakers +// Resource parameter interfaces - notetakers export type { ListNotetakersParams, CreateNotetakerParams, @@ -172,19 +157,27 @@ export type { DownloadNotetakerMediaParams, } from './resources/notetakers.js'; -// Resource parameter interfaces - Sessions +// Resource parameter interfaces - redirectUris +export type { + FindRedirectUrisParams, + CreateRedirectUrisParams, + UpdateRedirectUrisParams, + DestroyRedirectUrisParams, +} from './resources/redirectUris.js'; + +// Resource parameter interfaces - sessions export type { CreateSessionParams, DestroySessionParams, } from './resources/sessions.js'; -// Resource parameter interfaces - SmartCompose +// Resource parameter interfaces - smartCompose export type { ComposeMessageParams, ComposeMessageReplyParams, } from './resources/smartCompose.js'; -// Resource parameter interfaces - Threads +// Resource parameter interfaces - threads export type { ListThreadsParams, FindThreadParams, @@ -192,7 +185,7 @@ export type { DestroyThreadParams, } from './resources/threads.js'; -// Resource parameter interfaces - Webhooks +// Resource parameter interfaces - webhooks export type { FindWebhookParams, CreateWebhookParams, @@ -204,4 +197,4 @@ export type { export { SDK_VERSION } from './version.js'; // Re-export all model types for convenience -export * from './models/index.js'; \ No newline at end of file +export * from './models/index.js'; From 51b329312490ebabb77eee01d226dc2b0c56f3c6 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Fri, 11 Jul 2025 16:41:45 -0400 Subject: [PATCH 4/4] Remove manual type generation system and quality gates - Remove src/types.ts and manual type generation infrastructure - Delete examples/types-usage/ directory and all related examples - Remove scripts/generateTypesIndex.js and scripts/checkTypesUpToDate.js - Update package.json to remove './types' export and related scripts - Update scripts/validateRelease.js to remove types validation - Update .github/workflows/validate-types.yml to remove types validation step - Add scripts/generateTypesViaApiExtractor.js as alternative implementation - Maintain core quality gates: linting, testing, build validation, examples validation The SDK returns to core functionality without additional types export. Quality assurance mechanisms remain functional for essential validations. --- .github/workflows/validate-types.yml | 13 +- examples/types-usage/README.md | 128 --------- examples/types-usage/async-iteration-test.ts | 58 ---- examples/types-usage/extension-example.ts | 273 ------------------- examples/types-usage/simple-example.ts | 268 ------------------ package.json | 10 +- scripts/checkTypesUpToDate.js | 101 ------- scripts/generateTypesIndex.js | 156 ----------- scripts/generateTypesViaApiExtractor.js | 232 ++++++++++++++++ scripts/validateRelease.js | 66 ++--- src/types.ts | 200 -------------- 11 files changed, 257 insertions(+), 1248 deletions(-) delete mode 100644 examples/types-usage/README.md delete mode 100644 examples/types-usage/async-iteration-test.ts delete mode 100644 examples/types-usage/extension-example.ts delete mode 100644 examples/types-usage/simple-example.ts delete mode 100644 scripts/checkTypesUpToDate.js delete mode 100644 scripts/generateTypesIndex.js create mode 100644 scripts/generateTypesViaApiExtractor.js delete mode 100644 src/types.ts diff --git a/.github/workflows/validate-types.yml b/.github/workflows/validate-types.yml index 688bb43f..dedb8208 100644 --- a/.github/workflows/validate-types.yml +++ b/.github/workflows/validate-types.yml @@ -1,7 +1,7 @@ -# Sample GitHub Actions workflow for validating types in CI/CD -# This ensures that types are always up-to-date and tests pass +# Sample GitHub Actions workflow for CI/CD validation +# This ensures that tests pass and builds are successful -name: Validate Types and Build +name: Validate and Build on: push: @@ -10,7 +10,7 @@ on: branches: [ main, develop ] jobs: - validate-types: + validate: runs-on: ubuntu-latest strategy: @@ -30,8 +30,7 @@ jobs: - name: Install dependencies run: npm ci - - name: Check if types are up-to-date - run: npm run check-types-current + - name: Run linting run: npm run lint:ci @@ -59,7 +58,7 @@ jobs: validate-release: runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' - needs: validate-types + needs: validate steps: - name: Checkout code diff --git a/examples/types-usage/README.md b/examples/types-usage/README.md deleted file mode 100644 index 6e79de0e..00000000 --- a/examples/types-usage/README.md +++ /dev/null @@ -1,128 +0,0 @@ -# Types Usage Examples - -This directory contains examples demonstrating how to use the Nylas SDK types for building third-party extensions and integrations. - -## Overview - -The Nylas SDK now provides a dedicated types export that allows third-party developers to import only the types they need, without importing the entire SDK. This is particularly useful for: - -- Building extensions that need type safety -- Creating custom middleware or wrappers -- Developing integrations with strict bundle size requirements -- Building developer tools that work with Nylas SDK types - -## Usage - -### Basic Type Imports - -```typescript -import type { NylasConfig, ListMessagesParams, NylasResponse } from 'nylas/types'; -``` - -### Extended Type Imports - -```typescript -import type { - NylasConfig, - OverridableNylasConfig, - ListMessagesParams, - SendMessageParams, - NylasResponse, - NylasListResponse, - Message, - Contact, - Event -} from 'nylas/types'; -``` - -### Configuration Types - -```typescript -import type { NylasConfig, Region } from 'nylas/types'; -import { DEFAULT_SERVER_URL, REGION_CONFIG } from 'nylas/types'; - -// Use types for configuration -const config: NylasConfig = { - apiKey: 'your-api-key', - apiUri: DEFAULT_SERVER_URL, - timeout: 30 -}; -``` - -### Resource Parameter Types - -```typescript -import type { - ListMessagesParams, - FindEventParams, - CreateContactParams -} from 'nylas/types'; - -// Use types for method parameters -const messageParams: ListMessagesParams = { - identifier: 'grant-id', - queryParams: { limit: 10 } -}; -``` - -### Response Types - -```typescript -import type { NylasResponse, NylasListResponse, Message } from 'nylas/types'; - -// Use types for handling responses -function handleMessageResponse(response: NylasResponse) { - const message = response.data; - console.log(`Message: ${message.subject}`); -} -``` - -### Base Classes for Extension - -```typescript -import type { Resource, APIClient } from 'nylas/types'; - -// Extend base classes for custom resources -class CustomResource extends Resource { - constructor(apiClient: APIClient) { - super(apiClient); - } - - // Add custom methods -} -``` - -## Benefits - -1. **Type Safety**: Full TypeScript support for your extensions -2. **Tree Shaking**: Import only what you need for better bundle optimization -3. **Extensibility**: Easy to build on top of the SDK with proper types -4. **Developer Experience**: Better IDE support and auto-completion -5. **Future-Proof**: Types are maintained alongside the SDK - -## Examples - -- `simple-example.ts` - Shows basic usage of SDK types for type safety -- `extension-example.ts` - Shows how to build a custom extension using SDK types (advanced) -- `async-iteration-test.ts` - Demonstrates the correct async iteration pattern for message lists - -## Running the Examples - -1. Install dependencies: - ```bash - cd examples - npm install - ``` - -2. Run the examples: - ```bash - npx ts-node types-usage/simple-example.ts - npx ts-node types-usage/extension-example.ts - ``` - -## Notes - -- All imports from `nylas/types` are type-only and don't include runtime code -- The types are generated automatically from the SDK source code -- Breaking changes to types will follow semantic versioning -- For runtime functionality, you still need to import from `nylas` \ No newline at end of file diff --git a/examples/types-usage/async-iteration-test.ts b/examples/types-usage/async-iteration-test.ts deleted file mode 100644 index eebbd135..00000000 --- a/examples/types-usage/async-iteration-test.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Test file demonstrating the correct async iteration pattern - * This shows how the type error was fixed in the extension example - */ - -import type { ListMessagesParams, NylasConfig } from 'nylas/types'; -import Nylas from 'nylas'; - -async function testAsyncIteration() { - const config: NylasConfig = { - apiKey: 'demo-key', - apiUri: 'https://api.us.nylas.com', - }; - - const nylas = new Nylas(config); - - const params: ListMessagesParams = { - identifier: 'demo-grant-id', - queryParams: { limit: 5 } - }; - - try { - // ✅ CORRECT: Use the async iterator directly without awaiting first - const messagesIterator = nylas.messages.list(params); - - console.log('Processing messages using async iteration...'); - - for await (const page of messagesIterator) { - console.log(`Found ${page.data.length} messages in this page`); - - for (const message of page.data) { - console.log(`- Message: ${message.subject || 'No Subject'}`); - } - - // Break after first page for demo - break; - } - - console.log('Async iteration completed successfully!'); - - } catch (error) { - console.log('Expected error for demo credentials:', error instanceof Error ? error.message : 'Unknown error'); - } -} - -// ❌ INCORRECT PATTERN (this would cause the type error): -// const messages = await nylas.messages.list(params); -// for await (const page of messages) { ... } - -// ✅ CORRECT PATTERN (this is what we fixed): -// const messagesIterator = nylas.messages.list(params); -// for await (const page of messagesIterator) { ... } - -if (require.main === module) { - testAsyncIteration(); -} - -export { testAsyncIteration }; \ No newline at end of file diff --git a/examples/types-usage/extension-example.ts b/examples/types-usage/extension-example.ts deleted file mode 100644 index c5fb831c..00000000 --- a/examples/types-usage/extension-example.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** - * Extension Example: Building a Custom Nylas Extension - * - * This example demonstrates how to build a custom extension on top of the Nylas SDK - * using only the types, without importing the entire SDK. - */ - -import type { - NylasConfig, - ListMessagesParams, - FindEventParams, - NylasResponse, - NylasListResponse, - Message, - Event, - Contact, - APIClient, - RequestOptionsParams, - Timespan, - AsyncListResponse, -} from 'nylas/types'; - -// Import values that we need at runtime -import { Resource, WhenType } from 'nylas/types'; - -// Import the actual SDK for runtime functionality -import Nylas from 'nylas'; - -/** - * Custom Analytics Extension - * - * This extension provides analytics functionality for Nylas data - * while maintaining full type safety. - */ -class NylasAnalytics { - private nylas: Nylas; - - constructor(config: NylasConfig) { - this.nylas = new Nylas(config); - } - - /** - * Analyze message patterns for a grant - */ - async analyzeMessagePatterns(params: ListMessagesParams): Promise { - const messagesIterator = this.nylas.messages.list(params); - - let totalMessages = 0; - let totalSize = 0; - const senders = new Map(); - const subjects = new Map(); - - // Process each page of messages using async iteration - for await (const page of messagesIterator) { - for (const message of page.data) { - totalMessages++; - totalSize += message.body?.length || 0; - - // Count senders - if (message.from && message.from.length > 0) { - const sender = message.from[0].email; - senders.set(sender, (senders.get(sender) || 0) + 1); - } - - // Count subjects - if (message.subject) { - subjects.set(message.subject, (subjects.get(message.subject) || 0) + 1); - } - } - } - - return { - totalMessages, - averageSize: totalMessages > 0 ? totalSize / totalMessages : 0, - topSenders: Array.from(senders.entries()) - .sort((a, b) => b[1] - a[1]) - .slice(0, 5), - topSubjects: Array.from(subjects.entries()) - .sort((a, b) => b[1] - a[1]) - .slice(0, 5), - }; - } - - /** - * Get event statistics for a time period - */ - async getEventStatistics( - identifier: string, - startDate: string, - endDate: string - ): Promise { - const eventParams: FindEventParams = { - identifier, - eventId: '', // This would be updated to use list events - queryParams: { - calendarId: '', // Would be set appropriately - // Add date range filters - } - }; - - // Note: In a real implementation, you'd use the events.list() method - // This is just for type demonstration - const events: Event[] = []; // Placeholder - - return { - totalEvents: events.length, - busyHours: this.analyzeBusyHours(events), - eventTypes: this.analyzeEventTypes(events), - }; - } - - /** - * Create a smart contact group based on interaction patterns - */ - async createSmartContactGroup( - identifier: string, - criteria: ContactGroupCriteria - ): Promise { - // This would implement logic to analyze contacts and group them - // based on interaction patterns, using proper types - - const contacts: Contact[] = []; - - // Example of using contact creation (CreateContactParams is not exported) - // This would be used for creating contacts via the SDK - const sampleContactData = { - displayName: 'Sample Contact', - emails: [{ email: 'sample@example.com' }], - phoneNumbers: [], - physicalAddresses: [], - webPages: [], - imAddresses: [], - notes: 'Created by smart grouping', - }; - - return contacts; - } - - private analyzeBusyHours(events: Event[]): { [hour: number]: number } { - const busyHours: { [hour: number]: number } = {}; - - events.forEach(event => { - if (event.when && event.when.object === WhenType.Timespan) { - const timespan = event.when as Timespan; - const hour = new Date(timespan.startTime * 1000).getHours(); - busyHours[hour] = (busyHours[hour] || 0) + 1; - } - }); - - return busyHours; - } - - private analyzeEventTypes(events: Event[]): { [type: string]: number } { - const eventTypes: { [type: string]: number } = {}; - - events.forEach(event => { - const type = event.title?.toLowerCase().includes('meeting') ? 'meeting' : 'other'; - eventTypes[type] = (eventTypes[type] || 0) + 1; - }); - - return eventTypes; - } -} - -/** - * Custom Resource Extension - * - * This shows how to extend the base Resource class to create custom resources - * with full type safety. - */ -class CustomInsightsResource extends Resource { - constructor(apiClient: APIClient) { - super(apiClient); - } - - /** - * Get custom insights with proper typing - */ - async getInsights(identifier: string): Promise { - // Example of using the protected methods from Resource base class - const requestOptions: RequestOptionsParams = { - method: 'GET', - path: `/v3/grants/${identifier}/insights`, - queryParams: { format: 'json' }, - }; - - // This would use the protected apiClient methods - // For demonstration, we'll return a mock response - return { - messageCount: 100, - eventCount: 50, - contactCount: 25, - insights: ['High email volume', 'Many meetings this week'] - }; - } -} - -// Type definitions for the extension -interface MessageAnalytics { - totalMessages: number; - averageSize: number; - topSenders: [string, number][]; - topSubjects: [string, number][]; -} - -interface EventStatistics { - totalEvents: number; - busyHours: { [hour: number]: number }; - eventTypes: { [type: string]: number }; -} - -interface ContactGroupCriteria { - minInteractions: number; - timeRange: string; - includeEvents: boolean; -} - -interface CustomInsights { - messageCount: number; - eventCount: number; - contactCount: number; - insights: string[]; -} - -// Example usage -async function demonstrateExtension() { - // Configuration using proper types - const config: NylasConfig = { - apiKey: process.env.NYLAS_API_KEY || 'your-api-key', - apiUri: 'https://api.us.nylas.com', - timeout: 30, - }; - - // Create analytics extension - const analytics = new NylasAnalytics(config); - - try { - // Analyze message patterns - const messageParams: ListMessagesParams = { - identifier: 'grant-id', - queryParams: { limit: 100 } - }; - - const messageAnalytics = await analytics.analyzeMessagePatterns(messageParams); - console.log('Message Analytics:', messageAnalytics); - - // Get event statistics - const eventStats = await analytics.getEventStatistics( - 'grant-id', - '2024-01-01', - '2024-01-31' - ); - console.log('Event Statistics:', eventStats); - - // Create smart contact group - const smartContacts = await analytics.createSmartContactGroup( - 'grant-id', - { minInteractions: 5, timeRange: '30d', includeEvents: true } - ); - console.log('Smart Contacts:', smartContacts.length); - - } catch (error) { - console.error('Extension error:', error); - } -} - -// Only run if this file is executed directly -if (require.main === module) { - demonstrateExtension(); -} - -export { NylasAnalytics, CustomInsightsResource }; -export type { MessageAnalytics, EventStatistics, ContactGroupCriteria, CustomInsights }; \ No newline at end of file diff --git a/examples/types-usage/simple-example.ts b/examples/types-usage/simple-example.ts deleted file mode 100644 index 699edee4..00000000 --- a/examples/types-usage/simple-example.ts +++ /dev/null @@ -1,268 +0,0 @@ -/** - * Simple Types Example: Using Nylas SDK Types - * - * This example demonstrates how to use the Nylas SDK types for basic type safety - * when working with the SDK in third-party applications. - */ - -import type { - NylasConfig, - ListMessagesParams, - FindEventParams, - NylasResponse, - NylasListResponse, - Message, - Event, - Contact, -} from 'nylas/types'; - -// Import runtime values -import { DEFAULT_SERVER_URL, REGION_CONFIG } from 'nylas/types'; - -// Import the actual SDK for runtime functionality -import Nylas from 'nylas'; - -/** - * Configuration Helper - * Uses proper typing for configuration - */ -function createNylasConfig(apiKey: string): NylasConfig { - const config: NylasConfig = { - apiKey, - apiUri: DEFAULT_SERVER_URL, - timeout: 30, - headers: { - 'X-Custom-Header': 'my-app-v1.0' - } - }; - - return config; -} - -/** - * Message Handler - * Uses proper typing for message operations - */ -class MessageHandler { - private nylas: Nylas; - - constructor(config: NylasConfig) { - this.nylas = new Nylas(config); - } - - /** - * Get messages with proper parameter typing - */ - async getMessages(identifier: string, limit: number = 10): Promise { - const params: ListMessagesParams = { - identifier, - queryParams: { - limit, - hasAttachment: false, - } - }; - - const response = await this.nylas.messages.list(params); - const firstPage = await response; - - return firstPage.data; - } - - /** - * Process message response with proper typing - */ - processMessageResponse(response: NylasResponse): void { - const message = response.data; - - console.log(`Message ID: ${message.id}`); - console.log(`Subject: ${message.subject}`); - console.log(`From: ${message.from?.[0]?.email || 'Unknown'}`); - console.log(`Date: ${new Date(message.date * 1000).toLocaleDateString()}`); - - if (message.attachments && message.attachments.length > 0) { - console.log(`Attachments: ${message.attachments.length}`); - } - } - - /** - * Process message list response with proper typing - */ - processMessageList(response: NylasListResponse): void { - console.log(`Total messages: ${response.data.length}`); - console.log(`Request ID: ${response.requestId}`); - - response.data.forEach((message, index) => { - console.log(`${index + 1}. ${message.subject || 'No Subject'}`); - }); - } -} - -/** - * Event Handler - * Uses proper typing for event operations - */ -class EventHandler { - private nylas: Nylas; - - constructor(config: NylasConfig) { - this.nylas = new Nylas(config); - } - - /** - * Get event with proper parameter typing - */ - async getEvent(identifier: string, eventId: string, calendarId: string): Promise { - const params: FindEventParams = { - identifier, - eventId, - queryParams: { - calendarId, - } - }; - - try { - const response = await this.nylas.events.find(params); - return response.data; - } catch (error) { - console.error('Error fetching event:', error); - return null; - } - } - - /** - * Process event with proper typing - */ - processEvent(event: Event): void { - console.log(`Event: ${event.title || 'No Title'}`); - console.log(`Calendar: ${event.calendarId}`); - console.log(`Description: ${event.description || 'No Description'}`); - - if (event.participants && event.participants.length > 0) { - console.log(`Participants: ${event.participants.length}`); - event.participants.forEach(participant => { - console.log(` - ${participant.name || participant.email} (${participant.status})`); - }); - } - } -} - -/** - * Type-safe utility functions - */ -class TypeUtilities { - /** - * Check if response is successful - */ - static isSuccessfulResponse(response: NylasResponse): boolean { - return response.data !== null && response.data !== undefined; - } - - /** - * Extract email addresses from contacts - */ - static extractEmailAddresses(contacts: Contact[]): string[] { - return contacts - .flatMap(contact => contact.emails) - .map(email => email.email) - .filter(email => email !== undefined); - } - - /** - * Format message for display - */ - static formatMessageSummary(message: Message): string { - const from = message.from?.[0]?.email || 'Unknown'; - const subject = message.subject || 'No Subject'; - const date = new Date(message.date * 1000).toLocaleDateString(); - - return `From: ${from} | Subject: ${subject} | Date: ${date}`; - } -} - -/** - * Demo function showing the types in action - */ -async function demonstrateTypes() { - console.log('=== Nylas SDK Types Demo ==='); - - // Create configuration with proper typing - const config = createNylasConfig(process.env.NYLAS_API_KEY || 'demo-key'); - console.log('Configuration created:', { - apiUri: config.apiUri, - timeout: config.timeout, - hasHeaders: !!config.headers - }); - - // Show available regions - console.log('\nAvailable regions:'); - Object.entries(REGION_CONFIG).forEach(([region, config]) => { - console.log(` ${region}: ${config.nylasAPIUrl}`); - }); - - // Create handlers with proper typing - const messageHandler = new MessageHandler(config); - const eventHandler = new EventHandler(config); - - console.log('\n=== Type Safety Demo ==='); - - // Example of type-safe parameter creation - const messageParams: ListMessagesParams = { - identifier: 'demo-grant-id', - queryParams: { - limit: 5, - hasAttachment: false, - } - }; - - console.log('Message parameters:', messageParams); - - // Example of type-safe event parameters - const eventParams: FindEventParams = { - identifier: 'demo-grant-id', - eventId: 'demo-event-id', - queryParams: { - calendarId: 'primary', - } - }; - - console.log('Event parameters:', eventParams); - - // Demo utility functions - const demoMessage: Message = { - id: 'demo-message-id', - grantId: 'demo-grant-id', - object: 'message', - threadId: 'demo-thread-id', - subject: 'Demo Message', - from: [{ email: 'demo@example.com', name: 'Demo User' }], - to: [{ email: 'recipient@example.com', name: 'Recipient' }], - date: Math.floor(Date.now() / 1000), - body: 'This is a demo message', - folders: ['inbox'], - attachments: [], - unread: false, - starred: false, - snippet: 'This is a demo message', - replyTo: [], - cc: [], - bcc: [], - }; - - console.log('\nFormatted message summary:'); - console.log(TypeUtilities.formatMessageSummary(demoMessage)); - - console.log('\n=== Types Demo Complete ==='); -} - -// Export for use in other modules -export { - MessageHandler, - EventHandler, - TypeUtilities, - createNylasConfig, -}; - -// Only run if this file is executed directly -if (require.main === module) { - demonstrateTypes().catch(console.error); -} \ No newline at end of file diff --git a/package.json b/package.json index de149849..cb840f6e 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,9 @@ "export-version": "node scripts/exportVersion.js", "generate-lib-package-json": "node scripts/generateLibPackageJson.js", "generate-model-index": "node scripts/generateModelIndex.js", - "generate-types-index": "node scripts/generateTypesIndex.js", + "generate-types-via-api-extractor": "node scripts/generateTypesViaApiExtractor.js", "validate-release": "node scripts/validateRelease.js", - "check-types-current": "node scripts/checkTypesUpToDate.js", - "prebuild": "npm run export-version && npm run generate-model-index && npm run generate-types-index", + "prebuild": "npm run export-version && npm run generate-model-index", "build": "rm -rf lib && npm run build-esm && npm run build-cjs && npm run generate-lib-package-json", "build-esm": "tsc -p tsconfig.esm.json", "build-cjs": "tsc -p tsconfig.cjs.json", @@ -80,11 +79,6 @@ "import": "./lib/esm/nylas.js", "require": "./lib/cjs/nylas.js", "types": "./lib/types/nylas.d.ts" - }, - "./types": { - "import": "./lib/esm/types.js", - "require": "./lib/cjs/types.js", - "types": "./lib/types/types.d.ts" } } } diff --git a/scripts/checkTypesUpToDate.js b/scripts/checkTypesUpToDate.js deleted file mode 100644 index ec43e91e..00000000 --- a/scripts/checkTypesUpToDate.js +++ /dev/null @@ -1,101 +0,0 @@ -const { execSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -/** - * CI script to check if types are up-to-date - * This validates that types.ts matches what would be generated - * Used in CI/CD pipelines to catch out-of-date types - */ - -const COLORS = { - green: '\x1b[32m', - red: '\x1b[31m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - reset: '\x1b[0m' -}; - -function log(message, color = 'reset') { - console.log(`${COLORS[color]}${message}${COLORS.reset}`); // eslint-disable-line no-console -} - -function checkTypesUpToDate() { - log('🔍 Checking if types are up-to-date...', 'blue'); - - const typesPath = path.join(__dirname, '..', 'src', 'types.ts'); - - // Check if types.ts exists - if (!fs.existsSync(typesPath)) { - log('❌ src/types.ts does not exist', 'red'); - log('💡 Run: npm run generate-types-index', 'yellow'); - process.exit(1); - } - - // Read current content - const currentContent = fs.readFileSync(typesPath, 'utf8'); - - // Check if it's auto-generated - if (!currentContent.includes('auto-generated by scripts/generateTypesIndex.js')) { - log('❌ src/types.ts is not auto-generated', 'red'); - log('💡 Run: npm run generate-types-index', 'yellow'); - process.exit(1); - } - - // Create a backup of current content - const backupPath = typesPath + '.backup'; - fs.writeFileSync(backupPath, currentContent); - - try { - // Generate new types silently - execSync('npm run generate-types-index', { stdio: 'pipe' }); - - // Read the newly generated content - const newContent = fs.readFileSync(typesPath, 'utf8'); - - // Compare contents - if (currentContent !== newContent) { - log('❌ Types are out of date!', 'red'); - log('', 'reset'); - log('The src/types.ts file is not up-to-date with the current codebase.', 'red'); - log('', 'reset'); - log('To fix this, run locally:', 'yellow'); - log(' npm run generate-types-index', 'yellow'); - log(' git add src/types.ts', 'yellow'); - log(' git commit -m "chore: update generated types"', 'yellow'); - log('', 'reset'); - - // Restore original content - fs.writeFileSync(typesPath, currentContent); - fs.unlinkSync(backupPath); - - process.exit(1); - } - - // Clean up backup - fs.unlinkSync(backupPath); - - log('✅ Types are up-to-date!', 'green'); - - } catch (error) { - // Restore original content if generation failed - if (fs.existsSync(backupPath)) { - fs.writeFileSync(typesPath, currentContent); - fs.unlinkSync(backupPath); - } - - log('❌ Failed to generate types:', 'red'); - console.error(error.message); - process.exit(1); - } -} - -function main() { - log('🚀 Checking types status for CI...', 'blue'); - checkTypesUpToDate(); - log('🎉 All checks passed!', 'green'); -} - -if (require.main === module) { - main(); -} \ No newline at end of file diff --git a/scripts/generateTypesIndex.js b/scripts/generateTypesIndex.js deleted file mode 100644 index bec474fa..00000000 --- a/scripts/generateTypesIndex.js +++ /dev/null @@ -1,156 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -/** - * Generates src/types.ts by extracting types from the codebase - */ -async function generateTypesIndex() { - console.log('🔍 Extracting types from codebase...'); // eslint-disable-line no-console - - // Define the header comment and imports - const header = `/** - * Types and interfaces for third-party developers building extensions or integrations with the Nylas SDK. - * - * This file is auto-generated by scripts/generateTypesIndex.js - * DO NOT EDIT MANUALLY - your changes will be overwritten - * - * This module provides type-only exports for: - * - Configuration types - * - Base classes and interfaces - * - Resource parameter interfaces - * - Request/response types - * - Error types - * - Constants and utility types - * - * @example - * \`\`\`typescript - * import type { NylasConfig, ListMessagesParams, NylasResponse } from 'nylas/types'; - * - * // Use types for configuration - * const config: NylasConfig = { - * apiKey: 'your-api-key', - * apiUri: 'https://api.us.nylas.com' - * }; - * - * // Use types for method parameters - * const params: ListMessagesParams = { - * identifier: 'grant-id', - * queryParams: { limit: 10 } - * }; - * \`\`\` - */ - -// Configuration types -export type { - NylasConfig, - OverridableNylasConfig, - Overrides, - Region, -} from './config.js'; - -export { - REGION_CONFIG, - DEFAULT_SERVER_URL, - DEFAULT_REGION, -} from './config.js'; - -// Base classes and interfaces -export type { AsyncListResponse } from './resources/resource.js'; -export { Resource } from './resources/resource.js'; -export type { default as APIClient } from './apiClient.js'; - -// Export enum values that need to be used at runtime -export { WhenType } from './models/events.js'; - -// APIClient types -export type { RequestOptionsParams } from './apiClient.js'; -export { FLOW_ID_HEADER, REQUEST_ID_HEADER } from './apiClient.js'; - -// Response types -export type { - NylasResponse, - NylasListResponse, - NylasBaseResponse, - ListResponseInnerType, -} from './models/response.js'; - -// Common query parameter types -export type { ListQueryParams } from './models/listQueryParams.js'; - -// Error types -export type { - AbstractNylasApiError, - AbstractNylasSdkError, - NylasApiError, - NylasOAuthError, - NylasSdkTimeoutError, - NylasApiErrorResponse, - NylasApiErrorResponseData, - NylasOAuthErrorResponse, -} from './models/error.js'; - -`; - - // Get resource parameter interfaces by scanning resource files - const resourcesDir = path.join(__dirname, '..', 'src', 'resources'); - const resourceFiles = fs.readdirSync(resourcesDir) - .filter(file => file.endsWith('.ts') && file !== 'resource.ts') - .map(file => path.join(resourcesDir, file)); - - let resourceExports = ''; - - // Extract parameter interfaces from each resource file - for (const resourceFile of resourceFiles) { - const resourceName = path.basename(resourceFile, '.ts'); - const relativePath = `./resources/${resourceName}.js`; - - try { - const content = fs.readFileSync(resourceFile, 'utf8'); - - // Extract interface names that end with 'Params' - const interfacePattern = /^export interface ([A-Za-z0-9_]+Params)\s*{/gm; - const interfaces = []; - let match; - - while ((match = interfacePattern.exec(content)) !== null) { - interfaces.push(match[1]); - } - - if (interfaces.length > 0) { - resourceExports += `// Resource parameter interfaces - ${resourceName}\n`; - resourceExports += `export type {\n`; - interfaces.forEach(interfaceName => { - resourceExports += ` ${interfaceName},\n`; - }); - resourceExports += `} from '${relativePath}';\n\n`; - } - } catch (error) { - console.warn(`⚠️ Could not process ${resourceFile}: ${error.message}`); - } - } - - // Footer exports - const footer = `// Utility constants -export { SDK_VERSION } from './version.js'; - -// Re-export all model types for convenience -export * from './models/index.js'; -`; - - // Combine all parts - const generatedContent = header + resourceExports + footer; - - // Write to src/types.ts - const outputPath = path.join(__dirname, '..', 'src', 'types.ts'); - fs.writeFileSync(outputPath, generatedContent); - - console.log('✅ Generated src/types.ts successfully!'); // eslint-disable-line no-console - console.log(`📄 File: ${outputPath}`); // eslint-disable-line no-console - console.log(`📊 Extracted parameter interfaces from ${resourceFiles.length} resource files`); // eslint-disable-line no-console -} - -// Run the generator -generateTypesIndex().catch(error => { - console.error('❌ Error generating types index:', error); - process.exit(1); -}); \ No newline at end of file diff --git a/scripts/generateTypesViaApiExtractor.js b/scripts/generateTypesViaApiExtractor.js new file mode 100644 index 00000000..108021cb --- /dev/null +++ b/scripts/generateTypesViaApiExtractor.js @@ -0,0 +1,232 @@ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +/** + * Alternative types generation using Microsoft API Extractor + * This generates types.d.ts using the TypeScript compiler and API Extractor + * instead of manual regex parsing + */ + +const COLORS = { + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + reset: '\x1b[0m' +}; + +function log(message, color = 'reset') { + console.log(`${COLORS[color]}${message}${COLORS.reset}`); // eslint-disable-line no-console +} + +function createApiExtractorConfig() { + const configPath = path.join(__dirname, '..', 'api-extractor.json'); + + const config = { + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "/lib/types/types.d.ts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": false + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/types.d.ts", + "publicTrimmedFilePath": "/dist/types-public.d.ts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "compilerMessageReporting": { + "default": { + "logLevel": "warning" + } + }, + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + } + } + } + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + log(`✅ Created API Extractor config: ${configPath}`, 'green'); +} + +function createTypesEntryPoint() { + // Create a comprehensive types entry point for API Extractor to analyze + const typesEntryContent = `/** + * Public API types for third-party developers. + * This file serves as the main entry point for API Extractor. + * + * @public + */ + +// Configuration types +export type { + NylasConfig, + OverridableNylasConfig, + Overrides, + Region, +} from '../config.js'; + +/** + * Configuration constants for different regions + * @public + */ +export { + REGION_CONFIG, + DEFAULT_SERVER_URL, + DEFAULT_REGION, +} from '../config.js'; + +// Base classes and interfaces +export type { AsyncListResponse } from '../resources/resource.js'; + +/** + * Base Resource class for extending Nylas SDK functionality + * @public + */ +export { Resource } from '../resources/resource.js'; + +export type { default as APIClient } from '../apiClient.js'; + +// Export enum values that need to be used at runtime +export { WhenType } from '../models/events.js'; + +// APIClient types +export type { RequestOptionsParams } from '../apiClient.js'; +export { FLOW_ID_HEADER, REQUEST_ID_HEADER } from '../apiClient.js'; + +// Response types +export type { + NylasResponse, + NylasListResponse, + NylasBaseResponse, + ListResponseInnerType, +} from '../models/response.js'; + +// Common query parameter types +export type { ListQueryParams } from '../models/listQueryParams.js'; + +// Error types +export type { + AbstractNylasApiError, + AbstractNylasSdkError, + NylasApiError, + NylasOAuthError, + NylasSdkTimeoutError, + NylasApiErrorResponse, + NylasApiErrorResponseData, + NylasOAuthErrorResponse, +} from '../models/error.js'; + +// Utility constants +export { SDK_VERSION } from '../version.js'; + +// All model types +export * from '../models/index.js'; + +// Resource parameter interfaces - auto-generated exports +// These would be automatically included by API Extractor +`; + + const entryPointPath = path.join(__dirname, '..', 'src', 'typesEntry.ts'); + fs.writeFileSync(entryPointPath, typesEntryContent); + log(`✅ Created types entry point: ${entryPointPath}`, 'green'); +} + +function runApiExtractor() { + log('🔍 Running API Extractor...', 'blue'); + + try { + // First, ensure we have a clean build + execSync('npm run build', { stdio: 'pipe' }); + + // Check if API Extractor is installed + try { + execSync('npx api-extractor --version', { stdio: 'pipe' }); + } catch (error) { + log('⚠️ API Extractor not installed. To try this approach:', 'yellow'); + log(' npm install -D @microsoft/api-extractor', 'yellow'); + return; + } + + // Run API Extractor + execSync('npx api-extractor run --local', { + encoding: 'utf8', + stdio: 'pipe' + }); + + log('✅ API Extractor completed successfully', 'green'); + + // Check if output files were created + const outputFiles = [ + 'dist/types.d.ts', + 'dist/types-public.d.ts' + ]; + + outputFiles.forEach(file => { + const filePath = path.join(__dirname, '..', file); + if (fs.existsSync(filePath)) { + const size = fs.statSync(filePath).size; + log(`📄 Generated: ${file} (${size} bytes)`, 'green'); + } + }); + + } catch (error) { + log('❌ API Extractor failed:', 'red'); + log(error.stdout || error.message, 'red'); + throw error; + } +} + +function generateTypesWithApiExtractor() { + log('🚀 Generating types using API Extractor...', 'blue'); + + try { + // Step 1: Create API Extractor configuration + createApiExtractorConfig(); + + // Step 2: Create types entry point + createTypesEntryPoint(); + + // Step 3: Run API Extractor + runApiExtractor(); + + log('🎉 Types generation completed!', 'green'); + log('', 'reset'); + log('Generated files:', 'blue'); + log(' - dist/types.d.ts (complete API)', 'green'); + log(' - dist/types-public.d.ts (public API only)', 'green'); + log('', 'reset'); + log('Benefits of this approach:', 'blue'); + log(' ✅ Uses TypeScript compiler (100% accurate)', 'green'); + log(' ✅ Automatic type extraction', 'green'); + log(' ✅ Industry standard tooling', 'green'); + log(' ✅ Can trim @internal/@beta APIs', 'green'); + log(' ✅ Generates API reports for reviews', 'green'); + + } catch (error) { + log('💥 Types generation failed!', 'red'); + throw error; + } +} + +if (require.main === module) { + generateTypesWithApiExtractor(); +} \ No newline at end of file diff --git a/scripts/validateRelease.js b/scripts/validateRelease.js index 574a3906..73f98d65 100644 --- a/scripts/validateRelease.js +++ b/scripts/validateRelease.js @@ -35,36 +35,7 @@ function runCommand(command, description) { } } -function validateTypesGeneration() { - log('🔍 Validating types generation...', 'blue'); - - // Check if types.ts exists - const typesPath = path.join(__dirname, '..', 'src', 'types.ts'); - if (!fs.existsSync(typesPath)) { - log('❌ src/types.ts does not exist', 'red'); - throw new Error('Types file missing'); - } - - // Check if types.ts is auto-generated (has the warning header) - const typesContent = fs.readFileSync(typesPath, 'utf8'); - if (!typesContent.includes('auto-generated by scripts/generateTypesIndex.js')) { - log('❌ src/types.ts is not auto-generated', 'red'); - throw new Error('Types file not auto-generated'); - } - - // Regenerate types and check if there are any changes - const originalContent = typesContent; - runCommand('npm run generate-types-index', 'Regenerating types'); - - const newContent = fs.readFileSync(typesPath, 'utf8'); - if (originalContent !== newContent) { - log('❌ Types file was out of date and has been regenerated', 'red'); - log('⚠️ Please commit the updated types.ts file and try again', 'yellow'); - throw new Error('Types file was out of date'); - } - - log('✅ Types generation validation passed', 'green'); -} + function validateBuildOutput() { log('🔍 Validating build output...', 'blue'); @@ -76,23 +47,23 @@ function validateBuildOutput() { throw new Error('Build output missing'); } - // Check for both CJS and ESM builds - const cjsPath = path.join(libPath, 'cjs', 'types.js'); - const esmPath = path.join(libPath, 'esm', 'types.js'); - const dtsPath = path.join(libPath, 'types', 'types.d.ts'); + // Check for main build outputs + const mainCjsPath = path.join(libPath, 'cjs', 'nylas.js'); + const mainEsmPath = path.join(libPath, 'esm', 'nylas.js'); + const mainDtsPath = path.join(libPath, 'types', 'nylas.d.ts'); - if (!fs.existsSync(cjsPath)) { - log('❌ CJS types.js build missing', 'red'); + if (!fs.existsSync(mainCjsPath)) { + log('❌ CJS nylas.js build missing', 'red'); throw new Error('CJS build missing'); } - if (!fs.existsSync(esmPath)) { - log('❌ ESM types.js build missing', 'red'); + if (!fs.existsSync(mainEsmPath)) { + log('❌ ESM nylas.js build missing', 'red'); throw new Error('ESM build missing'); } - if (!fs.existsSync(dtsPath)) { - log('❌ TypeScript declaration types.d.ts missing', 'red'); + if (!fs.existsSync(mainDtsPath)) { + log('❌ TypeScript declaration nylas.d.ts missing', 'red'); throw new Error('TypeScript declarations missing'); } @@ -123,25 +94,22 @@ async function main() { log('🚀 Starting release validation...', 'blue'); try { - // Step 1: Regenerate and validate types - validateTypesGeneration(); - - // Step 2: Run linting + // Step 1: Run linting runCommand('npm run lint:ci', 'Running linter'); - // Step 3: Run tests + // Step 2: Run tests runCommand('npm test', 'Running tests'); - // Step 4: Clean build + // Step 3: Clean build runCommand('npm run build', 'Building project'); - // Step 5: Validate build output + // Step 4: Validate build output validateBuildOutput(); - // Step 6: Validate examples (optional) + // Step 5: Validate examples (optional) validateExamples(); - // Step 7: Run test coverage + // Step 6: Run test coverage runCommand('npm run test:coverage', 'Running test coverage'); log('🎉 All release validation checks passed!', 'green'); diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 250a6725..00000000 --- a/src/types.ts +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Types and interfaces for third-party developers building extensions or integrations with the Nylas SDK. - * - * This file is auto-generated by scripts/generateTypesIndex.js - * DO NOT EDIT MANUALLY - your changes will be overwritten - * - * This module provides type-only exports for: - * - Configuration types - * - Base classes and interfaces - * - Resource parameter interfaces - * - Request/response types - * - Error types - * - Constants and utility types - * - * @example - * ```typescript - * import type { NylasConfig, ListMessagesParams, NylasResponse } from 'nylas/types'; - * - * // Use types for configuration - * const config: NylasConfig = { - * apiKey: 'your-api-key', - * apiUri: 'https://api.us.nylas.com' - * }; - * - * // Use types for method parameters - * const params: ListMessagesParams = { - * identifier: 'grant-id', - * queryParams: { limit: 10 } - * }; - * ``` - */ - -// Configuration types -export type { - NylasConfig, - OverridableNylasConfig, - Overrides, - Region, -} from './config.js'; - -export { - REGION_CONFIG, - DEFAULT_SERVER_URL, - DEFAULT_REGION, -} from './config.js'; - -// Base classes and interfaces -export type { AsyncListResponse } from './resources/resource.js'; -export { Resource } from './resources/resource.js'; -export type { default as APIClient } from './apiClient.js'; - -// Export enum values that need to be used at runtime -export { WhenType } from './models/events.js'; - -// APIClient types -export type { RequestOptionsParams } from './apiClient.js'; -export { FLOW_ID_HEADER, REQUEST_ID_HEADER } from './apiClient.js'; - -// Response types -export type { - NylasResponse, - NylasListResponse, - NylasBaseResponse, - ListResponseInnerType, -} from './models/response.js'; - -// Common query parameter types -export type { ListQueryParams } from './models/listQueryParams.js'; - -// Error types -export type { - AbstractNylasApiError, - AbstractNylasSdkError, - NylasApiError, - NylasOAuthError, - NylasSdkTimeoutError, - NylasApiErrorResponse, - NylasApiErrorResponseData, - NylasOAuthErrorResponse, -} from './models/error.js'; - -// Resource parameter interfaces - bookings -export type { - FindBookingParams, - CreateBookingParams, - ConfirmBookingParams, - RescheduleBookingParams, - DestroyBookingParams, -} from './resources/bookings.js'; - -// Resource parameter interfaces - calendars -export type { - FindCalendarParams, - ListCalendersParams, - CreateCalendarParams, - UpdateCalendarParams, - DestroyCalendarParams, - GetAvailabilityParams, - GetFreeBusyParams, -} from './resources/calendars.js'; - -// Resource parameter interfaces - configurations -export type { - FindConfigurationParams, - ListConfigurationsParams, - CreateConfigurationParams, - UpdateConfigurationParams, - DestroyConfigurationParams, -} from './resources/configurations.js'; - -// Resource parameter interfaces - drafts -export type { - ListDraftsParams, - FindDraftParams, - CreateDraftParams, - UpdateDraftParams, -} from './resources/drafts.js'; - -// Resource parameter interfaces - events -export type { - FindEventParams, - ListEventParams, - CreateEventParams, - UpdateEventParams, - DestroyEventParams, - ListImportEventParams, -} from './resources/events.js'; - -// Resource parameter interfaces - grants -export type { - ListGrantsParams, - FindGrantParams, - UpdateGrantParams, - DestroyGrantParams, -} from './resources/grants.js'; - -// Resource parameter interfaces - messages -export type { - ListMessagesParams, - FindMessageParams, - UpdateMessageParams, - DestroyMessageParams, - SendMessageParams, - ListScheduledMessagesParams, - FindScheduledMessageParams, - CleanMessagesParams, -} from './resources/messages.js'; - -// Resource parameter interfaces - notetakers -export type { - ListNotetakersParams, - CreateNotetakerParams, - FindNotetakerParams, - UpdateNotetakerParams, - CancelNotetakerParams, - LeaveNotetakerParams, - DownloadNotetakerMediaParams, -} from './resources/notetakers.js'; - -// Resource parameter interfaces - redirectUris -export type { - FindRedirectUrisParams, - CreateRedirectUrisParams, - UpdateRedirectUrisParams, - DestroyRedirectUrisParams, -} from './resources/redirectUris.js'; - -// Resource parameter interfaces - sessions -export type { - CreateSessionParams, - DestroySessionParams, -} from './resources/sessions.js'; - -// Resource parameter interfaces - smartCompose -export type { - ComposeMessageParams, - ComposeMessageReplyParams, -} from './resources/smartCompose.js'; - -// Resource parameter interfaces - threads -export type { - ListThreadsParams, - FindThreadParams, - UpdateThreadParams, - DestroyThreadParams, -} from './resources/threads.js'; - -// Resource parameter interfaces - webhooks -export type { - FindWebhookParams, - CreateWebhookParams, - UpdateWebhookParams, - DestroyWebhookParams, -} from './resources/webhooks.js'; - -// Utility constants -export { SDK_VERSION } from './version.js'; - -// Re-export all model types for convenience -export * from './models/index.js';