diff --git a/CHANGELOG.md b/CHANGELOG.md index df9c6477..0e9fce71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for `trackingOptions` property in Message responses when using `fields=include_tracking_options` - Support for `rawMime` property in Message responses when using `fields=raw_mime` - `MessageTrackingOptions` interface for tracking message opens, thread replies, link clicks, and custom labels +- Support for `singleLevel` query parameter in `ListFolderQueryParams` for Microsoft accounts to control folder hierarchy traversal ### Fixed - Fixed 3MB payload size limit to consider total request size (message body + attachments) instead of just attachment size when determining whether to use multipart/form-data encoding diff --git a/examples/README.md b/examples/README.md index 1517f82c..18e0e954 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,6 +6,7 @@ This directory contains examples of how to use the Nylas Node.js SDK to interact - [Notetakers](./notetakers/README.md) - Examples of how to use the Nylas Notetakers API to invite a Notetaker bot to meetings, get recordings and transcripts, and more. - [Messages](./messages/README.md) - Examples of how to use the Nylas Messages API to list, find, send, update messages, and work with new features like tracking options and raw MIME data. +- [Folders](./folders/README.md) - Examples of how to use the Nylas Folders API, including the new `singleLevel` parameter for Microsoft accounts. ## Running the Examples diff --git a/examples/folders/README.md b/examples/folders/README.md new file mode 100644 index 00000000..6a45c8cb --- /dev/null +++ b/examples/folders/README.md @@ -0,0 +1,114 @@ +# Nylas Folders API Examples + +This directory contains examples of how to use the Nylas Folders API with the Nylas Node.js SDK, including the new `singleLevel` query parameter. + +## What is the singleLevel Parameter? + +The `singleLevel` parameter is a new query parameter for the "list all folders" endpoint that controls folder hierarchy traversal: + +- **`singleLevel: true`** - Retrieves folders from a single-level hierarchy only (direct children) +- **`singleLevel: false`** - Retrieves folders across a multi-level hierarchy (all descendants, default behavior) +- **Microsoft accounts only** - This parameter is ignored for other providers + +## Examples + +- [folders.ts](./folders.ts) - A comprehensive example showing how to use the `singleLevel` parameter in various scenarios + +## Running the Examples + +To run these examples, you'll need to: + +1. Install dependencies from the examples directory: + ```bash + cd examples + npm install + ``` + +2. Copy the `.env.example` file to `.env` if you haven't already and add your credentials: + ```bash + cp .env.example .env + # Edit .env with your editor + ``` + +3. Edit the `.env` file to include: + - `NYLAS_API_KEY` - Your Nylas API key + - `NYLAS_API_URI` (optional) - The Nylas API server URI (defaults to "https://api.us.nylas.com") + - `NYLAS_GRANT_ID` - The Grant ID for a Microsoft account to see the `singleLevel` parameter in action + +4. Run the example: + ```bash + # From the examples directory + npx ts-node folders/folders.ts + + # Or if you add it to package.json scripts + npm run folders + ``` + +## Understanding the Example + +This example demonstrates: + +1. **Default Behavior**: Listing all folders with multi-level hierarchy (default) +2. **Single-Level Listing**: Using `singleLevel: true` to get only direct children +3. **Combined Parameters**: Using `singleLevel` with `parentId` to control the starting point +4. **Comparison**: Side-by-side comparison of single-level vs multi-level results +5. **Parameter Variations**: All possible combinations of the `singleLevel` parameter + +## Use Cases + +The `singleLevel` parameter is particularly useful when: + +- **Building UI Navigation**: You want to show only immediate child folders in a tree view +- **Performance Optimization**: Reducing the amount of data returned when you only need direct children +- **Hierarchical Processing**: Processing folder structures level by level rather than all at once +- **Microsoft-Specific Features**: Taking advantage of Microsoft's folder hierarchy capabilities + +## API Reference + +### ListFolderQueryParams Interface + +```typescript +interface ListFolderQueryParams extends ListQueryParams { + /** + * (Microsoft and EWS only.) Use the ID of a folder to find all child folders it contains. + */ + parentId?: string; + + /** + * (Microsoft only) If true, retrieves folders from a single-level hierarchy only. + * If false, retrieves folders across a multi-level hierarchy. + * @default false + */ + singleLevel?: boolean; +} +``` + +### Example Usage + +```typescript +import Nylas from 'nylas'; + +const nylas = new Nylas({ apiKey: 'your-api-key' }); + +// Get only direct children of a specific folder +const singleLevelFolders = await nylas.folders.list({ + identifier: 'grant-id', + queryParams: { + parentId: 'parent-folder-id', + singleLevel: true + } +}); + +// Get all descendants (default behavior) +const allFolders = await nylas.folders.list({ + identifier: 'grant-id', + queryParams: { + parentId: 'parent-folder-id', + singleLevel: false // or omit this parameter + } +}); +``` + +## Documentation + +For more information, see the [Nylas API Documentation](https://developer.nylas.com/). \ No newline at end of file diff --git a/examples/folders/folders.ts b/examples/folders/folders.ts new file mode 100644 index 00000000..7085b366 --- /dev/null +++ b/examples/folders/folders.ts @@ -0,0 +1,218 @@ +import dotenv from 'dotenv'; +import path from 'path'; +import * as process from 'process'; +import Nylas, { + Folder, + NylasResponse, + NylasListResponse, + NylasApiError, + ListFolderQueryParams +} from 'nylas'; + +// Load environment variables from .env file +dotenv.config({ path: path.resolve(__dirname, '../.env') }); + +const NYLAS_API_KEY = process.env.NYLAS_API_KEY; +const NYLAS_GRANT_ID = process.env.NYLAS_GRANT_ID; + +if (!NYLAS_API_KEY) { + console.error('NYLAS_API_KEY is required. Please add it to your .env file.'); + process.exit(1); +} + +if (!NYLAS_GRANT_ID) { + console.error('NYLAS_GRANT_ID is required. Please add it to your .env file.'); + process.exit(1); +} + +async function listFoldersExample() { + try { + // Initialize Nylas client + const nylas = new Nylas({ + apiKey: NYLAS_API_KEY!, + apiUri: process.env.NYLAS_API_URI || 'https://api.us.nylas.com', + }); + + console.log('=== Nylas Folders API Demo ===\n'); + + // 1. List all folders (default behavior - multi-level hierarchy) + console.log('1. Listing all folders (multi-level hierarchy):'); + const allFolders: NylasListResponse = await nylas.folders.list({ + identifier: NYLAS_GRANT_ID!, + queryParams: {} + }); + + console.log(`Found ${allFolders.data.length} folders total:`); + allFolders.data.forEach((folder, index) => { + console.log(` ${index + 1}. ${folder.name} (ID: ${folder.id})`); + if (folder.parentId) { + console.log(` └─ Parent ID: ${folder.parentId}`); + } + if (folder.childCount !== undefined) { + console.log(` └─ Child Count: ${folder.childCount}`); + } + }); + console.log(); + + // 2. List folders with single-level hierarchy (Microsoft only) + console.log('2. Listing folders with single-level hierarchy (Microsoft only):'); + const singleLevelFolders: NylasListResponse = await nylas.folders.list({ + identifier: NYLAS_GRANT_ID!, + queryParams: { + singleLevel: true + } as any + }); + + console.log(`Found ${singleLevelFolders.data.length} folders at single level:`); + singleLevelFolders.data.forEach((folder, index) => { + console.log(` ${index + 1}. ${folder.name} (ID: ${folder.id})`); + if (folder.parentId) { + console.log(` └─ Parent ID: ${folder.parentId}`); + } + }); + console.log(); + + // 3. List folders with both singleLevel and parentId parameters + const rootFolders = allFolders.data.filter(folder => !folder.parentId); + if (rootFolders.length > 0) { + const rootFolder = rootFolders[0]; + console.log(`3. Listing child folders of "${rootFolder.name}" with single-level hierarchy:`); + + const childFolders: NylasListResponse = await nylas.folders.list({ + identifier: NYLAS_GRANT_ID!, + queryParams: { + parentId: rootFolder.id, + singleLevel: true + } as any + }); + + console.log(`Found ${childFolders.data.length} direct child folders:`); + childFolders.data.forEach((folder, index) => { + console.log(` ${index + 1}. ${folder.name} (ID: ${folder.id})`); + }); + } else { + console.log('3. No root folders found to demonstrate parentId + singleLevel combination.'); + } + console.log(); + + // 4. Compare single-level vs multi-level for the same parent + if (rootFolders.length > 0) { + const rootFolder = rootFolders[0]; + console.log('4. Comparing single-level vs multi-level hierarchy:'); + + // Multi-level (default) + const multiLevelChildren: NylasListResponse = await nylas.folders.list({ + identifier: NYLAS_GRANT_ID!, + queryParams: { + parentId: rootFolder.id, + singleLevel: false // explicit false + } as any + }); + + // Single-level + const singleLevelChildren: NylasListResponse = await nylas.folders.list({ + identifier: NYLAS_GRANT_ID!, + queryParams: { + parentId: rootFolder.id, + singleLevel: true + } as any + }); + + console.log(`Multi-level hierarchy: ${multiLevelChildren.data.length} folders`); + console.log(`Single-level hierarchy: ${singleLevelChildren.data.length} folders`); + + if (multiLevelChildren.data.length !== singleLevelChildren.data.length) { + console.log('📝 Note: Different folder counts indicate the singleLevel parameter is working correctly.'); + console.log(' Multi-level includes nested folders, single-level shows only direct children.'); + } + } + + } catch (error) { + if (error instanceof NylasApiError) { + console.error('Nylas API Error:', error.message); + console.error('Status Code:', error.statusCode); + console.error('Error Type:', error.type); + } else { + console.error('Unexpected error:', error); + } + } +} + +// Enhanced demonstration with detailed explanations +async function detailedFoldersDemo() { + try { + const nylas = new Nylas({ + apiKey: NYLAS_API_KEY!, + apiUri: process.env.NYLAS_API_URI || 'https://api.us.nylas.com', + }); + + console.log('\n=== Detailed singleLevel Parameter Demo ===\n'); + + console.log('The singleLevel parameter controls folder hierarchy traversal:'); + console.log('• singleLevel: true → Returns only direct children (single level)'); + console.log('• singleLevel: false → Returns all descendants (multi-level, default)'); + console.log('• Microsoft accounts only - ignored for other providers\n'); + + // Show all query parameter combinations + const queryVariations: Array<{name: string, params: any}> = [ + { + name: 'Default (multi-level)', + params: {} + }, + { + name: 'Explicit multi-level', + params: { singleLevel: false } + }, + { + name: 'Single-level only', + params: { singleLevel: true } + } + ]; + + for (const variation of queryVariations) { + console.log(`--- ${variation.name} ---`); + console.log(`Query params: ${JSON.stringify(variation.params)}`); + + try { + const folders: NylasListResponse = await nylas.folders.list({ + identifier: NYLAS_GRANT_ID!, + queryParams: variation.params + }); + + console.log(`Result: ${folders.data.length} folders found`); + + // Show folder hierarchy structure + const rootFolders = folders.data.filter(f => !f.parentId); + const childFolders = folders.data.filter(f => f.parentId); + + console.log(`├─ Root folders: ${rootFolders.length}`); + console.log(`└─ Child folders: ${childFolders.length}`); + } catch (error) { + console.log(`Error: ${error instanceof NylasApiError ? error.message : 'Unknown error'}`); + } + console.log(); + } + + } catch (error) { + console.error('Demo error:', error); + } +} + +// Run the examples +async function main() { + await listFoldersExample(); + await detailedFoldersDemo(); + + console.log('=== Folders API Demo Complete ==='); + console.log('\nKey takeaways:'); + console.log('1. The singleLevel parameter is Microsoft-specific'); + console.log('2. Use singleLevel: true to get only direct children'); + console.log('3. Use singleLevel: false (or omit) for full hierarchy'); + console.log('4. Combine with parentId to control which folder to start from'); +} + +if (require.main === module) { + main().catch(console.error); +} + +export default main; \ No newline at end of file diff --git a/examples/package.json b/examples/package.json index 5d8a147b..64ade60f 100644 --- a/examples/package.json +++ b/examples/package.json @@ -8,7 +8,8 @@ "build": "tsc", "notetakers": "ts-node notetakers/notetaker.ts", "calendars": "ts-node calendars/event_with_notetaker.ts", - "messages": "ts-node messages/messages.ts" + "messages": "ts-node messages/messages.ts", + "folders": "ts-node folders/folders.ts" }, "dependencies": { "dotenv": "^16.0.0", diff --git a/src/models/folders.ts b/src/models/folders.ts index 87ccfe92..c3e33ffb 100644 --- a/src/models/folders.ts +++ b/src/models/folders.ts @@ -101,6 +101,13 @@ export interface ListFolderQueryParams extends ListQueryParams { * (Microsoft and EWS only.) Use the ID of a folder to find all child folders it contains. */ parentId?: string; + + /** + * (Microsoft only) If true, retrieves folders from a single-level hierarchy only. + * If false, retrieves folders across a multi-level hierarchy. + * @default false + */ + singleLevel?: boolean; } export type UpdateFolderRequest = Partial; diff --git a/tests/resources/folders.spec.ts b/tests/resources/folders.spec.ts index ae84a390..57a36dc0 100644 --- a/tests/resources/folders.spec.ts +++ b/tests/resources/folders.spec.ts @@ -73,6 +73,50 @@ describe('Folders', () => { }, }); }); + + it('should call apiClient.request with query params including single_level', async () => { + await folders.list({ + identifier: 'id123', + queryParams: { + singleLevel: true, + parentId: 'parent123', + }, + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'GET', + path: '/v3/grants/id123/folders', + queryParams: { + singleLevel: true, + parentId: 'parent123', + }, + overrides: { + apiUri: 'https://test.api.nylas.com', + headers: { override: 'bar' }, + }, + }); + }); + + it('should call apiClient.request with single_level set to false', async () => { + await folders.list({ + identifier: 'id123', + queryParams: { + singleLevel: false, + }, + }); + + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'GET', + path: '/v3/grants/id123/folders', + queryParams: { + singleLevel: false, + }, + }); + }); }); describe('find', () => {