@onlineapps/conn-base-storage 1.0.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.
Files changed (39) hide show
  1. package/API.md +618 -0
  2. package/README.md +341 -0
  3. package/SHARED_URL_ADDRESSING.md +258 -0
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/clover.xml +213 -0
  7. package/coverage/coverage-final.json +3 -0
  8. package/coverage/favicon.png +0 -0
  9. package/coverage/index.html +131 -0
  10. package/coverage/index.js.html +1579 -0
  11. package/coverage/internal-url-adapter.js.html +604 -0
  12. package/coverage/lcov-report/base.css +224 -0
  13. package/coverage/lcov-report/block-navigation.js +87 -0
  14. package/coverage/lcov-report/favicon.png +0 -0
  15. package/coverage/lcov-report/index.html +131 -0
  16. package/coverage/lcov-report/index.js.html +1579 -0
  17. package/coverage/lcov-report/internal-url-adapter.js.html +604 -0
  18. package/coverage/lcov-report/prettify.css +1 -0
  19. package/coverage/lcov-report/prettify.js +2 -0
  20. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  21. package/coverage/lcov-report/sorter.js +210 -0
  22. package/coverage/lcov.info +434 -0
  23. package/coverage/prettify.css +1 -0
  24. package/coverage/prettify.js +2 -0
  25. package/coverage/sort-arrow-sprite.png +0 -0
  26. package/coverage/sorter.js +210 -0
  27. package/jest.config.js +13 -0
  28. package/jest.integration.config.js +9 -0
  29. package/package.json +33 -0
  30. package/src/index.js +853 -0
  31. package/src/internal-url-adapter.js +174 -0
  32. package/src/sharedUrlAdapter.js +258 -0
  33. package/test/component/storage.component.test.js +363 -0
  34. package/test/integration/setup.js +3 -0
  35. package/test/integration/storage.integration.test.js +224 -0
  36. package/test/unit/internal-url-adapter.test.js +211 -0
  37. package/test/unit/legacy.storage.test.js.bak +614 -0
  38. package/test/unit/storage.extended.unit.test.js +435 -0
  39. package/test/unit/storage.unit.test.js +373 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Internal URL Adapter
3
+ * Převádí abstraktní internal:// URL na skutečné endpointy
4
+ * Skrývá implementační detaily před službami
5
+ */
6
+
7
+ class InternalUrlAdapter {
8
+ constructor(config = {}) {
9
+ this.environment = config.environment || process.env.NODE_ENV || 'development';
10
+ this.dockerNetwork = config.dockerNetwork || process.env.DOCKER_NETWORK || false;
11
+
12
+ // Mapování služeb na jejich interní adresy
13
+ this.serviceMap = {
14
+ storage: {
15
+ docker: 'api_services_storage:9000',
16
+ local: 'localhost:9000'
17
+ },
18
+ registry: {
19
+ docker: 'api_registry:8080',
20
+ local: 'localhost:8080'
21
+ },
22
+ monitoring: {
23
+ docker: 'api_monitoring:3000',
24
+ local: 'localhost:3000'
25
+ },
26
+ mq: {
27
+ docker: 'api_services_rabbit:5672',
28
+ local: 'localhost:5672'
29
+ },
30
+ cache: {
31
+ docker: 'api_node_cache:6379',
32
+ local: 'localhost:6379'
33
+ }
34
+ };
35
+
36
+ // Rozšiřitelné přes config
37
+ if (config.services) {
38
+ Object.assign(this.serviceMap, config.services);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Převede internal:// URL na skutečnou adresu
44
+ * @param {string} internalUrl - URL ve formátu internal://service/path
45
+ * @returns {string} Skutečná HTTP/HTTPS URL
46
+ */
47
+ resolve(internalUrl) {
48
+ if (!internalUrl || !internalUrl.startsWith('internal://')) {
49
+ return internalUrl; // Není internal URL, vrátíme jak je
50
+ }
51
+
52
+ // Parsování internal://service/path
53
+ const urlParts = internalUrl.replace('internal://', '').split('/');
54
+ const serviceName = urlParts[0];
55
+ const path = urlParts.slice(1).join('/');
56
+
57
+ // Najdeme mapování pro službu
58
+ const serviceConfig = this.serviceMap[serviceName];
59
+ if (!serviceConfig) {
60
+ throw new Error(`Unknown service: ${serviceName}`);
61
+ }
62
+
63
+ // Určíme endpoint podle prostředí
64
+ const isDocker = this.dockerNetwork || this.isRunningInDocker();
65
+ const endpoint = isDocker ? serviceConfig.docker : serviceConfig.local;
66
+
67
+ // Sestavíme skutečnou URL
68
+ return `http://${endpoint}/${path}`;
69
+ }
70
+
71
+ /**
72
+ * Převede skutečnou URL zpět na internal://
73
+ * @param {string} actualUrl - Skutečná HTTP URL
74
+ * @param {string} serviceName - Název služby
75
+ * @returns {string} Internal URL
76
+ */
77
+ toInternal(actualUrl, serviceName) {
78
+ if (!actualUrl || !serviceName) {
79
+ return actualUrl;
80
+ }
81
+
82
+ // Odstranit protokol a host
83
+ const url = new URL(actualUrl);
84
+ const path = url.pathname + url.search + url.hash;
85
+
86
+ return `internal://${serviceName}${path}`;
87
+ }
88
+
89
+ /**
90
+ * Detekce Docker prostředí
91
+ */
92
+ isRunningInDocker() {
93
+ // Několik způsobů detekce Docker kontejneru
94
+ return !!(
95
+ process.env.DOCKER_ENV ||
96
+ process.env.DOCKER_CONTAINER ||
97
+ (require('fs').existsSync('/.dockerenv')) ||
98
+ (process.env.HOSTNAME && process.env.HOSTNAME.length === 12) // Docker container ID
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Middleware pro Express - automaticky resolvuje internal:// URL v odpovědích
104
+ */
105
+ expressMiddleware() {
106
+ return (req, res, next) => {
107
+ const originalJson = res.json;
108
+
109
+ res.json = (data) => {
110
+ // Rekurzivně projít response a nahradit internal:// URL
111
+ const transformed = this.transformResponse(data);
112
+ return originalJson.call(res, transformed);
113
+ };
114
+
115
+ next();
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Rekurzivní transformace odpovědi
121
+ */
122
+ transformResponse(obj) {
123
+ if (typeof obj === 'string' && obj.startsWith('internal://')) {
124
+ return this.resolve(obj);
125
+ }
126
+
127
+ if (Array.isArray(obj)) {
128
+ return obj.map(item => this.transformResponse(item));
129
+ }
130
+
131
+ if (obj && typeof obj === 'object') {
132
+ const transformed = {};
133
+ for (const [key, value] of Object.entries(obj)) {
134
+ transformed[key] = this.transformResponse(value);
135
+ }
136
+ return transformed;
137
+ }
138
+
139
+ return obj;
140
+ }
141
+
142
+ /**
143
+ * HTTP client interceptor - automaticky resolvuje internal:// URL v požadavcích
144
+ */
145
+ axiosInterceptor(axios) {
146
+ axios.interceptors.request.use((config) => {
147
+ if (config.url && config.url.startsWith('internal://')) {
148
+ config.url = this.resolve(config.url);
149
+ }
150
+ return config;
151
+ });
152
+
153
+ return axios;
154
+ }
155
+
156
+ /**
157
+ * Získat všechny dostupné služby
158
+ */
159
+ getAvailableServices() {
160
+ return Object.keys(this.serviceMap);
161
+ }
162
+
163
+ /**
164
+ * Přidat nebo aktualizovat mapování služby
165
+ */
166
+ registerService(name, dockerEndpoint, localEndpoint) {
167
+ this.serviceMap[name] = {
168
+ docker: dockerEndpoint,
169
+ local: localEndpoint
170
+ };
171
+ }
172
+ }
173
+
174
+ module.exports = InternalUrlAdapter;
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Shared URL Adapter
3
+ *
4
+ * Provides unified addressing for backend and frontend using shared:// protocol
5
+ *
6
+ * Examples:
7
+ * - shared://registry/service-name/v1.0.0/spec.json
8
+ * - shared://workflow/cookbook-123/step-1.json
9
+ * - shared://storage/files/document.pdf
10
+ *
11
+ * Maps to MinIO buckets:
12
+ * - registry -> registry bucket
13
+ * - workflow -> workflow bucket
14
+ * - storage -> default storage bucket
15
+ */
16
+
17
+ class SharedUrlAdapter {
18
+ constructor(storageConnector) {
19
+ this.storage = storageConnector;
20
+
21
+ // Bucket mapping
22
+ this.bucketMap = {
23
+ 'registry': 'registry',
24
+ 'workflow': 'workflow',
25
+ 'storage': 'api-storage',
26
+ 'cache': 'cache',
27
+ 'logs': 'logs'
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Parse shared:// URL into components
33
+ * @param {string} sharedUrl - URL in format shared://bucket/path
34
+ * @returns {Object} { bucket, path, protocol }
35
+ */
36
+ parseSharedUrl(sharedUrl) {
37
+ if (!sharedUrl.startsWith('shared://')) {
38
+ throw new Error(`Invalid shared URL format. Must start with 'shared://'. Got: ${sharedUrl}`);
39
+ }
40
+
41
+ const urlPart = sharedUrl.substring(9); // Remove 'shared://'
42
+ const firstSlash = urlPart.indexOf('/');
43
+
44
+ if (firstSlash === -1) {
45
+ throw new Error(`Invalid shared URL format. Missing path. Got: ${sharedUrl}`);
46
+ }
47
+
48
+ const namespace = urlPart.substring(0, firstSlash);
49
+ const path = urlPart.substring(firstSlash + 1);
50
+
51
+ const bucket = this.bucketMap[namespace];
52
+ if (!bucket) {
53
+ throw new Error(`Unknown namespace '${namespace}'. Available: ${Object.keys(this.bucketMap).join(', ')}`);
54
+ }
55
+
56
+ return {
57
+ protocol: 'shared',
58
+ namespace,
59
+ bucket,
60
+ path,
61
+ originalUrl: sharedUrl
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Convert shared:// URL to MinIO internal URL
67
+ * @param {string} sharedUrl
68
+ * @returns {string} MinIO URL
69
+ */
70
+ toMinioUrl(sharedUrl) {
71
+ const { bucket, path } = this.parseSharedUrl(sharedUrl);
72
+ return `s3://${bucket}/${path}`;
73
+ }
74
+
75
+ /**
76
+ * Convert shared:// URL to HTTP URL for external access
77
+ * @param {string} sharedUrl
78
+ * @returns {Promise<string>} Presigned HTTP URL
79
+ */
80
+ async toHttpUrl(sharedUrl, expiry = 3600) {
81
+ const { bucket, path } = this.parseSharedUrl(sharedUrl);
82
+
83
+ if (!this.storage) {
84
+ throw new Error('Storage connector not initialized');
85
+ }
86
+
87
+ return await this.storage.getPresignedUrl(bucket, path, expiry);
88
+ }
89
+
90
+ /**
91
+ * Upload content to shared:// URL
92
+ * @param {string} sharedUrl - Target URL
93
+ * @param {string|Buffer} content - Content to upload
94
+ * @returns {Promise<Object>} Upload result with fingerprint
95
+ */
96
+ async upload(sharedUrl, content) {
97
+ const { bucket, path } = this.parseSharedUrl(sharedUrl);
98
+
99
+ if (!this.storage) {
100
+ throw new Error('Storage connector not initialized');
101
+ }
102
+
103
+ // Ensure bucket exists
104
+ await this.storage.ensureBucket(bucket);
105
+
106
+ // For shared URLs, we want to use the exact path specified
107
+ // So we upload to the specific location instead of using fingerprinted names
108
+ const fingerprint = this.storage.generateFingerprint(content);
109
+
110
+ // Prepare content for upload
111
+ const data = Buffer.isBuffer(content) ? content : Buffer.from(content);
112
+
113
+ await this.storage.client.putObject(bucket, path, data, data.length, {
114
+ 'Content-Type': path.endsWith('.json') ? 'application/json' : 'application/octet-stream',
115
+ 'x-amz-meta-fingerprint': fingerprint
116
+ });
117
+
118
+ this.storage.logger.info(`Uploaded to shared://${bucket}/${path}`, {
119
+ bucket,
120
+ path,
121
+ fingerprint,
122
+ size: data.length
123
+ });
124
+
125
+ return {
126
+ bucket,
127
+ path,
128
+ fingerprint,
129
+ size: data.length,
130
+ sharedUrl
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Download content from shared:// URL
136
+ * @param {string} sharedUrl - Source URL
137
+ * @param {string} [expectedFingerprint] - Optional fingerprint for verification
138
+ * @returns {Promise<string>} Downloaded content
139
+ */
140
+ async download(sharedUrl, expectedFingerprint = null) {
141
+ const { bucket, path } = this.parseSharedUrl(sharedUrl);
142
+
143
+ if (!this.storage) {
144
+ throw new Error('Storage connector not initialized');
145
+ }
146
+
147
+ if (expectedFingerprint) {
148
+ return await this.storage.downloadWithVerification(bucket, path, expectedFingerprint);
149
+ }
150
+
151
+ // Direct download without verification
152
+ const stream = await this.storage.client.getObject(bucket, path);
153
+ const chunks = [];
154
+
155
+ return new Promise((resolve, reject) => {
156
+ stream.on('data', chunk => chunks.push(chunk));
157
+ stream.on('error', reject);
158
+ stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
159
+ });
160
+ }
161
+
162
+ /**
163
+ * List objects under shared:// path
164
+ * @param {string} sharedUrl - Base URL to list
165
+ * @returns {Promise<Array>} List of objects
166
+ */
167
+ async list(sharedUrl) {
168
+ const { bucket, path } = this.parseSharedUrl(sharedUrl);
169
+
170
+ if (!this.storage) {
171
+ throw new Error('Storage connector not initialized');
172
+ }
173
+
174
+ return await this.storage.listWithFingerprints(bucket, path);
175
+ }
176
+
177
+ /**
178
+ * Delete object at shared:// URL
179
+ * @param {string} sharedUrl - URL to delete
180
+ * @returns {Promise<boolean>} Success status
181
+ */
182
+ async delete(sharedUrl) {
183
+ const { bucket, path } = this.parseSharedUrl(sharedUrl);
184
+
185
+ if (!this.storage) {
186
+ throw new Error('Storage connector not initialized');
187
+ }
188
+
189
+ return await this.storage.deleteObject(bucket, path);
190
+ }
191
+
192
+ /**
193
+ * Check if shared:// URL exists
194
+ * @param {string} sharedUrl
195
+ * @returns {Promise<boolean>}
196
+ */
197
+ async exists(sharedUrl) {
198
+ const { bucket, path } = this.parseSharedUrl(sharedUrl);
199
+
200
+ if (!this.storage) {
201
+ throw new Error('Storage connector not initialized');
202
+ }
203
+
204
+ try {
205
+ await this.storage.client.statObject(bucket, path);
206
+ return true;
207
+ } catch (error) {
208
+ if (error.code === 'NotFound') {
209
+ return false;
210
+ }
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Create a shared:// URL from components
217
+ * @param {string} namespace - Namespace (registry, workflow, storage)
218
+ * @param {string} path - Path within namespace
219
+ * @returns {string} Shared URL
220
+ */
221
+ createSharedUrl(namespace, path) {
222
+ if (!this.bucketMap[namespace]) {
223
+ throw new Error(`Unknown namespace '${namespace}'. Available: ${Object.keys(this.bucketMap).join(', ')}`);
224
+ }
225
+
226
+ // Ensure path doesn't start with /
227
+ const cleanPath = path.startsWith('/') ? path.substring(1) : path;
228
+
229
+ return `shared://${namespace}/${cleanPath}`;
230
+ }
231
+
232
+ /**
233
+ * Get metadata for shared:// URL
234
+ * @param {string} sharedUrl
235
+ * @returns {Promise<Object>} Object metadata
236
+ */
237
+ async getMetadata(sharedUrl) {
238
+ const { bucket, path } = this.parseSharedUrl(sharedUrl);
239
+
240
+ if (!this.storage) {
241
+ throw new Error('Storage connector not initialized');
242
+ }
243
+
244
+ const stat = await this.storage.client.statObject(bucket, path);
245
+
246
+ return {
247
+ size: stat.size,
248
+ etag: stat.etag,
249
+ lastModified: stat.lastModified,
250
+ metadata: stat.metaData,
251
+ sharedUrl,
252
+ bucket,
253
+ path
254
+ };
255
+ }
256
+ }
257
+
258
+ module.exports = SharedUrlAdapter;