@rachelallyson/planning-center-people-ts 1.1.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +181 -0
- package/README.md +16 -0
- package/dist/batch.d.ts +47 -0
- package/dist/batch.js +376 -0
- package/dist/client-manager.d.ts +66 -0
- package/dist/client-manager.js +156 -0
- package/dist/client.d.ts +71 -0
- package/dist/client.js +123 -0
- package/dist/core/http.d.ts +48 -0
- package/dist/core/http.js +265 -0
- package/dist/core/pagination.d.ts +34 -0
- package/dist/core/pagination.js +164 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +23 -5
- package/dist/matching/matcher.d.ts +41 -0
- package/dist/matching/matcher.js +161 -0
- package/dist/matching/scoring.d.ts +35 -0
- package/dist/matching/scoring.js +141 -0
- package/dist/matching/strategies.d.ts +35 -0
- package/dist/matching/strategies.js +79 -0
- package/dist/modules/base.d.ts +46 -0
- package/dist/modules/base.js +82 -0
- package/dist/modules/contacts.d.ts +103 -0
- package/dist/modules/contacts.js +130 -0
- package/dist/modules/fields.d.ts +157 -0
- package/dist/modules/fields.js +294 -0
- package/dist/modules/households.d.ts +42 -0
- package/dist/modules/households.js +74 -0
- package/dist/modules/lists.d.ts +62 -0
- package/dist/modules/lists.js +92 -0
- package/dist/modules/notes.d.ts +74 -0
- package/dist/modules/notes.js +125 -0
- package/dist/modules/people.d.ts +196 -0
- package/dist/modules/people.js +221 -0
- package/dist/modules/workflows.d.ts +131 -0
- package/dist/modules/workflows.js +221 -0
- package/dist/monitoring.d.ts +53 -0
- package/dist/monitoring.js +142 -0
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.js +24 -0
- package/dist/testing/recorder.d.ts +58 -0
- package/dist/testing/recorder.js +195 -0
- package/dist/testing/simple-builders.d.ts +33 -0
- package/dist/testing/simple-builders.js +124 -0
- package/dist/testing/simple-factories.d.ts +91 -0
- package/dist/testing/simple-factories.js +288 -0
- package/dist/testing/types.d.ts +160 -0
- package/dist/testing/types.js +5 -0
- package/dist/types/batch.d.ts +50 -0
- package/dist/types/batch.js +5 -0
- package/dist/types/client.d.ts +89 -0
- package/dist/types/client.js +5 -0
- package/dist/types/events.d.ts +85 -0
- package/dist/types/events.js +5 -0
- package/dist/types/people.d.ts +20 -1
- package/package.json +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,187 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.1.0] - 2025-01-17
|
|
9
|
+
|
|
10
|
+
### 🔒 **SECURITY RELEASE - Required Refresh Token Handling**
|
|
11
|
+
|
|
12
|
+
This release addresses a critical security issue where OAuth 2.0 clients could lose access when tokens expire without proper refresh handling.
|
|
13
|
+
|
|
14
|
+
### Breaking Changes
|
|
15
|
+
|
|
16
|
+
- **OAuth 2.0 Authentication**: `onRefresh` and `onRefreshFailure` callbacks are now **required** for OAuth configurations
|
|
17
|
+
- **Type Safety**: Enhanced type-safe authentication configuration prevents invalid configurations at compile time
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
- **CRITICAL**: OAuth 2.0 authentication now requires refresh token handling to prevent token loss
|
|
22
|
+
- **BREAKING**: Type-safe authentication configuration enforces required fields
|
|
23
|
+
- Enhanced token refresh implementation with proper error handling
|
|
24
|
+
- Improved authentication type safety with union types
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Fixed person matching to properly handle default fuzzy strategy
|
|
29
|
+
- Fixed mock client to support createWithContacts method
|
|
30
|
+
- Fixed event system tests to work with mock client
|
|
31
|
+
- Fixed phone number builder in mock response builder
|
|
32
|
+
|
|
33
|
+
### Migration from v2.0.0
|
|
34
|
+
|
|
35
|
+
**Before (v2.0.0):**
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const client = new PcoClient({
|
|
39
|
+
auth: {
|
|
40
|
+
type: 'oauth',
|
|
41
|
+
accessToken: 'access-token',
|
|
42
|
+
refreshToken: 'refresh-token'
|
|
43
|
+
// Missing required callbacks - this will now cause TypeScript errors
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**After (v2.1.0):**
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const client = new PcoClient({
|
|
52
|
+
auth: {
|
|
53
|
+
type: 'oauth',
|
|
54
|
+
accessToken: 'access-token',
|
|
55
|
+
refreshToken: 'refresh-token',
|
|
56
|
+
// REQUIRED: Handle token refresh to prevent token loss
|
|
57
|
+
onRefresh: async (tokens) => {
|
|
58
|
+
await saveTokensToDatabase(userId, tokens);
|
|
59
|
+
},
|
|
60
|
+
// REQUIRED: Handle refresh failures
|
|
61
|
+
onRefreshFailure: async (error) => {
|
|
62
|
+
console.error('Token refresh failed:', error.message);
|
|
63
|
+
await clearUserTokens(userId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## [2.0.0] - 2025-01-17
|
|
70
|
+
|
|
71
|
+
### 🚀 **MAJOR RELEASE - Complete API Redesign**
|
|
72
|
+
|
|
73
|
+
This is a **breaking change** release that completely redesigns the API for better developer experience, type safety, and maintainability.
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
|
|
77
|
+
#### **🏗️ New Class-Based Architecture**
|
|
78
|
+
|
|
79
|
+
- **PcoClient Class**: Main client with modular architecture
|
|
80
|
+
- **PcoClientManager**: Automatic client caching and lifecycle management
|
|
81
|
+
- **Event System**: Comprehensive event emission for monitoring and debugging
|
|
82
|
+
- **Module Architecture**: Organized API interactions into focused modules
|
|
83
|
+
|
|
84
|
+
#### **🔧 Core Utilities**
|
|
85
|
+
|
|
86
|
+
- **Built-in Pagination**: `getAllPages()` method for automatic pagination
|
|
87
|
+
- **Batch Operations**: Execute multiple operations with dependency resolution
|
|
88
|
+
- **Person Matching**: Smart person matching with fuzzy logic and `findOrCreate`
|
|
89
|
+
- **Type-Safe Field Operations**: Enhanced custom field operations with caching
|
|
90
|
+
- **Workflow State Management**: Smart workflow operations with duplicate detection
|
|
91
|
+
|
|
92
|
+
#### **📦 New Modules**
|
|
93
|
+
|
|
94
|
+
- **PeopleModule**: Core person operations with smart matching
|
|
95
|
+
- **FieldsModule**: Type-safe custom field operations with caching
|
|
96
|
+
- **WorkflowsModule**: Complete workflow and workflow card management
|
|
97
|
+
- **ContactsModule**: Email, phone, address, and social profile management
|
|
98
|
+
- **HouseholdsModule**: Household operations and member management
|
|
99
|
+
- **NotesModule**: Note and note category operations
|
|
100
|
+
- **ListsModule**: List and list category operations with rule-based membership
|
|
101
|
+
|
|
102
|
+
#### **🔐 Enhanced Authentication**
|
|
103
|
+
|
|
104
|
+
- **OAuth 2.0 Support**: Full OAuth with automatic token refresh
|
|
105
|
+
- **Personal Access Token**: HTTP Basic Auth support
|
|
106
|
+
- **Token Refresh**: Automatic refresh with callback support
|
|
107
|
+
- **Environment Persistence**: Automatic token persistence in test environments
|
|
108
|
+
|
|
109
|
+
#### **⚡ Performance & Reliability**
|
|
110
|
+
|
|
111
|
+
- **Rate Limiting**: Built-in rate limiting (100 req/min)
|
|
112
|
+
- **Error Handling**: Comprehensive error handling with retry logic
|
|
113
|
+
- **Request Timeouts**: Configurable request timeouts
|
|
114
|
+
- **Event Monitoring**: Real-time request/response monitoring
|
|
115
|
+
|
|
116
|
+
#### **🧪 Testing Infrastructure**
|
|
117
|
+
|
|
118
|
+
- **MockPcoClient**: Complete mock implementation for testing
|
|
119
|
+
- **MockResponseBuilder**: Response building utilities
|
|
120
|
+
- **RequestRecorder**: Request recording for testing
|
|
121
|
+
- **Integration Tests**: 129 comprehensive integration tests
|
|
122
|
+
|
|
123
|
+
### Changed
|
|
124
|
+
|
|
125
|
+
#### **🔄 Breaking Changes**
|
|
126
|
+
|
|
127
|
+
- **API Design**: Complete redesign from functional to class-based approach
|
|
128
|
+
- **Import Structure**: New import structure with `PcoClient` class
|
|
129
|
+
- **Method Names**: Updated method names for consistency
|
|
130
|
+
- **Type Definitions**: Enhanced type definitions with better type safety
|
|
131
|
+
|
|
132
|
+
#### **📈 Improvements**
|
|
133
|
+
|
|
134
|
+
- **Type Safety**: Enhanced TypeScript support with strict typing
|
|
135
|
+
- **Error Messages**: More descriptive error messages and handling
|
|
136
|
+
- **Documentation**: Comprehensive inline documentation
|
|
137
|
+
- **Performance**: Optimized request handling and caching
|
|
138
|
+
|
|
139
|
+
### Migration Guide
|
|
140
|
+
|
|
141
|
+
#### **Before (v1.x)**
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { createPcoClient, getPeople, createPerson } from '@rachelallyson/planning-center-people-ts';
|
|
145
|
+
|
|
146
|
+
const client = createPcoClient({
|
|
147
|
+
personalAccessToken: 'your-token',
|
|
148
|
+
appId: 'your-app-id',
|
|
149
|
+
appSecret: 'your-app-secret'
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const people = await getPeople(client, { per_page: 10 });
|
|
153
|
+
const person = await createPerson(client, { first_name: 'John', last_name: 'Doe' });
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### **After (v2.0.0)**
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { PcoClient } from '@rachelallyson/planning-center-people-ts';
|
|
160
|
+
|
|
161
|
+
const client = new PcoClient({
|
|
162
|
+
auth: {
|
|
163
|
+
type: 'personal_access_token',
|
|
164
|
+
personalAccessToken: 'your-token'
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const people = await client.people.getAll({ perPage: 10 });
|
|
169
|
+
const person = await client.people.create({ first_name: 'John', last_name: 'Doe' });
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Removed
|
|
173
|
+
|
|
174
|
+
- **Functional API**: All functional API methods removed in favor of class-based approach
|
|
175
|
+
- **Legacy Types**: Old type definitions replaced with enhanced versions
|
|
176
|
+
- **Deprecated Methods**: All deprecated methods removed
|
|
177
|
+
|
|
178
|
+
### Fixed
|
|
179
|
+
|
|
180
|
+
- **Type Safety**: Resolved all TypeScript strict mode issues
|
|
181
|
+
- **Error Handling**: Improved error handling and retry logic
|
|
182
|
+
- **Rate Limiting**: Fixed rate limiting edge cases
|
|
183
|
+
- **Authentication**: Resolved token refresh and persistence issues
|
|
184
|
+
- Fixed person matching to properly handle default fuzzy strategy
|
|
185
|
+
- Fixed mock client to support createWithContacts method
|
|
186
|
+
- Fixed event system tests to work with mock client
|
|
187
|
+
- Fixed phone number builder in mock response builder
|
|
188
|
+
|
|
8
189
|
## [1.1.0] - 2025-10-08
|
|
9
190
|
|
|
10
191
|
### Added
|
package/README.md
CHANGED
|
@@ -515,6 +515,22 @@ See [TYPE_VALIDATION_SUMMARY.md](./TYPE_VALIDATION_SUMMARY.md) for detailed docu
|
|
|
515
515
|
4. Add tests (both unit and integration)
|
|
516
516
|
5. Submit a pull request
|
|
517
517
|
|
|
518
|
+
## 📚 Comprehensive Documentation
|
|
519
|
+
|
|
520
|
+
This library includes extensive documentation covering all aspects of usage:
|
|
521
|
+
|
|
522
|
+
- **[📖 Complete Documentation](./docs/README.md)** - Comprehensive guide covering all features
|
|
523
|
+
- **[🚀 Getting Started](./docs/OVERVIEW.md)** - What this library does and why you should use it
|
|
524
|
+
- **[⚙️ Installation Guide](./docs/INSTALLATION.md)** - Complete setup instructions for all environments
|
|
525
|
+
- **[🔐 Authentication Guide](./docs/AUTHENTICATION.md)** - All authentication methods and token management
|
|
526
|
+
- **[📋 API Reference](./docs/API_REFERENCE.md)** - Complete reference for all 40+ functions
|
|
527
|
+
- **[💡 Examples & Patterns](./docs/EXAMPLES.md)** - Real-world examples and common patterns
|
|
528
|
+
- **[🛠️ Error Handling](./docs/ERROR_HANDLING.md)** - Advanced error management and recovery
|
|
529
|
+
- **[⚡ Performance Guide](./docs/PERFORMANCE.md)** - Optimization techniques and bulk operations
|
|
530
|
+
- **[🔧 Troubleshooting](./docs/TROUBLESHOOTING.md)** - Common issues and solutions
|
|
531
|
+
- **[🔄 Migration Guide](./docs/MIGRATION.md)** - Switching from other libraries
|
|
532
|
+
- **[⭐ Best Practices](./docs/BEST_PRACTICES.md)** - Production-ready patterns and security
|
|
533
|
+
|
|
518
534
|
## License
|
|
519
535
|
|
|
520
536
|
MIT
|
package/dist/batch.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2.0.0 Batch Operations Executor
|
|
3
|
+
*/
|
|
4
|
+
import type { PcoClient } from './client';
|
|
5
|
+
import type { PcoEventEmitter } from './monitoring';
|
|
6
|
+
import type { BatchOperation, BatchOptions, BatchSummary } from './types/batch';
|
|
7
|
+
export declare class BatchExecutor {
|
|
8
|
+
private client;
|
|
9
|
+
private eventEmitter;
|
|
10
|
+
constructor(client: PcoClient, eventEmitter: PcoEventEmitter);
|
|
11
|
+
/**
|
|
12
|
+
* Execute a batch of operations
|
|
13
|
+
*/
|
|
14
|
+
execute<T = any>(operations: BatchOperation[], options?: BatchOptions): Promise<BatchSummary>;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve operation dependencies and references
|
|
17
|
+
*/
|
|
18
|
+
private resolveOperations;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve references in operation data
|
|
21
|
+
*/
|
|
22
|
+
private resolveReferences;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve string references like "$0.id" or "$1.data.attributes.name"
|
|
25
|
+
*/
|
|
26
|
+
private resolveStringReferences;
|
|
27
|
+
/**
|
|
28
|
+
* Get nested value from object using dot notation
|
|
29
|
+
*/
|
|
30
|
+
private getNestedValue;
|
|
31
|
+
/**
|
|
32
|
+
* Find dependencies for an operation
|
|
33
|
+
*/
|
|
34
|
+
private findDependencies;
|
|
35
|
+
/**
|
|
36
|
+
* Execute a single operation
|
|
37
|
+
*/
|
|
38
|
+
private executeOperation;
|
|
39
|
+
/**
|
|
40
|
+
* Rollback successful operations
|
|
41
|
+
*/
|
|
42
|
+
private rollbackOperations;
|
|
43
|
+
/**
|
|
44
|
+
* Rollback a single operation
|
|
45
|
+
*/
|
|
46
|
+
private rollbackOperation;
|
|
47
|
+
}
|
package/dist/batch.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* v2.0.0 Batch Operations Executor
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BatchExecutor = void 0;
|
|
7
|
+
class BatchExecutor {
|
|
8
|
+
constructor(client, eventEmitter) {
|
|
9
|
+
this.client = client;
|
|
10
|
+
this.eventEmitter = eventEmitter;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Execute a batch of operations
|
|
14
|
+
*/
|
|
15
|
+
async execute(operations, options = {}) {
|
|
16
|
+
const { continueOnError = true, maxConcurrency = 5, enableRollback = false, onOperationComplete, onBatchComplete, } = options;
|
|
17
|
+
const startTime = Date.now();
|
|
18
|
+
const results = [];
|
|
19
|
+
const successfulOperations = [];
|
|
20
|
+
try {
|
|
21
|
+
// Resolve operation dependencies and references
|
|
22
|
+
const resolvedOperations = await this.resolveOperations(operations);
|
|
23
|
+
// Execute operations with dependency resolution
|
|
24
|
+
const semaphore = new Semaphore(maxConcurrency);
|
|
25
|
+
const operationResults = new Map();
|
|
26
|
+
const executeOperationWithDependencies = async (operation, index) => {
|
|
27
|
+
// Wait for dependencies to complete
|
|
28
|
+
if (operation.dependencies && operation.dependencies.length > 0) {
|
|
29
|
+
const dependencyPromises = operation.dependencies.map(depId => {
|
|
30
|
+
// Handle index-based dependencies
|
|
31
|
+
if (depId.startsWith('$index_')) {
|
|
32
|
+
const depIndex = parseInt(depId.substring(7));
|
|
33
|
+
const depOperation = resolvedOperations[depIndex];
|
|
34
|
+
if (!depOperation) {
|
|
35
|
+
throw new Error(`Dependency at index ${depIndex} not found`);
|
|
36
|
+
}
|
|
37
|
+
const depResult = operationResults.get(depOperation.id);
|
|
38
|
+
if (!depResult) {
|
|
39
|
+
throw new Error(`Dependency '${depOperation.id}' not found in operations list`);
|
|
40
|
+
}
|
|
41
|
+
return depResult;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Handle operation ID-based dependencies
|
|
45
|
+
const depResult = operationResults.get(depId);
|
|
46
|
+
if (!depResult) {
|
|
47
|
+
throw new Error(`Dependency '${depId}' not found in operations list`);
|
|
48
|
+
}
|
|
49
|
+
return depResult;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
await Promise.all(dependencyPromises);
|
|
53
|
+
}
|
|
54
|
+
await semaphore.acquire();
|
|
55
|
+
try {
|
|
56
|
+
// Create a function to get current results for reference resolution
|
|
57
|
+
const getCurrentResults = () => results;
|
|
58
|
+
const result = await this.executeOperation(operation, getCurrentResults);
|
|
59
|
+
const batchResult = {
|
|
60
|
+
index,
|
|
61
|
+
operation,
|
|
62
|
+
success: true,
|
|
63
|
+
data: result,
|
|
64
|
+
};
|
|
65
|
+
results.push(batchResult);
|
|
66
|
+
successfulOperations.push(operation);
|
|
67
|
+
onOperationComplete?.(batchResult);
|
|
68
|
+
return batchResult;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const batchResult = {
|
|
72
|
+
index,
|
|
73
|
+
operation,
|
|
74
|
+
success: false,
|
|
75
|
+
error: error,
|
|
76
|
+
};
|
|
77
|
+
results.push(batchResult);
|
|
78
|
+
if (!continueOnError) {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
onOperationComplete?.(batchResult);
|
|
82
|
+
return batchResult;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
semaphore.release();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
// Create promises for all operations
|
|
89
|
+
const operationPromises = resolvedOperations.map((operation, index) => {
|
|
90
|
+
const promise = executeOperationWithDependencies(operation, index);
|
|
91
|
+
operationResults.set(operation.id, promise);
|
|
92
|
+
return promise;
|
|
93
|
+
});
|
|
94
|
+
await Promise.all(operationPromises);
|
|
95
|
+
const summary = {
|
|
96
|
+
total: operations.length,
|
|
97
|
+
successful: results.filter(r => r.success).length,
|
|
98
|
+
failed: results.filter(r => !r.success).length,
|
|
99
|
+
successRate: results.length > 0 ? results.filter(r => r.success).length / results.length : 0,
|
|
100
|
+
duration: Date.now() - startTime,
|
|
101
|
+
results,
|
|
102
|
+
};
|
|
103
|
+
onBatchComplete?.(results);
|
|
104
|
+
return summary;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Rollback successful operations if enabled
|
|
108
|
+
if (enableRollback && successfulOperations.length > 0) {
|
|
109
|
+
await this.rollbackOperations(successfulOperations);
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve operation dependencies and references
|
|
116
|
+
*/
|
|
117
|
+
async resolveOperations(operations) {
|
|
118
|
+
const resolved = [];
|
|
119
|
+
for (let i = 0; i < operations.length; i++) {
|
|
120
|
+
const operation = operations[i];
|
|
121
|
+
const resolvedOperation = { ...operation };
|
|
122
|
+
// Resolve references in data
|
|
123
|
+
if (operation.data) {
|
|
124
|
+
resolvedOperation.resolvedData = await this.resolveReferences(operation.data, resolved);
|
|
125
|
+
}
|
|
126
|
+
// Determine dependencies
|
|
127
|
+
resolvedOperation.dependencies = this.findDependencies(operation, i);
|
|
128
|
+
resolved.push(resolvedOperation);
|
|
129
|
+
}
|
|
130
|
+
return resolved;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Resolve references in operation data
|
|
134
|
+
*/
|
|
135
|
+
async resolveReferences(data, previousResults) {
|
|
136
|
+
if (typeof data === 'string') {
|
|
137
|
+
// Convert ResolvedBatchOperation[] to BatchResult[] for resolveStringReferences
|
|
138
|
+
const batchResults = previousResults.map((op, index) => ({
|
|
139
|
+
index,
|
|
140
|
+
operation: op,
|
|
141
|
+
success: true,
|
|
142
|
+
data: op.resolvedData || op.data,
|
|
143
|
+
}));
|
|
144
|
+
return this.resolveStringReferences(data, batchResults);
|
|
145
|
+
}
|
|
146
|
+
if (Array.isArray(data)) {
|
|
147
|
+
return Promise.all(data.map(item => this.resolveReferences(item, previousResults)));
|
|
148
|
+
}
|
|
149
|
+
if (data && typeof data === 'object') {
|
|
150
|
+
const resolved = {};
|
|
151
|
+
for (const [key, value] of Object.entries(data)) {
|
|
152
|
+
resolved[key] = await this.resolveReferences(value, previousResults);
|
|
153
|
+
}
|
|
154
|
+
return resolved;
|
|
155
|
+
}
|
|
156
|
+
return data;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Resolve string references like "$0.id" or "$1.data.attributes.name"
|
|
160
|
+
*/
|
|
161
|
+
resolveStringReferences(str, previousResults) {
|
|
162
|
+
return str.replace(/\$(\d+)\.([\w.]+)/g, (match, indexStr, path) => {
|
|
163
|
+
const index = parseInt(indexStr);
|
|
164
|
+
if (index < previousResults.length) {
|
|
165
|
+
const result = previousResults[index];
|
|
166
|
+
// For simple references like $0.id, look in result.data.id
|
|
167
|
+
if (path === 'id' && result.data?.id) {
|
|
168
|
+
return String(result.data.id);
|
|
169
|
+
}
|
|
170
|
+
// For other paths, look in result.data
|
|
171
|
+
const value = this.getNestedValue(result.data, path);
|
|
172
|
+
return value !== undefined ? String(value) : match;
|
|
173
|
+
}
|
|
174
|
+
return match;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get nested value from object using dot notation
|
|
179
|
+
*/
|
|
180
|
+
getNestedValue(obj, path) {
|
|
181
|
+
return path.split('.').reduce((current, key) => {
|
|
182
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
183
|
+
}, obj);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Find dependencies for an operation
|
|
187
|
+
*/
|
|
188
|
+
findDependencies(operation, currentIndex) {
|
|
189
|
+
const dependencies = [];
|
|
190
|
+
// If operation has explicit dependencies, use those
|
|
191
|
+
if (operation.dependencies && Array.isArray(operation.dependencies)) {
|
|
192
|
+
dependencies.push(...operation.dependencies);
|
|
193
|
+
}
|
|
194
|
+
// Also find references to previous operations by index
|
|
195
|
+
const operationStr = JSON.stringify(operation);
|
|
196
|
+
const referenceMatches = operationStr.match(/\$(\d+)/g);
|
|
197
|
+
if (referenceMatches) {
|
|
198
|
+
for (const match of referenceMatches) {
|
|
199
|
+
const index = parseInt(match.substring(1));
|
|
200
|
+
if (index < currentIndex) {
|
|
201
|
+
// Convert index to operation ID reference
|
|
202
|
+
const depRef = `$index_${index}`;
|
|
203
|
+
if (!dependencies.includes(depRef)) {
|
|
204
|
+
dependencies.push(depRef);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return dependencies;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Execute a single operation
|
|
213
|
+
*/
|
|
214
|
+
async executeOperation(operation, getPreviousResults) {
|
|
215
|
+
const { type, resolvedData, data, endpoint, resourceType } = operation;
|
|
216
|
+
const operationData = resolvedData || data;
|
|
217
|
+
// Check if type is module.method format
|
|
218
|
+
if (type.includes('.')) {
|
|
219
|
+
// Existing module.method approach
|
|
220
|
+
const [module, method] = type.split('.');
|
|
221
|
+
if (!module || !method) {
|
|
222
|
+
throw new Error(`Invalid operation type: ${type}`);
|
|
223
|
+
}
|
|
224
|
+
// Get the appropriate module
|
|
225
|
+
const moduleInstance = this.client[module];
|
|
226
|
+
if (!moduleInstance) {
|
|
227
|
+
throw new Error(`Unknown module: ${module}`);
|
|
228
|
+
}
|
|
229
|
+
// Execute the method
|
|
230
|
+
const methodFunc = moduleInstance[method];
|
|
231
|
+
if (typeof methodFunc !== 'function') {
|
|
232
|
+
throw new Error(`Unknown method: ${module}.${method}`);
|
|
233
|
+
}
|
|
234
|
+
return methodFunc.call(moduleInstance, operationData);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// New endpoint-based approach
|
|
238
|
+
const methodMap = {
|
|
239
|
+
'create': 'POST',
|
|
240
|
+
'update': 'PATCH',
|
|
241
|
+
'delete': 'DELETE'
|
|
242
|
+
};
|
|
243
|
+
const httpMethod = methodMap[type];
|
|
244
|
+
if (!httpMethod || !endpoint) {
|
|
245
|
+
throw new Error(`Invalid operation: ${type} requires endpoint`);
|
|
246
|
+
}
|
|
247
|
+
// Resolve endpoint references
|
|
248
|
+
const previousResults = getPreviousResults();
|
|
249
|
+
const resolvedEndpoint = this.resolveStringReferences(endpoint, previousResults);
|
|
250
|
+
// Map endpoint to module and method
|
|
251
|
+
if (resolvedEndpoint === '/people') {
|
|
252
|
+
// /people endpoint
|
|
253
|
+
if (type === 'create') {
|
|
254
|
+
return this.client.people.create(operationData);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (resolvedEndpoint.startsWith('/people/') && resolvedEndpoint.includes('/emails')) {
|
|
258
|
+
// /people/{id}/emails endpoint
|
|
259
|
+
if (type === 'create') {
|
|
260
|
+
const personId = resolvedEndpoint.split('/')[2];
|
|
261
|
+
return this.client.people.addEmail(personId, operationData);
|
|
262
|
+
}
|
|
263
|
+
else if (type === 'update') {
|
|
264
|
+
const parts = resolvedEndpoint.split('/');
|
|
265
|
+
const personId = parts[2];
|
|
266
|
+
const emailId = parts[4];
|
|
267
|
+
return this.client.people.updateEmail(personId, emailId, operationData);
|
|
268
|
+
}
|
|
269
|
+
else if (type === 'delete') {
|
|
270
|
+
const parts = resolvedEndpoint.split('/');
|
|
271
|
+
const personId = parts[2];
|
|
272
|
+
const emailId = parts[4];
|
|
273
|
+
return this.client.people.deleteEmail(personId, emailId);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (resolvedEndpoint.startsWith('/people/') && resolvedEndpoint.includes('/phone_numbers')) {
|
|
277
|
+
// /people/{id}/phone_numbers endpoint
|
|
278
|
+
if (type === 'create') {
|
|
279
|
+
const personId = resolvedEndpoint.split('/')[2];
|
|
280
|
+
return this.client.people.addPhoneNumber(personId, operationData);
|
|
281
|
+
}
|
|
282
|
+
else if (type === 'update') {
|
|
283
|
+
const parts = resolvedEndpoint.split('/');
|
|
284
|
+
const personId = parts[2];
|
|
285
|
+
const phoneId = parts[4];
|
|
286
|
+
return this.client.people.updatePhoneNumber(personId, phoneId, operationData);
|
|
287
|
+
}
|
|
288
|
+
else if (type === 'delete') {
|
|
289
|
+
const parts = resolvedEndpoint.split('/');
|
|
290
|
+
const personId = parts[2];
|
|
291
|
+
const phoneId = parts[4];
|
|
292
|
+
return this.client.people.deletePhoneNumber(personId, phoneId);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (resolvedEndpoint.startsWith('/people/') && !resolvedEndpoint.includes('/emails') && !resolvedEndpoint.includes('/phone_numbers')) {
|
|
296
|
+
// /people/{id} endpoint
|
|
297
|
+
if (type === 'update') {
|
|
298
|
+
const personId = resolvedEndpoint.split('/')[2];
|
|
299
|
+
return this.client.people.update(personId, operationData);
|
|
300
|
+
}
|
|
301
|
+
else if (type === 'delete') {
|
|
302
|
+
const personId = resolvedEndpoint.split('/')[2];
|
|
303
|
+
return this.client.people.delete(personId);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
throw new Error(`Unsupported endpoint for batch operation: ${resolvedEndpoint}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Rollback successful operations
|
|
311
|
+
*/
|
|
312
|
+
async rollbackOperations(operations) {
|
|
313
|
+
// Reverse the order for rollback
|
|
314
|
+
const rollbackOps = [...operations].reverse();
|
|
315
|
+
for (const operation of rollbackOps) {
|
|
316
|
+
try {
|
|
317
|
+
await this.rollbackOperation(operation);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
console.error(`Failed to rollback operation ${operation.type}:`, error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Rollback a single operation
|
|
326
|
+
*/
|
|
327
|
+
async rollbackOperation(operation) {
|
|
328
|
+
const { type } = operation;
|
|
329
|
+
const [module, method] = type.split('.');
|
|
330
|
+
// Determine rollback method based on operation type
|
|
331
|
+
let rollbackMethod;
|
|
332
|
+
if (method.startsWith('create')) {
|
|
333
|
+
rollbackMethod = method.replace('create', 'delete');
|
|
334
|
+
}
|
|
335
|
+
else if (method.startsWith('add')) {
|
|
336
|
+
rollbackMethod = method.replace('add', 'delete');
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
// For other operations, we might need to implement specific rollback logic
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const moduleInstance = this.client[module];
|
|
343
|
+
if (moduleInstance && typeof moduleInstance[rollbackMethod] === 'function') {
|
|
344
|
+
// This is a simplified rollback - in practice, you'd need to store
|
|
345
|
+
// the created resource IDs to properly rollback
|
|
346
|
+
console.warn(`Rollback not fully implemented for ${type}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
exports.BatchExecutor = BatchExecutor;
|
|
351
|
+
/**
|
|
352
|
+
* Semaphore for controlling concurrency
|
|
353
|
+
*/
|
|
354
|
+
class Semaphore {
|
|
355
|
+
constructor(permits) {
|
|
356
|
+
this.waiting = [];
|
|
357
|
+
this.permits = permits;
|
|
358
|
+
}
|
|
359
|
+
async acquire() {
|
|
360
|
+
if (this.permits > 0) {
|
|
361
|
+
this.permits--;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
return new Promise(resolve => {
|
|
365
|
+
this.waiting.push(resolve);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
release() {
|
|
369
|
+
this.permits++;
|
|
370
|
+
if (this.waiting.length > 0) {
|
|
371
|
+
const resolve = this.waiting.shift();
|
|
372
|
+
this.permits--;
|
|
373
|
+
resolve();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|