s3db.js 8.2.0 → 9.1.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.
package/package.json CHANGED
@@ -1,13 +1,10 @@
1
1
  {
2
2
  "name": "s3db.js",
3
- "version": "8.2.0",
3
+ "version": "9.1.0",
4
4
  "description": "Use AWS S3, the world's most reliable document storage, as a database with this ORM.",
5
5
  "main": "dist/s3db.cjs.js",
6
6
  "module": "dist/s3db.es.js",
7
- "browser": "dist/s3db.iife.js",
8
7
  "types": "dist/s3db.d.ts",
9
- "unpkg": "dist/s3db.iife.min.js",
10
- "jsdelivr": "dist/s3db.iife.min.js",
11
8
  "author": "@stone/martech",
12
9
  "license": "UNLICENSED",
13
10
  "bin": {
@@ -26,7 +23,14 @@
26
23
  "keywords": [
27
24
  "s3",
28
25
  "aws",
29
- "database"
26
+ "database",
27
+ "orm",
28
+ "nosql",
29
+ "document-store",
30
+ "cloud-database",
31
+ "metadata-encoding",
32
+ "s3-database",
33
+ "serverless"
30
34
  ],
31
35
  "type": "module",
32
36
  "sideEffects": false,
@@ -39,24 +43,25 @@
39
43
  },
40
44
  "exports": {
41
45
  ".": {
46
+ "types": "./dist/s3db.d.ts",
42
47
  "import": "./dist/s3db.es.js",
43
- "require": "./dist/s3db.cjs.js",
44
- "types": "./dist/s3db.d.ts"
48
+ "require": "./dist/s3db.cjs.js"
45
49
  }
46
50
  },
47
51
  "files": [
48
- "dist",
49
- "src",
52
+ "dist/",
53
+ "src/",
54
+ "bin/cli.js",
55
+ "mcp/server.js",
50
56
  "README.md",
51
57
  "PLUGINS.md",
52
58
  "UNLICENSE"
53
59
  ],
54
60
  "dependencies": {
55
- "@aws-sdk/client-s3": "^3.850.0",
61
+ "@aws-sdk/client-s3": "^3.864.0",
56
62
  "@modelcontextprotocol/sdk": "^1.17.3",
57
- "@smithy/node-http-handler": "^4.1.0",
63
+ "@smithy/node-http-handler": "^4.1.1",
58
64
  "@supercharge/promise-pool": "^3.2.0",
59
- "commander": "^12.1.0",
60
65
  "dotenv": "^17.2.1",
61
66
  "fastest-validator": "^1.19.1",
62
67
  "flat": "^6.0.1",
@@ -89,43 +94,54 @@
89
94
  }
90
95
  },
91
96
  "devDependencies": {
97
+ "@babel/core": "^7.28.3",
98
+ "@babel/preset-env": "^7.28.3",
92
99
  "@rollup/plugin-commonjs": "^28.0.6",
93
100
  "@rollup/plugin-json": "^6.1.0",
94
101
  "@rollup/plugin-node-resolve": "^16.0.1",
95
102
  "@rollup/plugin-replace": "^6.0.2",
96
103
  "@rollup/plugin-terser": "^0.4.4",
97
- "@types/node": "24.1.0",
104
+ "@types/node": "24.3.0",
105
+ "babel-loader": "^10.0.0",
106
+ "chalk": "^5.5.0",
107
+ "cli-table3": "^0.6.5",
108
+ "commander": "^14.0.0",
109
+ "esbuild": "^0.25.9",
110
+ "inquirer": "^12.9.2",
98
111
  "jest": "^30.0.5",
99
- "rollup": "^4.46.1",
112
+ "node-loader": "^2.1.0",
113
+ "ora": "^8.2.0",
114
+ "pkg": "^5.8.1",
115
+ "rollup": "^4.46.2",
100
116
  "rollup-plugin-copy": "^3.5.0",
101
117
  "rollup-plugin-esbuild": "^6.2.1",
102
118
  "rollup-plugin-polyfill-node": "^0.13.0",
119
+ "rollup-plugin-shebang-bin": "^0.1.0",
103
120
  "rollup-plugin-terser": "^7.0.2",
104
- "typescript": "5.8.3"
121
+ "typescript": "5.9.2",
122
+ "webpack": "^5.101.2",
123
+ "webpack-cli": "^6.0.1"
105
124
  },
106
125
  "funding": [
107
126
  "https://github.com/sponsors/forattini-dev"
108
127
  ],
109
128
  "scripts": {
110
129
  "build": "rollup -c",
130
+ "build:cli": "rollup -c rollup.cli.config.mjs",
131
+ "build:binaries": "./scripts/scripts/build-binaries.sh",
111
132
  "dev": "rollup -c -w",
112
- "test": "npm run test:js && npm run test:ts",
133
+ "test": "pnpm run test:js && pnpm run test:ts",
113
134
  "test:js": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --testTimeout=10000",
114
135
  "test:ts": "tsc --noEmit --project tests/typescript/tsconfig.json",
115
- "test:js-converage": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles --coverage --runInBand",
116
- "test:js-ai": "node --max-old-space-size=4096 --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathIgnorePatterns=\"plugin-audit.test.js|tests/typescript/\"",
117
- "test:full": "npm run test:js-ai && npm run test:ts",
118
- "test:audit": "node --max-old-space-size=8192 --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/plugin-audit.test.js --runInBand --testTimeout=60000",
119
- "test:cache": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/plugin-cache*.test.js --runInBand",
136
+ "test:coverage": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles --coverage --runInBand",
120
137
  "test:quick": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testTimeout=10000",
121
- "test:batch": "./test-batch.sh",
122
138
  "test:plugins": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/ --runInBand --testTimeout=60000",
123
- "test:plugins:fast": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/ --runInBand --testTimeout=15000 --testPathIgnorePatterns='plugin-audit.test.js|plugin-replicator-s3db.test.js|plugin-fulltext.test.js'",
124
- "test:slow": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/plugin-audit.test.js tests/plugins/plugin-replicator-s3db.test.js tests/plugins/plugin-fulltext.test.js --runInBand --testTimeout=120000",
125
- "test:types": "tsc --noEmit --project tests/typescript/tsconfig.json",
126
- "test:types:basic": "tsc --noEmit tests/typescript/basic-usage.test.ts",
127
- "test:types:direct": "tsc --noEmit tests/typescript/direct-type-test.ts",
128
- "test:types:watch": "tsc --noEmit --watch --project tests/typescript/tsconfig.json",
129
- "validate:types": "npm run test:types && echo 'TypeScript definitions are valid!'"
139
+ "test:full": "pnpm run test:js && pnpm run test:ts",
140
+ "benchmark": "node benchmark-compression.js",
141
+ "version": "echo 'Use pnpm run release v<version> instead of npm version'",
142
+ "release:check": "./scripts/pre-release-check.sh",
143
+ "release:prepare": "pnpm run build:binaries && echo 'Binaries ready for GitHub release'",
144
+ "release": "./scripts/release.sh",
145
+ "validate:types": "pnpm run test:ts && echo 'TypeScript definitions are valid!'"
130
146
  }
131
147
  }
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import Table from 'cli-table3';
7
+ import inquirer from 'inquirer';
8
+ import { S3db } from '../database.class.js';
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import os from 'os';
12
+
13
+ const program = new Command();
14
+ const configPath = path.join(os.homedir(), '.s3db', 'config.json');
15
+
16
+ // Helper to load config
17
+ async function loadConfig() {
18
+ try {
19
+ const data = await fs.readFile(configPath, 'utf-8');
20
+ return JSON.parse(data);
21
+ } catch {
22
+ return {};
23
+ }
24
+ }
25
+
26
+ // Helper to save config
27
+ async function saveConfig(config) {
28
+ const dir = path.dirname(configPath);
29
+ await fs.mkdir(dir, { recursive: true });
30
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
31
+ }
32
+
33
+ // Connect to database
34
+ async function getDatabase(options) {
35
+ const config = await loadConfig();
36
+ const connectionString = options.connection || config.connection || process.env.S3DB_CONNECTION;
37
+
38
+ if (!connectionString) {
39
+ console.error(chalk.red('No connection string provided. Use --connection or s3db configure'));
40
+ process.exit(1);
41
+ }
42
+
43
+ return new S3db({ connectionString });
44
+ }
45
+
46
+ program
47
+ .name('s3db')
48
+ .description('S3DB CLI - Transform AWS S3 into a powerful document database')
49
+ .version('9.0.0');
50
+
51
+ // Configure command
52
+ program
53
+ .command('configure')
54
+ .description('Configure S3DB connection')
55
+ .action(async () => {
56
+ const answers = await inquirer.prompt([
57
+ {
58
+ type: 'input',
59
+ name: 'connection',
60
+ message: 'Enter S3 connection string:',
61
+ default: 's3://KEY:SECRET@bucket/database'
62
+ },
63
+ {
64
+ type: 'list',
65
+ name: 'defaultBehavior',
66
+ message: 'Default behavior for resources:',
67
+ choices: ['user-managed', 'enforce-limits', 'body-overflow', 'body-only', 'truncate-data'],
68
+ default: 'user-managed'
69
+ }
70
+ ]);
71
+
72
+ await saveConfig(answers);
73
+ console.log(chalk.green('✓ Configuration saved to ~/.s3db/config.json'));
74
+ });
75
+
76
+ // List resources
77
+ program
78
+ .command('list')
79
+ .description('List all resources')
80
+ .option('-c, --connection <string>', 'Connection string')
81
+ .action(async (options) => {
82
+ const spinner = ora('Connecting to S3DB...').start();
83
+
84
+ try {
85
+ const db = await getDatabase(options);
86
+ await db.init();
87
+
88
+ const resources = await db.listResources();
89
+ spinner.stop();
90
+
91
+ if (resources.length === 0) {
92
+ console.log(chalk.yellow('No resources found'));
93
+ return;
94
+ }
95
+
96
+ const table = new Table({
97
+ head: ['Resource', 'Behavior', 'Timestamps', 'Paranoid', 'Partitions'],
98
+ style: { head: ['cyan'] }
99
+ });
100
+
101
+ resources.forEach(r => {
102
+ table.push([
103
+ r.name,
104
+ r.config.behavior || 'user-managed',
105
+ r.config.timestamps ? '✓' : '✗',
106
+ r.config.paranoid ? '✓' : '✗',
107
+ Object.keys(r.config.partitions || {}).length
108
+ ]);
109
+ });
110
+
111
+ console.log(table.toString());
112
+ } catch (error) {
113
+ spinner.fail(chalk.red(error.message));
114
+ process.exit(1);
115
+ }
116
+ });
117
+
118
+ // Query resource
119
+ program
120
+ .command('query <resource>')
121
+ .description('Query a resource')
122
+ .option('-c, --connection <string>', 'Connection string')
123
+ .option('-l, --limit <number>', 'Limit results', '10')
124
+ .option('-f, --filter <json>', 'Filter as JSON')
125
+ .option('-p, --partition <name>', 'Partition name')
126
+ .option('--csv', 'Output as CSV')
127
+ .option('--json', 'Output as JSON')
128
+ .action(async (resourceName, options) => {
129
+ const spinner = ora('Querying...').start();
130
+
131
+ try {
132
+ const db = await getDatabase(options);
133
+ await db.init();
134
+
135
+ const resource = await db.resource(resourceName);
136
+
137
+ const queryOptions = {
138
+ limit: parseInt(options.limit)
139
+ };
140
+
141
+ if (options.filter) {
142
+ queryOptions.filter = JSON.parse(options.filter);
143
+ }
144
+
145
+ if (options.partition) {
146
+ queryOptions.partition = options.partition;
147
+ }
148
+
149
+ const results = await resource.list(queryOptions);
150
+ spinner.stop();
151
+
152
+ if (options.json) {
153
+ console.log(JSON.stringify(results, null, 2));
154
+ } else if (options.csv) {
155
+ if (results.length > 0) {
156
+ const headers = Object.keys(results[0]);
157
+ console.log(headers.join(','));
158
+ results.forEach(row => {
159
+ console.log(headers.map(h => JSON.stringify(row[h] || '')).join(','));
160
+ });
161
+ }
162
+ } else {
163
+ // Table output
164
+ if (results.length === 0) {
165
+ console.log(chalk.yellow('No results found'));
166
+ return;
167
+ }
168
+
169
+ const headers = Object.keys(results[0]);
170
+ const table = new Table({
171
+ head: headers,
172
+ style: { head: ['cyan'] }
173
+ });
174
+
175
+ results.forEach(row => {
176
+ table.push(headers.map(h => {
177
+ const val = row[h];
178
+ if (val === null || val === undefined) return '';
179
+ if (typeof val === 'object') return JSON.stringify(val);
180
+ return String(val).substring(0, 50);
181
+ }));
182
+ });
183
+
184
+ console.log(table.toString());
185
+ }
186
+ } catch (error) {
187
+ spinner.fail(chalk.red(error.message));
188
+ process.exit(1);
189
+ }
190
+ });
191
+
192
+ // Insert data
193
+ program
194
+ .command('insert <resource>')
195
+ .description('Insert data into a resource')
196
+ .option('-c, --connection <string>', 'Connection string')
197
+ .option('-d, --data <json>', 'Data as JSON')
198
+ .option('-f, --file <path>', 'Read data from file')
199
+ .action(async (resourceName, options) => {
200
+ const spinner = ora('Inserting...').start();
201
+
202
+ try {
203
+ const db = await getDatabase(options);
204
+ await db.init();
205
+
206
+ const resource = await db.resource(resourceName);
207
+
208
+ let data;
209
+ if (options.file) {
210
+ const content = await fs.readFile(options.file, 'utf-8');
211
+ data = JSON.parse(content);
212
+ } else if (options.data) {
213
+ data = JSON.parse(options.data);
214
+ } else {
215
+ spinner.fail('No data provided. Use --data or --file');
216
+ process.exit(1);
217
+ }
218
+
219
+ const result = await resource.insert(data);
220
+ spinner.succeed(chalk.green(`✓ Inserted with ID: ${result.id}`));
221
+
222
+ if (!options.quiet) {
223
+ console.log(JSON.stringify(result, null, 2));
224
+ }
225
+ } catch (error) {
226
+ spinner.fail(chalk.red(error.message));
227
+ process.exit(1);
228
+ }
229
+ });
230
+
231
+ // Update data
232
+ program
233
+ .command('update <resource> <id>')
234
+ .description('Update a record')
235
+ .option('-c, --connection <string>', 'Connection string')
236
+ .option('-d, --data <json>', 'Data as JSON')
237
+ .action(async (resourceName, id, options) => {
238
+ const spinner = ora('Updating...').start();
239
+
240
+ try {
241
+ const db = await getDatabase(options);
242
+ await db.init();
243
+
244
+ const resource = await db.resource(resourceName);
245
+ const data = JSON.parse(options.data || '{}');
246
+
247
+ const result = await resource.update(id, data);
248
+ spinner.succeed(chalk.green(`✓ Updated ID: ${id}`));
249
+
250
+ console.log(JSON.stringify(result, null, 2));
251
+ } catch (error) {
252
+ spinner.fail(chalk.red(error.message));
253
+ process.exit(1);
254
+ }
255
+ });
256
+
257
+ // Delete data
258
+ program
259
+ .command('delete <resource> <id>')
260
+ .description('Delete a record')
261
+ .option('-c, --connection <string>', 'Connection string')
262
+ .option('--force', 'Force delete (no confirmation)')
263
+ .action(async (resourceName, id, options) => {
264
+ if (!options.force) {
265
+ const { confirm } = await inquirer.prompt([
266
+ {
267
+ type: 'confirm',
268
+ name: 'confirm',
269
+ message: `Are you sure you want to delete ${id} from ${resourceName}?`,
270
+ default: false
271
+ }
272
+ ]);
273
+
274
+ if (!confirm) {
275
+ console.log(chalk.yellow('Cancelled'));
276
+ return;
277
+ }
278
+ }
279
+
280
+ const spinner = ora('Deleting...').start();
281
+
282
+ try {
283
+ const db = await getDatabase(options);
284
+ await db.init();
285
+
286
+ const resource = await db.resource(resourceName);
287
+ await resource.delete(id);
288
+
289
+ spinner.succeed(chalk.green(`✓ Deleted ID: ${id}`));
290
+ } catch (error) {
291
+ spinner.fail(chalk.red(error.message));
292
+ process.exit(1);
293
+ }
294
+ });
295
+
296
+ // Create resource
297
+ program
298
+ .command('create-resource <name>')
299
+ .description('Create a new resource')
300
+ .option('-c, --connection <string>', 'Connection string')
301
+ .option('-s, --schema <json>', 'Schema as JSON')
302
+ .option('-b, --behavior <type>', 'Behavior type', 'user-managed')
303
+ .option('--timestamps', 'Enable timestamps')
304
+ .option('--paranoid', 'Enable soft deletes')
305
+ .action(async (name, options) => {
306
+ const spinner = ora('Creating resource...').start();
307
+
308
+ try {
309
+ const db = await getDatabase(options);
310
+ await db.init();
311
+
312
+ const config = {
313
+ name,
314
+ behavior: options.behavior,
315
+ timestamps: options.timestamps,
316
+ paranoid: options.paranoid
317
+ };
318
+
319
+ if (options.schema) {
320
+ config.attributes = JSON.parse(options.schema);
321
+ }
322
+
323
+ const resource = await db.createResource(config);
324
+ spinner.succeed(chalk.green(`✓ Created resource: ${name}`));
325
+
326
+ console.log(JSON.stringify(resource.config, null, 2));
327
+ } catch (error) {
328
+ spinner.fail(chalk.red(error.message));
329
+ process.exit(1);
330
+ }
331
+ });
332
+
333
+ // Interactive mode
334
+ program
335
+ .command('interactive')
336
+ .description('Interactive REPL mode')
337
+ .option('-c, --connection <string>', 'Connection string')
338
+ .action(async (options) => {
339
+ console.log(chalk.cyan('S3DB Interactive Mode'));
340
+ console.log(chalk.gray('Type "help" for commands, "exit" to quit\n'));
341
+
342
+ const db = await getDatabase(options);
343
+ await db.init();
344
+
345
+ const repl = await import('repl');
346
+ const server = repl.start({
347
+ prompt: chalk.green('s3db> '),
348
+ eval: async (cmd, context, filename, callback) => {
349
+ try {
350
+ // Make db available in REPL
351
+ context.db = db;
352
+
353
+ // Parse commands
354
+ const trimmed = cmd.trim();
355
+ if (trimmed === 'help') {
356
+ console.log(`
357
+ Available commands:
358
+ db - Database instance
359
+ db.listResources() - List all resources
360
+ db.resource('name') - Get a resource
361
+ await ... - Use await for async operations
362
+ .exit - Exit REPL
363
+ `);
364
+ callback(null);
365
+ } else {
366
+ // Default eval
367
+ const result = await eval(cmd);
368
+ callback(null, result);
369
+ }
370
+ } catch (error) {
371
+ callback(error);
372
+ }
373
+ }
374
+ });
375
+
376
+ server.setupHistory(path.join(os.homedir(), '.s3db', 'history'), () => {});
377
+ });
378
+
379
+ // Stats command
380
+ program
381
+ .command('stats [resource]')
382
+ .description('Show statistics')
383
+ .option('-c, --connection <string>', 'Connection string')
384
+ .action(async (resourceName, options) => {
385
+ const spinner = ora('Gathering stats...').start();
386
+
387
+ try {
388
+ const db = await getDatabase(options);
389
+ await db.init();
390
+
391
+ if (resourceName) {
392
+ const resource = await db.resource(resourceName);
393
+ const count = await resource.count();
394
+ spinner.stop();
395
+
396
+ console.log(chalk.cyan(`\nResource: ${resourceName}`));
397
+ console.log(`Total records: ${count}`);
398
+ } else {
399
+ const resources = await db.listResources();
400
+ spinner.stop();
401
+
402
+ console.log(chalk.cyan('\nDatabase Statistics'));
403
+ console.log(`Total resources: ${resources.length}`);
404
+
405
+ if (resources.length > 0) {
406
+ const table = new Table({
407
+ head: ['Resource', 'Count'],
408
+ style: { head: ['cyan'] }
409
+ });
410
+
411
+ for (const r of resources) {
412
+ const resource = await db.resource(r.name);
413
+ const count = await resource.count();
414
+ table.push([r.name, count]);
415
+ }
416
+
417
+ console.log(table.toString());
418
+ }
419
+ }
420
+ } catch (error) {
421
+ spinner.fail(chalk.red(error.message));
422
+ process.exit(1);
423
+ }
424
+ });
425
+
426
+ program.parse(process.argv);
@@ -20,6 +20,7 @@ import {
20
20
  import tryFn from "./concerns/try-fn.js";
21
21
  import { md5 } from "./concerns/crypto.js";
22
22
  import { idGenerator } from "./concerns/id.js";
23
+ import { metadataEncode, metadataDecode } from "./concerns/metadata-encoding.js";
23
24
  import { ConnectionString } from "./connection-string.class.js";
24
25
  import { mapAwsError, UnknownError, NoSuchKey, NotFound } from "./errors.js";
25
26
 
@@ -119,23 +120,16 @@ export class Client extends EventEmitter {
119
120
  const keyPrefix = typeof this.config.keyPrefix === 'string' ? this.config.keyPrefix : '';
120
121
  const fullKey = keyPrefix ? path.join(keyPrefix, key) : key;
121
122
 
122
- // Ensure all metadata values are strings and keys are valid
123
+ // Ensure all metadata values are strings and use smart encoding
123
124
  const stringMetadata = {};
124
125
  if (metadata) {
125
126
  for (const [k, v] of Object.entries(metadata)) {
126
- // Ensure key is a valid string and value is a string
127
+ // Ensure key is a valid string
127
128
  const validKey = String(k).replace(/[^a-zA-Z0-9\-_]/g, '_');
128
- const stringValue = String(v);
129
129
 
130
- // Check if value contains non-ASCII characters that might be corrupted by HTTP headers
131
- const hasSpecialChars = /[^\x00-\x7F]/.test(stringValue);
132
-
133
- if (hasSpecialChars) {
134
- // Encode as base64 without prefix - we'll detect it intelligently on read
135
- stringMetadata[validKey] = Buffer.from(stringValue, 'utf8').toString('base64');
136
- } else {
137
- stringMetadata[validKey] = stringValue;
138
- }
130
+ // Smart encode the value
131
+ const { encoded } = metadataEncode(v);
132
+ stringMetadata[validKey] = encoded;
139
133
  }
140
134
  }
141
135
 
@@ -178,30 +172,11 @@ export class Client extends EventEmitter {
178
172
  try {
179
173
  response = await this.sendCommand(new GetObjectCommand(options));
180
174
 
181
- // Smart decode: try to detect base64-encoded UTF-8 values without prefix
175
+ // Smart decode metadata values
182
176
  if (response.Metadata) {
183
177
  const decodedMetadata = {};
184
178
  for (const [key, value] of Object.entries(response.Metadata)) {
185
- if (typeof value === 'string') {
186
- // Try to decode as base64 and check if it's valid UTF-8 with special chars
187
- try {
188
- const decoded = Buffer.from(value, 'base64').toString('utf8');
189
- // Check if decoded string is different from original and contains non-ASCII chars
190
- const hasSpecialChars = /[^\x00-\x7F]/.test(decoded);
191
- const isValidBase64 = Buffer.from(decoded, 'utf8').toString('base64') === value;
192
-
193
- if (isValidBase64 && hasSpecialChars && decoded !== value) {
194
- decodedMetadata[key] = decoded;
195
- } else {
196
- decodedMetadata[key] = value;
197
- }
198
- } catch (decodeError) {
199
- // If decode fails, use original value
200
- decodedMetadata[key] = value;
201
- }
202
- } else {
203
- decodedMetadata[key] = value;
204
- }
179
+ decodedMetadata[key] = metadataDecode(value);
205
180
  }
206
181
  response.Metadata = decodedMetadata;
207
182
  }