@sinoia/hubdoc-tools 1.3.5 → 1.3.7

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.
@@ -1,155 +0,0 @@
1
- # Guide de test du plugin Core
2
-
3
- Ce guide vous permet de tester le plugin Core avec l'instance https://rec.plugandwork.fr.
4
-
5
- ## Prérequis
6
-
7
- 1. Token d'authentification Core valide
8
- 2. HubDoc Tools configuré
9
-
10
- ## Configuration de test
11
-
12
- ### 1. Créer un fichier de configuration
13
-
14
- ```json
15
- {
16
- "baseUrl": "https://rec.plugandwork.fr",
17
- "bearerToken": "YOUR_BEARER_TOKEN_HERE",
18
- "limit": 10
19
- }
20
- ```
21
-
22
- ### 2. Tester la connexion
23
-
24
- ```bash
25
- # Lister les plugins disponibles
26
- npm run dev -- plugins
27
-
28
- # Vérifier que Core est bien listé
29
- ```
30
-
31
- ### 3. Scanner les documents (test rapide)
32
-
33
- Créer un script de test :
34
-
35
- ```javascript
36
- // test-core.js
37
- const fs = require('fs-extra');
38
- const { register } = require('ts-node');
39
- register({ transpileOnly: true, compilerOptions: { esModuleInterop: true } });
40
-
41
- const CorePlugin = require('./plugins/core/index.ts').default;
42
-
43
- async function test() {
44
- const config = {
45
- baseUrl: "https://rec.plugandwork.fr",
46
- bearerToken: "YOUR_TOKEN",
47
- limit: 5
48
- };
49
-
50
- const plugin = new CorePlugin();
51
-
52
- console.log('Testing connection...');
53
- const connected = await plugin.testConnection(config);
54
- console.log('Connected:', connected);
55
-
56
- if (connected) {
57
- console.log('Scanning documents...');
58
- const result = await plugin.scan(config);
59
- console.log(`Found ${result.totalCount} documents`);
60
- result.sources.slice(0, 3).forEach(doc => {
61
- console.log(`- ${doc.name} (${doc.path})`);
62
- });
63
- }
64
- }
65
-
66
- test().catch(console.error);
67
- ```
68
-
69
- Puis exécuter :
70
- ```bash
71
- node test-core.js
72
- ```
73
-
74
- ## Résultats attendus
75
-
76
- ### Scan réussi
77
- - ✅ Connection: SUCCESS
78
- - ✅ Documents trouvés avec métadonnées Core
79
- - ✅ Limitation respectée (paramètre `limit`)
80
- - ✅ Types de documents variés (file, contact, note, email, etc.)
81
-
82
- ### Structure des documents
83
- - **Chemin** : `{type}/{subtype}/{title}.txt`
84
- - **Métadonnées** : Core ID, type, tags, etc.
85
- - **Types MIME** : text/plain par défaut
86
-
87
- ## Test d'intégration complète
88
-
89
- ### 1. Configuration d'une connexion
90
- ```bash
91
- echo '{"baseUrl":"https://rec.plugandwork.fr","bearerToken":"YOUR_TOKEN","limit":10}' > core-config.json
92
- npm run dev -- connect core --config core-config.json --name test-core
93
- ```
94
-
95
- ### 2. Scan des documents
96
- ```bash
97
- npm run dev -- plugin-scan test-core --output core-mapping.csv
98
- ```
99
-
100
- ### 3. Vérification du mapping
101
- ```bash
102
- head -10 core-mapping.csv
103
- ```
104
-
105
- Le fichier doit contenir :
106
- - Colonnes : File Path, Target Folder, Workspace, Metadata (JSON), Permissions
107
- - Métadonnées JSON avec Core ID, type, tags
108
- - Chemins organisés par type Core
109
-
110
- ## Dépannage
111
-
112
- ### Token expiré
113
- ```
114
- Error: Request failed with status code 401
115
- ```
116
- → Vérifier/renouveler le token Bearer
117
-
118
- ### URL incorrecte
119
- ```
120
- Error: ENOTFOUND rec.plugandwork.fr
121
- ```
122
- → Vérifier l'URL de base
123
-
124
- ### Limite dépassée
125
- ```
126
- Error: Too many requests
127
- ```
128
- → Augmenter les délais ou réduire la limit
129
-
130
- ## Performance
131
-
132
- Avec limit=10 :
133
- - Durée : ~1-2 secondes
134
- - Documents : 10 maximum
135
- - Pagination : 1 page
136
-
137
- Avec limit=100 :
138
- - Durée : ~5-10 secondes
139
- - Documents : 100 maximum
140
- - Pagination : Multiple pages
141
-
142
- ## Paramètres utiles
143
-
144
- - **limit** : Nombre max de documents (utile pour tests)
145
- - **tempDir** : Répertoire temporaire pour les buffers
146
- - **bearerToken** : Token d'auth direct (évite username/password)
147
-
148
- ## Types de documents Core observés
149
-
150
- - **file** : Fichiers généraux
151
- - **contact** : Contacts/fiches client
152
- - **note** : Notes internes
153
- - **email** : Emails
154
- - **task** : Tâches
155
- - **doc** : Documents structurés
@@ -1,510 +0,0 @@
1
- import axios, { AxiosInstance } from 'axios';
2
- import fs from 'fs-extra';
3
- import path from 'path';
4
- import { ConcurrentProcessor } from '../../src/utils/concurrent-processor';
5
- import {
6
- DocumentSourcePlugin,
7
- DocumentSource,
8
- PluginConfig,
9
- ScanResult,
10
- PluginImportOptions,
11
- PluginExportOptions,
12
- ImportResult,
13
- ExportResult
14
- } from '../../src/types/plugins';
15
-
16
- interface CoreConfig extends PluginConfig {
17
- baseUrl: string;
18
- username: string;
19
- password: string;
20
- bearerToken?: string;
21
- tempDir?: string; // Directory for local buffer files
22
- }
23
-
24
- interface CoreDocResponse {
25
- data: CoreDoc[];
26
- meta?: {
27
- pagination?: {
28
- page?: number;
29
- pages?: number;
30
- count?: number;
31
- total?: number;
32
- };
33
- };
34
- }
35
-
36
- interface CoreDoc {
37
- id: string;
38
- type: string;
39
- attributes: {
40
- title: string;
41
- body?: string;
42
- type?: string;
43
- subtype?: string;
44
- path?: string;
45
- created_at?: string;
46
- updated_at?: string;
47
- tags?: string[];
48
- percent?: number;
49
- note?: number;
50
- has_attachments?: boolean;
51
- signed?: boolean;
52
- };
53
- relationships?: any;
54
- }
55
-
56
- export default class CorePlugin implements DocumentSourcePlugin {
57
- readonly name = 'core';
58
- readonly version = '1.0.0';
59
- readonly description = 'Core document management system source';
60
- readonly supportedOperations = ['import', 'both'] as const;
61
-
62
- private config?: CoreConfig;
63
- private apiClient?: AxiosInstance;
64
-
65
- async testConnection(config: PluginConfig): Promise<boolean> {
66
- try {
67
- const coreConfig = config as CoreConfig;
68
- const client = await this.createApiClient(coreConfig);
69
-
70
- // Test connection by trying to fetch docs (first page only)
71
- const response = await client.get('/api/d2/docs', {
72
- params: {
73
- '[page][number]': 1,
74
- '[page][size]': 1
75
- }
76
- });
77
-
78
- return response.status === 200;
79
- } catch (error: any) {
80
- console.error(`Core connection test failed: ${error.message}`);
81
- return false;
82
- }
83
- }
84
-
85
- async scan(config: PluginConfig, options?: PluginImportOptions): Promise<ScanResult> {
86
- this.config = config as CoreConfig;
87
- this.apiClient = await this.createApiClient(this.config);
88
-
89
- const sources: DocumentSource[] = [];
90
- const errors: string[] = [];
91
- let totalSize = 0;
92
-
93
- try {
94
- const limit = (this.config as any).limit || (options as any)?.limit;
95
- console.log(`🔍 Scanning Core documents${limit ? ` (limit: ${limit})` : ''}...`);
96
-
97
- // Scan all pages of documents
98
- let currentPage = 1;
99
- let hasMorePages = true;
100
- const pageSize = limit && limit < 100 ? limit : 100; // Use limit if smaller than default batch
101
-
102
- while (hasMorePages && (!limit || sources.length < limit)) {
103
- try {
104
- const response = await this.apiClient.get<CoreDocResponse>('/api/d2/docs', {
105
- params: {
106
- '[page][number]': currentPage,
107
- '[page][size]': pageSize,
108
- // Add filters if provided in options
109
- ...(options?.filters?.path && { path: options.filters.path }),
110
- ...(options?.filters?.mimeTypes && { type: options.filters.mimeTypes.join(',') })
111
- }
112
- });
113
-
114
- const docs = response.data.data || [];
115
- console.log(`📄 Processing page ${currentPage}: ${docs.length} documents`);
116
-
117
- for (const doc of docs) {
118
- if (doc.attributes && doc.attributes.title) {
119
- const source: DocumentSource = {
120
- id: doc.id,
121
- name: doc.attributes.title,
122
- path: this.buildDocumentPath(doc),
123
- size: this.estimateDocumentSize(doc),
124
- mimeType: this.determineMimeType(doc),
125
- lastModified: new Date(doc.attributes.updated_at || doc.attributes.created_at || Date.now()),
126
- metadata: {
127
- coreId: doc.id,
128
- coreType: doc.attributes.type,
129
- coreSubtype: doc.attributes.subtype,
130
- corePath: doc.attributes.path,
131
- tags: doc.attributes.tags,
132
- hasAttachments: doc.attributes.has_attachments,
133
- signed: doc.attributes.signed,
134
- percent: doc.attributes.percent,
135
- note: doc.attributes.note,
136
- body: doc.attributes.body?.substring(0, 500) + '...' // Truncate body for metadata
137
- }
138
- };
139
-
140
- // Apply filters
141
- if (this.shouldIncludeSource(source, options)) {
142
- sources.push(source);
143
- totalSize += source.size;
144
-
145
- // Stop if we've reached the limit
146
- if (limit && sources.length >= limit) {
147
- console.log(`📏 Reached limit of ${limit} documents`);
148
- break;
149
- }
150
- }
151
- }
152
- }
153
-
154
- // Check for more pages
155
- const pagination = response.data.meta?.pagination;
156
- if (pagination && pagination.page && pagination.pages) {
157
- hasMorePages = pagination.page < pagination.pages;
158
- currentPage++;
159
- } else {
160
- // Fallback: if no docs returned, assume we've reached the end
161
- hasMorePages = docs.length === pageSize;
162
- if (hasMorePages) currentPage++;
163
- }
164
-
165
- // Add small delay to respect API limits
166
- await this.sleep(100);
167
-
168
- } catch (pageError: any) {
169
- console.warn(`⚠️ Warning: Failed to fetch page ${currentPage}: ${pageError.message}`);
170
- errors.push(`Failed to fetch page ${currentPage}: ${pageError.message}`);
171
- break;
172
- }
173
- }
174
-
175
- console.log(`✅ Scan completed: ${sources.length} documents found`);
176
-
177
- return {
178
- sources,
179
- totalCount: sources.length,
180
- totalSize,
181
- errors
182
- };
183
- } catch (error: any) {
184
- return {
185
- sources: [],
186
- totalCount: 0,
187
- totalSize: 0,
188
- errors: [`Core scan failed: ${error.message}`]
189
- };
190
- }
191
- }
192
-
193
- async import(
194
- config: PluginConfig,
195
- sources: DocumentSource[],
196
- targetDir: string,
197
- options?: PluginImportOptions
198
- ): Promise<ImportResult[]> {
199
- this.config = config as CoreConfig;
200
- this.apiClient = await this.createApiClient(this.config);
201
-
202
- const concurrency = options?.concurrent || 1;
203
- const tempDir = this.config.tempDir || path.join(targetDir, '.temp');
204
-
205
- // Ensure temp directory exists
206
- await fs.ensureDir(tempDir);
207
-
208
- console.log(`📥 Starting concurrent import of ${sources.length} documents from Core (${concurrency} jobs)`);
209
-
210
- // Use concurrent processor
211
- const processingResult = await ConcurrentProcessor.processWithProgress(
212
- sources,
213
- async (source: DocumentSource, index: number) => {
214
- const result = await this.importSingle(source, targetDir, tempDir);
215
-
216
- // Rate limiting delay (only for concurrent > 1)
217
- if (concurrency > 1) {
218
- await this.sleep(100); // Reduced delay for concurrent operations
219
- }
220
-
221
- return result;
222
- },
223
- {
224
- concurrency,
225
- operation: 'Import from Core',
226
- itemName: 'documents'
227
- }
228
- );
229
-
230
- // Cleanup temp directory
231
- try {
232
- await fs.remove(tempDir);
233
- console.log('🧹 Temporary files cleaned up');
234
- } catch (cleanupError) {
235
- console.warn('⚠️ Warning: Failed to cleanup temporary directory');
236
- }
237
-
238
- // Filter valid results and handle errors
239
- const validResults = processingResult.results.filter(r => r !== undefined);
240
- const errorResults: ImportResult[] = processingResult.errors.map(({ item, error }) => ({
241
- success: false,
242
- source: item,
243
- error: error.message
244
- }));
245
-
246
- return [...validResults, ...errorResults];
247
- }
248
-
249
- private async importSingle(source: DocumentSource, targetDir: string, tempDir: string): Promise<ImportResult> {
250
- try {
251
- if (!this.apiClient) throw new Error('API client not initialized');
252
-
253
- console.log(`📄 Importing: ${source.name}`);
254
-
255
- // Step 1: Fetch document details and content from Core
256
- const docResponse = await this.apiClient.get(`/api/d2/docs/${source.id}`);
257
- const docData = docResponse.data.data;
258
-
259
- if (!docData) {
260
- throw new Error('Document not found in Core');
261
- }
262
-
263
- // Step 2: Create local buffer file
264
- const tempFileName = `${source.id}_${source.name.replace(/[^a-zA-Z0-9.-]/g, '_')}`;
265
- const tempFilePath = path.join(tempDir, tempFileName);
266
-
267
- // Step 3: Generate document content (Core stores content in body field)
268
- let content = '';
269
-
270
- if (docData.attributes.body) {
271
- content = docData.attributes.body;
272
- } else {
273
- // Fallback: create a minimal document with metadata
274
- content = `Document: ${source.name}\n\n`;
275
- content += `Type: ${source.metadata?.coreType || 'unknown'}\n`;
276
- content += `Subtype: ${source.metadata?.coreSubtype || 'unknown'}\n`;
277
- content += `Created: ${source.lastModified.toISOString()}\n`;
278
- if (source.metadata?.tags && Array.isArray(source.metadata.tags)) {
279
- content += `Tags: ${source.metadata.tags.join(', ')}\n`;
280
- }
281
- content += `\nImported from Core (ID: ${source.id})`;
282
- }
283
-
284
- // Step 4: Write content to temporary file
285
- await fs.writeFile(tempFilePath, content, 'utf8');
286
-
287
- // Step 5: Move to target directory with proper structure
288
- const targetPath = path.join(targetDir, source.path);
289
- const targetDirectory = path.dirname(targetPath);
290
-
291
- await fs.ensureDir(targetDirectory);
292
- await fs.move(tempFilePath, targetPath, { overwrite: true });
293
-
294
- const stats = await fs.stat(targetPath);
295
-
296
- console.log(`✅ Imported: ${source.name} (${stats.size} bytes)`);
297
-
298
- return {
299
- success: true,
300
- source,
301
- localPath: targetPath,
302
- bytesTransferred: stats.size
303
- };
304
- } catch (error: any) {
305
- console.error(`❌ Failed to import ${source.name}: ${error.message}`);
306
- return {
307
- success: false,
308
- source,
309
- error: error.message
310
- };
311
- }
312
- }
313
-
314
- // Export is not supported for Core (documents are managed within Core)
315
- async export?(
316
- config: PluginConfig,
317
- localSources: DocumentSource[],
318
- options?: PluginExportOptions
319
- ): Promise<ExportResult[]> {
320
- throw new Error('Export to Core is not supported. Core documents should be managed within the Core system.');
321
- }
322
-
323
- getConfigSchema(): Record<string, any> {
324
- return {
325
- type: 'object',
326
- properties: {
327
- baseUrl: {
328
- type: 'string',
329
- description: 'Core API base URL (e.g., https://your-core-instance.com)',
330
- required: true
331
- },
332
- username: {
333
- type: 'string',
334
- description: 'Core username for authentication',
335
- required: true
336
- },
337
- password: {
338
- type: 'string',
339
- description: 'Core password for authentication',
340
- required: true
341
- },
342
- bearerToken: {
343
- type: 'string',
344
- description: 'Pre-obtained bearer token (optional, will be fetched if not provided)',
345
- required: false
346
- },
347
- tempDir: {
348
- type: 'string',
349
- description: 'Temporary directory for local buffer files (optional)',
350
- required: false
351
- },
352
- limit: {
353
- type: 'integer',
354
- description: 'Maximum number of documents to scan (useful for testing)',
355
- required: false
356
- }
357
- },
358
- required: ['baseUrl', 'username', 'password']
359
- };
360
- }
361
-
362
- async initialize(config: PluginConfig): Promise<void> {
363
- this.config = config as CoreConfig;
364
-
365
- if (!this.config.baseUrl) {
366
- throw new Error('Core plugin requires baseUrl');
367
- }
368
-
369
- if (!this.config.bearerToken && (!this.config.username || !this.config.password)) {
370
- throw new Error('Core plugin requires either bearerToken or username/password');
371
- }
372
-
373
- // Test authentication
374
- try {
375
- this.apiClient = await this.createApiClient(this.config);
376
- console.log('✅ Core plugin initialized successfully');
377
- } catch (error: any) {
378
- throw new Error(`Failed to initialize Core plugin: ${error.message}`);
379
- }
380
- }
381
-
382
- async destroy(): Promise<void> {
383
- this.config = undefined;
384
- this.apiClient = undefined;
385
- }
386
-
387
- private async createApiClient(config: CoreConfig): Promise<AxiosInstance> {
388
- let bearerToken = config.bearerToken;
389
-
390
- // If no bearer token provided, authenticate to get one
391
- if (!bearerToken && config.username && config.password) {
392
- console.log('🔐 No bearer token provided, authenticating...');
393
- bearerToken = await this.authenticate(config);
394
- } else if (!bearerToken) {
395
- throw new Error('Either bearerToken or username/password must be provided');
396
- } else {
397
- console.log('🔑 Using provided bearer token');
398
- }
399
-
400
- return axios.create({
401
- baseURL: config.baseUrl,
402
- headers: {
403
- 'Authorization': `Bearer ${bearerToken}`,
404
- 'Content-Type': 'application/json'
405
- },
406
- timeout: 30000
407
- });
408
- }
409
-
410
- private async authenticate(config: CoreConfig): Promise<string> {
411
- try {
412
- console.log('🔐 Authenticating with Core...');
413
-
414
- const authClient = axios.create({
415
- baseURL: config.baseUrl,
416
- timeout: 30000
417
- });
418
-
419
- // Use the token endpoint from the swagger
420
- const response = await authClient.post('/api/token', null, {
421
- params: {
422
- 'auth[username]': config.username,
423
- 'auth[password]': config.password,
424
- 'grant_type': 'password'
425
- }
426
- });
427
-
428
- if (response.data && response.data.access_token) {
429
- console.log('✅ Core authentication successful');
430
- return response.data.access_token;
431
- } else {
432
- throw new Error('No access token received from Core API');
433
- }
434
- } catch (error: any) {
435
- throw new Error(`Core authentication failed: ${error.response?.data?.message || error.message}`);
436
- }
437
- }
438
-
439
- private buildDocumentPath(doc: CoreDoc): string {
440
- // Build a meaningful path structure
441
- const type = doc.attributes.type || 'unknown';
442
- const subtype = doc.attributes.subtype || 'general';
443
- const title = doc.attributes.title || doc.id;
444
-
445
- // Clean title for filesystem
446
- const cleanTitle = title.replace(/[^a-zA-Z0-9.-]/g, '_');
447
-
448
- return `${type}/${subtype}/${cleanTitle}.txt`;
449
- }
450
-
451
- private estimateDocumentSize(doc: CoreDoc): number {
452
- // Estimate size based on content
453
- let size = 0;
454
-
455
- if (doc.attributes.body) {
456
- size += doc.attributes.body.length;
457
- }
458
-
459
- if (doc.attributes.title) {
460
- size += doc.attributes.title.length;
461
- }
462
-
463
- // Add base size for metadata
464
- size += 500;
465
-
466
- return Math.max(size, 100); // Minimum 100 bytes
467
- }
468
-
469
- private determineMimeType(doc: CoreDoc): string {
470
- const type = doc.attributes.type?.toLowerCase();
471
- const subtype = doc.attributes.subtype?.toLowerCase();
472
-
473
- // Map Core types to MIME types
474
- if (type === 'document' || subtype === 'document') {
475
- return 'text/plain';
476
- } else if (type === 'html' || subtype === 'html') {
477
- return 'text/html';
478
- } else if (doc.attributes.has_attachments) {
479
- return 'application/octet-stream';
480
- }
481
-
482
- // Default to plain text
483
- return 'text/plain';
484
- }
485
-
486
- private shouldIncludeSource(source: DocumentSource, options?: PluginImportOptions): boolean {
487
- // Apply size filter
488
- if (options?.filters?.maxSize && source.size > options.filters.maxSize) {
489
- return false;
490
- }
491
-
492
- // Apply date range filter
493
- if (options?.filters?.dateRange) {
494
- const { from, to } = options.filters.dateRange;
495
- if (from && source.lastModified < from) return false;
496
- if (to && source.lastModified > to) return false;
497
- }
498
-
499
- // Apply MIME type filter
500
- if (options?.filters?.mimeTypes && !options.filters.mimeTypes.includes(source.mimeType)) {
501
- return false;
502
- }
503
-
504
- return true;
505
- }
506
-
507
- private sleep(ms: number): Promise<void> {
508
- return new Promise(resolve => setTimeout(resolve, ms));
509
- }
510
- }
@@ -1,26 +0,0 @@
1
- {
2
- "name": "core",
3
- "version": "1.0.0",
4
- "description": "Core document management system source plugin",
5
- "author": "HubDoc Tools",
6
- "main": "index.ts",
7
- "dependencies": {
8
- "axios": "^1.5.0",
9
- "fs-extra": "^11.1.0"
10
- },
11
- "hubdocToolVersion": "^1.0.0",
12
- "capabilities": {
13
- "scan": true,
14
- "import": true,
15
- "export": false
16
- },
17
- "apiEndpoints": {
18
- "authentication": "/api/token",
19
- "documents": "/api/d2/docs",
20
- "documentDetail": "/api/d2/docs/{id}"
21
- },
22
- "configuration": {
23
- "required": ["baseUrl", "username", "password"],
24
- "optional": ["bearerToken", "tempDir"]
25
- }
26
- }