@onlineapps/content-resolver 1.1.2 → 1.1.4

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.
Files changed (3) hide show
  1. package/README.md +31 -0
  2. package/package.json +1 -1
  3. package/src/index.js +115 -2
package/README.md CHANGED
@@ -41,6 +41,7 @@ const content = await resolver.resolve('minio://workflow/path/to/file.txt');
41
41
  // Store large content - returns Content Descriptor
42
42
  const descriptor = await resolver.store(largeText, { workflow_id: 'wf-123' }, 'document.html');
43
43
  // → {
44
+ // _descriptor: true,
44
45
  // type: 'file',
45
46
  // storage_ref: 'minio://workflow/...',
46
47
  // filename: 'document.html',
@@ -109,6 +110,30 @@ Create Content Descriptor from raw content. Automatically decides inline vs file
109
110
  #### `normalizeToDescriptor(value, options): Promise<Object>`
110
111
  Normalize any value (string, reference, Buffer) to Content Descriptor.
111
112
 
113
+ #### `createDescriptorFromFile(tempPath, options): Promise<Object>`
114
+ Create Content Descriptor from a temp file. **Used by ApiMapper for file outputs.**
115
+
116
+ | Option | Type | Default | Description |
117
+ |--------|------|---------|-------------|
118
+ | `filename` | string | basename | Output filename |
119
+ | `content_type` | string | auto-detect | MIME type |
120
+ | `context` | Object | {} | `{ workflow_id, step_id }` |
121
+ | `deleteAfterUpload` | boolean | true | Delete temp file after upload |
122
+
123
+ ```javascript
124
+ // Handler returns temp file path
125
+ const handlerOutput = { temp_path: '/tmp/output.pdf', filename: 'report.pdf', content_type: 'application/pdf' };
126
+
127
+ // ApiMapper calls createDescriptorFromFile
128
+ const descriptor = await resolver.createDescriptorFromFile(handlerOutput.temp_path, {
129
+ filename: handlerOutput.filename,
130
+ content_type: handlerOutput.content_type,
131
+ context: { workflow_id: 'wf-123', step_id: 'pdf_gen' },
132
+ deleteAfterUpload: true // Cleanup temp file
133
+ });
134
+ // → Content Descriptor with _descriptor: true, type: 'file', storage_ref, etc.
135
+ ```
136
+
112
137
  #### `resolveInput(input, fields?): Promise<Object>`
113
138
  Resolves all reference fields in input object (legacy method).
114
139
 
@@ -122,6 +147,7 @@ Stores large content fields as Descriptors (returns Descriptors, not plain strin
122
147
  ```javascript
123
148
  // Inline content (small)
124
149
  {
150
+ _descriptor: true,
125
151
  type: 'inline',
126
152
  content: 'actual text content',
127
153
  encoding: 'utf-8',
@@ -133,6 +159,7 @@ Stores large content fields as Descriptors (returns Descriptors, not plain strin
133
159
 
134
160
  // File content (large or binary)
135
161
  {
162
+ _descriptor: true,
136
163
  type: 'file',
137
164
  storage_ref: 'minio://workflow/path/to/file',
138
165
  filename: 'invoice.pdf',
@@ -142,6 +169,10 @@ Stores large content fields as Descriptors (returns Descriptors, not plain strin
142
169
  }
143
170
  ```
144
171
 
172
+ **Descriptor Identification:**
173
+ - `_descriptor: true` - Explicit identifier that value is a Content Descriptor
174
+ - `type: "inline"|"file"` - Distinguishes inline text from file reference
175
+
145
176
  ### Usage Example
146
177
 
147
178
  ```javascript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/content-resolver",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Automatic conversion between text content and storage references with Content Descriptor pattern",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -28,7 +28,8 @@ function isDescriptor(value) {
28
28
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
29
29
  return false;
30
30
  }
31
- return value.type === 'inline' || value.type === 'file';
31
+ // Check explicit identifier first, then fallback to type check for backward compatibility
32
+ return value._descriptor === true || (value.type === 'inline' || value.type === 'file');
32
33
  }
33
34
 
34
35
  /**
@@ -196,6 +197,7 @@ class ContentResolver {
196
197
  async store(content, context = {}, filename = null, content_type = null) {
197
198
  if (!content) {
198
199
  return {
200
+ _descriptor: true,
199
201
  type: 'inline',
200
202
  content: '',
201
203
  encoding: 'utf-8',
@@ -417,6 +419,7 @@ class ContentResolver {
417
419
  const finalContentType = content_type || getContentType(finalFilename, buffer);
418
420
 
419
421
  return {
422
+ _descriptor: true,
420
423
  type: 'file',
421
424
  storage_ref: `minio://workflow/${result.path}`,
422
425
  filename: finalFilename,
@@ -450,6 +453,7 @@ class ContentResolver {
450
453
  const finalFilename = filename || `${result.fingerprint.slice(0, 8)}.${ext}`;
451
454
 
452
455
  return {
456
+ _descriptor: true,
453
457
  type: 'file',
454
458
  storage_ref: `minio://workflow/${result.path}`,
455
459
  filename: finalFilename,
@@ -464,6 +468,7 @@ class ContentResolver {
464
468
  const fingerprint = crypto.createHash('sha256').update(contentString).digest('hex');
465
469
 
466
470
  return {
471
+ _descriptor: true,
467
472
  type: 'inline',
468
473
  content: contentString,
469
474
  encoding: 'utf-8',
@@ -481,8 +486,12 @@ class ContentResolver {
481
486
  * @returns {Promise<Object>} Content Descriptor
482
487
  */
483
488
  async normalizeToDescriptor(value, options = {}) {
484
- // Already a Descriptor
489
+ // Already a Descriptor - ensure it has _descriptor flag
485
490
  if (isDescriptor(value)) {
491
+ // Ensure _descriptor flag is present
492
+ if (value._descriptor !== true) {
493
+ return { ...value, _descriptor: true };
494
+ }
486
495
  return value;
487
496
  }
488
497
 
@@ -502,6 +511,110 @@ class ContentResolver {
502
511
  // Fallback
503
512
  return await this.createDescriptor(String(value), options);
504
513
  }
514
+
515
+ /**
516
+ * Create Content Descriptor from a temp file path
517
+ * Reads file, uploads to MinIO, returns Descriptor, DELETES temp file
518
+ *
519
+ * @param {string} tempPath - Path to temp file
520
+ * @param {Object} options - Options
521
+ * @param {string} options.filename - Output filename
522
+ * @param {string} options.content_type - MIME type
523
+ * @param {Object} options.context - Workflow context { workflow_id, step_id }
524
+ * @param {boolean} options.deleteAfterUpload - Delete temp file after upload (default: true)
525
+ * @returns {Promise<Object>} Content Descriptor
526
+ */
527
+ async createDescriptorFromFile(tempPath, options = {}) {
528
+ const fs = require('fs');
529
+ const path = require('path');
530
+
531
+ const {
532
+ filename,
533
+ content_type,
534
+ context = {},
535
+ deleteAfterUpload = true
536
+ } = options;
537
+
538
+ // Validate temp file exists
539
+ if (!fs.existsSync(tempPath)) {
540
+ throw new Error(`[ContentResolver:FAIL] Temp file not found: ${tempPath}`);
541
+ }
542
+
543
+ // Read file into buffer
544
+ const buffer = fs.readFileSync(tempPath);
545
+ const size = buffer.length;
546
+
547
+ // Determine filename and content_type
548
+ const finalFilename = filename || path.basename(tempPath);
549
+ const finalContentType = content_type || getContentType(finalFilename, buffer);
550
+
551
+ // Log start
552
+ const logContext = {
553
+ workflow_id: context.workflow_id || 'standalone',
554
+ step_id: context.step_id || 'content-resolver'
555
+ };
556
+ console.log(`[${logContext.workflow_id}:${logContext.step_id}:ContentResolver:STORE_START] ${JSON.stringify({
557
+ temp_path: tempPath,
558
+ filename: finalFilename,
559
+ content_type: finalContentType,
560
+ size: size
561
+ })}`);
562
+
563
+ try {
564
+ // Upload to MinIO
565
+ const storage = await this.getStorage();
566
+ const workflowId = context.workflow_id || 'standalone';
567
+ const stepId = context.step_id || 'content';
568
+ const pathPrefix = `outputs/${workflowId}/${stepId}`;
569
+ const ext = finalFilename.split('.').pop() || 'bin';
570
+
571
+ const result = await storage.uploadWithFingerprint(
572
+ 'workflow',
573
+ buffer,
574
+ pathPrefix,
575
+ ext
576
+ );
577
+
578
+ // Build Descriptor
579
+ const descriptor = {
580
+ _descriptor: true,
581
+ type: 'file',
582
+ storage_ref: `minio://workflow/${result.path}`,
583
+ filename: finalFilename,
584
+ content_type: finalContentType,
585
+ size: size,
586
+ fingerprint: result.fingerprint
587
+ };
588
+
589
+ // Delete temp file if requested
590
+ if (deleteAfterUpload) {
591
+ try {
592
+ fs.unlinkSync(tempPath);
593
+ } catch (unlinkErr) {
594
+ console.warn(`[${logContext.workflow_id}:${logContext.step_id}:ContentResolver:WARN] Failed to delete temp file: ${unlinkErr.message}`);
595
+ }
596
+ }
597
+
598
+ // Log success
599
+ console.log(`[${logContext.workflow_id}:${logContext.step_id}:ContentResolver:STORE_SUCCESS] ${JSON.stringify({
600
+ storage_ref: descriptor.storage_ref,
601
+ filename: descriptor.filename,
602
+ size: descriptor.size,
603
+ fingerprint: descriptor.fingerprint.substring(0, 16) + '...'
604
+ })}`);
605
+
606
+ return descriptor;
607
+
608
+ } catch (error) {
609
+ // Log failure
610
+ console.error(`[${logContext.workflow_id}:${logContext.step_id}:ContentResolver:STORE_FAIL] ${JSON.stringify({
611
+ error: error.message,
612
+ temp_path: tempPath,
613
+ filename: finalFilename
614
+ })}`);
615
+ throw error;
616
+ }
617
+ }
505
618
  }
506
619
 
507
620
  // Export class and utilities