@onlineapps/content-resolver 1.0.1
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/README.md +101 -0
- package/package.json +24 -0
- package/src/index.js +269 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @onlineapps/content-resolver
|
|
2
|
+
|
|
3
|
+
Automatic conversion between text content and storage references.
|
|
4
|
+
|
|
5
|
+
## Threshold
|
|
6
|
+
|
|
7
|
+
Default threshold: **16 KB (16384 bytes)**
|
|
8
|
+
|
|
9
|
+
Content larger than threshold is automatically stored in MinIO and replaced with a reference.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic Usage
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
const ContentResolver = require('@onlineapps/content-resolver');
|
|
17
|
+
|
|
18
|
+
const resolver = new ContentResolver({
|
|
19
|
+
threshold: 16 * 1024, // 16KB (default)
|
|
20
|
+
storage: {
|
|
21
|
+
endPoint: 'api_shared_storage',
|
|
22
|
+
port: 9000,
|
|
23
|
+
accessKey: 'minioadmin',
|
|
24
|
+
secretKey: 'minioadmin'
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Resolve reference to content
|
|
29
|
+
const content = await resolver.resolve('minio://workflow/path/to/file.txt');
|
|
30
|
+
// → Returns actual file content
|
|
31
|
+
|
|
32
|
+
// Store large content as reference
|
|
33
|
+
const result = await resolver.store(largeText, { workflow_id: 'wf-123' });
|
|
34
|
+
// → { value: 'minio://workflow/...', stored: true, size: 50000 }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### In Business Service Handler
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
const ContentResolver = require('@onlineapps/content-resolver');
|
|
41
|
+
|
|
42
|
+
exports.processDocument = async (input, context = {}) => {
|
|
43
|
+
const resolver = new ContentResolver();
|
|
44
|
+
|
|
45
|
+
// Input can be either text or reference - resolve transparently
|
|
46
|
+
const resolvedInput = await resolver.resolveInput(input, ['content', 'markdown']);
|
|
47
|
+
|
|
48
|
+
// Now resolvedInput.content and resolvedInput.markdown are always text
|
|
49
|
+
const result = processContent(resolvedInput.content);
|
|
50
|
+
|
|
51
|
+
// Store large output as reference
|
|
52
|
+
const output = await resolver.storeOutput({ result }, context, ['result']);
|
|
53
|
+
|
|
54
|
+
return output;
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
### `new ContentResolver(options)`
|
|
61
|
+
|
|
62
|
+
| Option | Type | Default | Description |
|
|
63
|
+
|--------|------|---------|-------------|
|
|
64
|
+
| `threshold` | number | 16384 | Size threshold in bytes |
|
|
65
|
+
| `storage` | Object | env-based | Storage connector config |
|
|
66
|
+
| `logger` | Object | console | Logger instance |
|
|
67
|
+
|
|
68
|
+
### Methods
|
|
69
|
+
|
|
70
|
+
#### `resolve(value): Promise<string>`
|
|
71
|
+
If value is a reference (`minio://...`), downloads and returns content.
|
|
72
|
+
Otherwise returns value unchanged.
|
|
73
|
+
|
|
74
|
+
#### `store(content, context, filename?): Promise<Object>`
|
|
75
|
+
If content size > threshold, stores in MinIO and returns reference.
|
|
76
|
+
Returns `{ value, stored, size, fingerprint? }`.
|
|
77
|
+
|
|
78
|
+
#### `resolveInput(input, fields?): Promise<Object>`
|
|
79
|
+
Resolves all reference fields in input object.
|
|
80
|
+
|
|
81
|
+
#### `storeOutput(output, context, fields?): Promise<Object>`
|
|
82
|
+
Stores large content fields as references.
|
|
83
|
+
|
|
84
|
+
### Utilities
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
const { isReference, parseReference, DEFAULT_THRESHOLD } = require('@onlineapps/content-resolver');
|
|
88
|
+
|
|
89
|
+
isReference('minio://bucket/path'); // true
|
|
90
|
+
isReference('plain text'); // false
|
|
91
|
+
|
|
92
|
+
parseReference('minio://workflow/content/file.txt');
|
|
93
|
+
// → { bucket: 'workflow', path: 'content/file.txt' }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Reference Formats
|
|
97
|
+
|
|
98
|
+
Supported formats:
|
|
99
|
+
- `minio://bucket/path/to/file`
|
|
100
|
+
- `internal://storage/path/to/file` (uses default bucket)
|
|
101
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onlineapps/content-resolver",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Automatic conversion between text content and storage references",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"content",
|
|
11
|
+
"storage",
|
|
12
|
+
"minio",
|
|
13
|
+
"reference",
|
|
14
|
+
"resolver"
|
|
15
|
+
],
|
|
16
|
+
"author": "OnlineApps",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@onlineapps/conn-base-storage": "^1.0.0"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@onlineapps/conn-base-storage": "^1.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContentResolver - Automatic conversion between text content and storage references
|
|
3
|
+
*
|
|
4
|
+
* Handles transparent conversion:
|
|
5
|
+
* - Large text → stored as file, returns reference
|
|
6
|
+
* - Reference → downloads content, returns text
|
|
7
|
+
*
|
|
8
|
+
* Used by business services to handle both inline content and file references uniformly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const StorageConnector = require('@onlineapps/conn-base-storage');
|
|
12
|
+
|
|
13
|
+
// Default threshold: 16KB (16384 bytes)
|
|
14
|
+
const DEFAULT_THRESHOLD = 16 * 1024;
|
|
15
|
+
|
|
16
|
+
// Reference patterns
|
|
17
|
+
const MINIO_REF_PATTERN = /^minio:\/\/([^/]+)\/(.+)$/;
|
|
18
|
+
const INTERNAL_REF_PATTERN = /^internal:\/\/storage\/(.+)$/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a value is a storage reference
|
|
22
|
+
* @param {string} value - Value to check
|
|
23
|
+
* @returns {boolean} True if value is a storage reference
|
|
24
|
+
*/
|
|
25
|
+
function isReference(value) {
|
|
26
|
+
if (typeof value !== 'string') return false;
|
|
27
|
+
return MINIO_REF_PATTERN.test(value) || INTERNAL_REF_PATTERN.test(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a storage reference into bucket and path
|
|
32
|
+
* @param {string} ref - Storage reference (minio://bucket/path or internal://storage/path)
|
|
33
|
+
* @returns {{ bucket: string, path: string } | null}
|
|
34
|
+
*/
|
|
35
|
+
function parseReference(ref) {
|
|
36
|
+
const minioMatch = ref.match(MINIO_REF_PATTERN);
|
|
37
|
+
if (minioMatch) {
|
|
38
|
+
return { bucket: minioMatch[1], path: minioMatch[2] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const internalMatch = ref.match(INTERNAL_REF_PATTERN);
|
|
42
|
+
if (internalMatch) {
|
|
43
|
+
// Internal references use default bucket
|
|
44
|
+
return { bucket: 'workflow', path: internalMatch[1] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* ContentResolver class
|
|
52
|
+
*/
|
|
53
|
+
class ContentResolver {
|
|
54
|
+
/**
|
|
55
|
+
* Create a ContentResolver
|
|
56
|
+
* @param {Object} options - Configuration options
|
|
57
|
+
* @param {number} [options.threshold=16384] - Size threshold in bytes (default 16KB)
|
|
58
|
+
* @param {Object} [options.storage] - Storage connector config or instance
|
|
59
|
+
* @param {Object} [options.logger] - Logger instance
|
|
60
|
+
*/
|
|
61
|
+
constructor(options = {}) {
|
|
62
|
+
this.threshold = options.threshold || DEFAULT_THRESHOLD;
|
|
63
|
+
this.logger = options.logger || console;
|
|
64
|
+
|
|
65
|
+
// Storage can be passed as instance or config
|
|
66
|
+
if (options.storage instanceof StorageConnector) {
|
|
67
|
+
this.storage = options.storage;
|
|
68
|
+
} else {
|
|
69
|
+
this.storageConfig = options.storage || {
|
|
70
|
+
endPoint: process.env.MINIO_HOST || 'api_shared_storage',
|
|
71
|
+
port: parseInt(process.env.MINIO_PORT || '9000'),
|
|
72
|
+
useSSL: false,
|
|
73
|
+
accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
|
|
74
|
+
secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
|
|
75
|
+
defaultBucket: 'workflow'
|
|
76
|
+
};
|
|
77
|
+
this.storage = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get or create storage connector
|
|
83
|
+
* @returns {Promise<StorageConnector>}
|
|
84
|
+
*/
|
|
85
|
+
async getStorage() {
|
|
86
|
+
if (!this.storage) {
|
|
87
|
+
// Remove protocol from endpoint if present
|
|
88
|
+
const config = { ...this.storageConfig };
|
|
89
|
+
if (config.endPoint) {
|
|
90
|
+
config.endPoint = config.endPoint.replace(/^https?:\/\//, '');
|
|
91
|
+
}
|
|
92
|
+
this.storage = new StorageConnector(config);
|
|
93
|
+
await this.storage.initialize();
|
|
94
|
+
}
|
|
95
|
+
return this.storage;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve a value - if it's a reference, download content; if it's text, return as-is
|
|
100
|
+
* @param {string} value - Text content or storage reference
|
|
101
|
+
* @returns {Promise<string>} Resolved text content
|
|
102
|
+
*/
|
|
103
|
+
async resolve(value) {
|
|
104
|
+
if (!value || typeof value !== 'string') {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check if it's a reference
|
|
109
|
+
if (!isReference(value)) {
|
|
110
|
+
return value; // Already text content
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Parse reference
|
|
114
|
+
const parsed = parseReference(value);
|
|
115
|
+
if (!parsed) {
|
|
116
|
+
this.logger.warn(`[ContentResolver] Invalid reference format: ${value}`);
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Download content
|
|
121
|
+
try {
|
|
122
|
+
const storage = await this.getStorage();
|
|
123
|
+
const stream = await storage.client.getObject(parsed.bucket, parsed.path);
|
|
124
|
+
|
|
125
|
+
const chunks = [];
|
|
126
|
+
for await (const chunk of stream) {
|
|
127
|
+
chunks.push(chunk);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const content = Buffer.concat(chunks).toString('utf-8');
|
|
131
|
+
this.logger.debug?.(`[ContentResolver] Downloaded ${content.length} bytes from ${value}`);
|
|
132
|
+
|
|
133
|
+
return content;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.logger.error(`[ContentResolver] Failed to download ${value}: ${error.message}`);
|
|
136
|
+
throw new Error(`Failed to resolve content reference: ${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Store content if above threshold, otherwise return as-is
|
|
142
|
+
* @param {string} content - Text content to potentially store
|
|
143
|
+
* @param {Object} context - Workflow context for path generation
|
|
144
|
+
* @param {string} [context.workflow_id] - Workflow ID
|
|
145
|
+
* @param {string} [context.step_id] - Step ID
|
|
146
|
+
* @param {string} [filename] - Optional filename hint
|
|
147
|
+
* @returns {Promise<{ value: string, stored: boolean, size: number }>}
|
|
148
|
+
*/
|
|
149
|
+
async store(content, context = {}, filename = null) {
|
|
150
|
+
if (!content || typeof content !== 'string') {
|
|
151
|
+
return { value: content, stored: false, size: 0 };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const size = Buffer.byteLength(content, 'utf-8');
|
|
155
|
+
|
|
156
|
+
// Below threshold - return as-is
|
|
157
|
+
if (size <= this.threshold) {
|
|
158
|
+
return { value: content, stored: false, size };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Above threshold - store in MinIO
|
|
162
|
+
try {
|
|
163
|
+
const storage = await this.getStorage();
|
|
164
|
+
|
|
165
|
+
const workflowId = context.workflow_id || 'standalone';
|
|
166
|
+
const stepId = context.step_id || 'content';
|
|
167
|
+
const pathPrefix = `content/${workflowId}/${stepId}`;
|
|
168
|
+
|
|
169
|
+
const buffer = Buffer.from(content, 'utf-8');
|
|
170
|
+
const result = await storage.uploadWithFingerprint(
|
|
171
|
+
'workflow',
|
|
172
|
+
buffer,
|
|
173
|
+
pathPrefix,
|
|
174
|
+
filename ? filename.split('.').pop() : 'txt'
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const ref = `minio://workflow/${result.path}`;
|
|
178
|
+
this.logger.debug?.(`[ContentResolver] Stored ${size} bytes as ${ref}`);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
value: ref,
|
|
182
|
+
stored: true,
|
|
183
|
+
size,
|
|
184
|
+
fingerprint: result.fingerprint
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.logger.error(`[ContentResolver] Failed to store content: ${error.message}`);
|
|
188
|
+
// Fallback: return original content if storage fails
|
|
189
|
+
this.logger.warn('[ContentResolver] Falling back to inline content');
|
|
190
|
+
return { value: content, stored: false, size, warning: error.message };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Process an input object - resolve all string fields that are references
|
|
196
|
+
* @param {Object} input - Input object with potential references
|
|
197
|
+
* @param {string[]} [fields] - Specific fields to resolve (default: all string fields)
|
|
198
|
+
* @returns {Promise<Object>} Input with resolved content
|
|
199
|
+
*/
|
|
200
|
+
async resolveInput(input, fields = null) {
|
|
201
|
+
if (!input || typeof input !== 'object') {
|
|
202
|
+
return input;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = { ...input };
|
|
206
|
+
const fieldsToResolve = fields || Object.keys(input);
|
|
207
|
+
|
|
208
|
+
for (const field of fieldsToResolve) {
|
|
209
|
+
if (typeof result[field] === 'string' && isReference(result[field])) {
|
|
210
|
+
result[field] = await this.resolve(result[field]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Process an output object - store large string fields as references
|
|
219
|
+
* @param {Object} output - Output object with potential large content
|
|
220
|
+
* @param {Object} context - Workflow context
|
|
221
|
+
* @param {string[]} [fields] - Specific fields to process (default: all string fields)
|
|
222
|
+
* @returns {Promise<Object>} Output with large content stored as references
|
|
223
|
+
*/
|
|
224
|
+
async storeOutput(output, context = {}, fields = null) {
|
|
225
|
+
if (!output || typeof output !== 'object') {
|
|
226
|
+
return output;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const result = { ...output };
|
|
230
|
+
const fieldsToProcess = fields || Object.keys(output);
|
|
231
|
+
|
|
232
|
+
for (const field of fieldsToProcess) {
|
|
233
|
+
if (typeof result[field] === 'string') {
|
|
234
|
+
const stored = await this.store(result[field], context, field);
|
|
235
|
+
result[field] = stored.value;
|
|
236
|
+
if (stored.stored) {
|
|
237
|
+
result[`${field}_stored`] = true;
|
|
238
|
+
result[`${field}_size`] = stored.size;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get current threshold
|
|
248
|
+
* @returns {number} Threshold in bytes
|
|
249
|
+
*/
|
|
250
|
+
getThreshold() {
|
|
251
|
+
return this.threshold;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Set threshold
|
|
256
|
+
* @param {number} bytes - New threshold in bytes
|
|
257
|
+
*/
|
|
258
|
+
setThreshold(bytes) {
|
|
259
|
+
this.threshold = bytes;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Export class and utilities
|
|
264
|
+
module.exports = ContentResolver;
|
|
265
|
+
module.exports.ContentResolver = ContentResolver;
|
|
266
|
+
module.exports.isReference = isReference;
|
|
267
|
+
module.exports.parseReference = parseReference;
|
|
268
|
+
module.exports.DEFAULT_THRESHOLD = DEFAULT_THRESHOLD;
|
|
269
|
+
|