s3db.js 7.4.0 → 7.4.2

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,6 +1,6 @@
1
1
  {
2
2
  "name": "s3db.js",
3
- "version": "7.4.0",
3
+ "version": "7.4.2",
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",
@@ -103,7 +103,17 @@ export class Client extends EventEmitter {
103
103
  for (const [k, v] of Object.entries(metadata)) {
104
104
  // Ensure key is a valid string and value is a string
105
105
  const validKey = String(k).replace(/[^a-zA-Z0-9\-_]/g, '_');
106
- stringMetadata[validKey] = String(v);
106
+ const stringValue = String(v);
107
+
108
+ // Check if value contains non-ASCII characters that might be corrupted by HTTP headers
109
+ const hasSpecialChars = /[^\x00-\x7F]/.test(stringValue);
110
+
111
+ if (hasSpecialChars) {
112
+ // Encode as base64 without prefix - we'll detect it intelligently on read
113
+ stringMetadata[validKey] = Buffer.from(stringValue, 'utf8').toString('base64');
114
+ } else {
115
+ stringMetadata[validKey] = stringValue;
116
+ }
107
117
  }
108
118
  }
109
119
 
@@ -141,9 +151,39 @@ export class Client extends EventEmitter {
141
151
  Bucket: this.config.bucket,
142
152
  Key: keyPrefix ? path.join(keyPrefix, key) : key,
143
153
  };
154
+
144
155
  let response, error;
145
156
  try {
146
157
  response = await this.sendCommand(new GetObjectCommand(options));
158
+
159
+ // Smart decode: try to detect base64-encoded UTF-8 values without prefix
160
+ if (response.Metadata) {
161
+ const decodedMetadata = {};
162
+ for (const [key, value] of Object.entries(response.Metadata)) {
163
+ if (typeof value === 'string') {
164
+ // Try to decode as base64 and check if it's valid UTF-8 with special chars
165
+ try {
166
+ const decoded = Buffer.from(value, 'base64').toString('utf8');
167
+ // Check if decoded string is different from original and contains non-ASCII chars
168
+ const hasSpecialChars = /[^\x00-\x7F]/.test(decoded);
169
+ const isValidBase64 = Buffer.from(decoded, 'utf8').toString('base64') === value;
170
+
171
+ if (isValidBase64 && hasSpecialChars && decoded !== value) {
172
+ decodedMetadata[key] = decoded;
173
+ } else {
174
+ decodedMetadata[key] = value;
175
+ }
176
+ } catch (decodeError) {
177
+ // If decode fails, use original value
178
+ decodedMetadata[key] = value;
179
+ }
180
+ } else {
181
+ decodedMetadata[key] = value;
182
+ }
183
+ }
184
+ response.Metadata = decodedMetadata;
185
+ }
186
+
147
187
  return response;
148
188
  } catch (err) {
149
189
  error = err;
@@ -113,6 +113,23 @@ export class Database extends EventEmitter {
113
113
 
114
114
  if (versionData) {
115
115
  // Extract configuration from version data at root level
116
+ // Restore ID generator configuration
117
+ let restoredIdGenerator, restoredIdSize;
118
+ if (versionData.idGenerator !== undefined) {
119
+ if (versionData.idGenerator === 'custom_function') {
120
+ // Custom function was used but can't be restored - use default
121
+ restoredIdGenerator = undefined;
122
+ restoredIdSize = versionData.idSize || 22;
123
+ } else if (typeof versionData.idGenerator === 'number') {
124
+ // Size-based generator
125
+ restoredIdGenerator = versionData.idGenerator;
126
+ restoredIdSize = versionData.idSize || versionData.idGenerator;
127
+ }
128
+ } else {
129
+ // Legacy resource without saved ID config
130
+ restoredIdSize = versionData.idSize || 22;
131
+ }
132
+
116
133
  this.resources[name] = new Resource({
117
134
  name,
118
135
  client: this.client,
@@ -131,7 +148,9 @@ export class Database extends EventEmitter {
131
148
  autoDecrypt: versionData.autoDecrypt !== undefined ? versionData.autoDecrypt : true,
132
149
  hooks: versionData.hooks || {},
133
150
  versioningEnabled: this.versioningEnabled,
134
- map: versionData.map
151
+ map: versionData.map,
152
+ idGenerator: restoredIdGenerator,
153
+ idSize: restoredIdSize
135
154
  });
136
155
  }
137
156
  }
@@ -334,6 +353,8 @@ export class Database extends EventEmitter {
334
353
  autoDecrypt: resource.config.autoDecrypt,
335
354
  cache: resource.config.cache,
336
355
  hooks: resource.config.hooks,
356
+ idSize: resource.idSize,
357
+ idGenerator: resource.idGeneratorType,
337
358
  createdAt: isNewVersion ? new Date().toISOString() : existingVersionData?.createdAt
338
359
  }
339
360
  }
@@ -142,6 +142,19 @@ export class Resource extends EventEmitter {
142
142
 
143
143
  // Configure ID generator
144
144
  this.idGenerator = this.configureIdGenerator(customIdGenerator, idSize);
145
+
146
+ // Store ID configuration for persistence
147
+ // If customIdGenerator is a number, use it as idSize
148
+ // Otherwise, use the provided idSize or default to 22
149
+ if (typeof customIdGenerator === 'number' && customIdGenerator > 0) {
150
+ this.idSize = customIdGenerator;
151
+ } else if (typeof idSize === 'number' && idSize > 0) {
152
+ this.idSize = idSize;
153
+ } else {
154
+ this.idSize = 22;
155
+ }
156
+
157
+ this.idGeneratorType = this.getIdGeneratorType(customIdGenerator, this.idSize);
145
158
 
146
159
  // Store configuration - all at root level
147
160
  this.config = {
@@ -220,9 +233,9 @@ export class Resource extends EventEmitter {
220
233
  * @private
221
234
  */
222
235
  configureIdGenerator(customIdGenerator, idSize) {
223
- // If a custom function is provided, use it
236
+ // If a custom function is provided, wrap it to ensure string output
224
237
  if (typeof customIdGenerator === 'function') {
225
- return customIdGenerator;
238
+ return () => String(customIdGenerator());
226
239
  }
227
240
  // If customIdGenerator is a number (size), create a generator with that size
228
241
  if (typeof customIdGenerator === 'number' && customIdGenerator > 0) {
@@ -236,6 +249,22 @@ export class Resource extends EventEmitter {
236
249
  return defaultIdGenerator;
237
250
  }
238
251
 
252
+ /**
253
+ * Get a serializable representation of the ID generator type
254
+ * @param {Function|number} customIdGenerator - Custom ID generator function or size
255
+ * @param {number} idSize - Size for auto-generated IDs
256
+ * @returns {string|number} Serializable ID generator type
257
+ * @private
258
+ */
259
+ getIdGeneratorType(customIdGenerator, idSize) {
260
+ // If a custom function is provided
261
+ if (typeof customIdGenerator === 'function') {
262
+ return 'custom_function';
263
+ }
264
+ // For number generators or default size, return the actual idSize
265
+ return idSize;
266
+ }
267
+
239
268
  /**
240
269
  * Get resource options (for backward compatibility with tests)
241
270
  */