@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.
Files changed (3) hide show
  1. package/README.md +101 -0
  2. package/package.json +24 -0
  3. 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
+