s3db.js 4.1.6 → 4.1.8

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/dist/s3db.cjs.js CHANGED
@@ -8568,6 +8568,14 @@ function calculateUTF8Bytes(str) {
8568
8568
  }
8569
8569
  return bytes;
8570
8570
  }
8571
+ function calculateAttributeNamesSize(mappedObject) {
8572
+ let totalSize = 0;
8573
+ for (const key of Object.keys(mappedObject)) {
8574
+ if (key === "_v") continue;
8575
+ totalSize += calculateUTF8Bytes(key);
8576
+ }
8577
+ return totalSize;
8578
+ }
8571
8579
  function transformValue(value) {
8572
8580
  if (value === null || value === void 0) {
8573
8581
  return "";
@@ -8602,49 +8610,31 @@ function calculateAttributeSizes(mappedObject) {
8602
8610
  return sizes;
8603
8611
  }
8604
8612
  function calculateTotalSize(mappedObject) {
8605
- const sizes = calculateAttributeSizes(mappedObject);
8606
- return Object.values(sizes).reduce((total, size) => total + size, 0);
8613
+ const valueSizes = calculateAttributeSizes(mappedObject);
8614
+ const valueTotal = Object.values(valueSizes).reduce((total, size) => total + size, 0);
8615
+ const namesSize = calculateAttributeNamesSize(mappedObject);
8616
+ return valueTotal + namesSize;
8607
8617
  }
8608
8618
 
8609
- const S3_METADATA_LIMIT_BYTES$3 = 2e3;
8619
+ const S3_METADATA_LIMIT_BYTES = 2048;
8610
8620
  async function handleInsert$3({ resource, data, mappedData }) {
8611
8621
  const totalSize = calculateTotalSize(mappedData);
8612
- if (totalSize > S3_METADATA_LIMIT_BYTES$3) {
8613
- resource.emit("exceedsLimit", {
8614
- operation: "insert",
8615
- totalSize,
8616
- limit: S3_METADATA_LIMIT_BYTES$3,
8617
- excess: totalSize - S3_METADATA_LIMIT_BYTES$3,
8618
- data
8619
- });
8622
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8623
+ throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
8620
8624
  }
8621
8625
  return { mappedData, body: "" };
8622
8626
  }
8623
8627
  async function handleUpdate$3({ resource, id, data, mappedData }) {
8624
8628
  const totalSize = calculateTotalSize(mappedData);
8625
- if (totalSize > S3_METADATA_LIMIT_BYTES$3) {
8626
- resource.emit("exceedsLimit", {
8627
- operation: "update",
8628
- id,
8629
- totalSize,
8630
- limit: S3_METADATA_LIMIT_BYTES$3,
8631
- excess: totalSize - S3_METADATA_LIMIT_BYTES$3,
8632
- data
8633
- });
8629
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8630
+ throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
8634
8631
  }
8635
8632
  return { mappedData, body: "" };
8636
8633
  }
8637
8634
  async function handleUpsert$3({ resource, id, data, mappedData }) {
8638
8635
  const totalSize = calculateTotalSize(mappedData);
8639
- if (totalSize > S3_METADATA_LIMIT_BYTES$3) {
8640
- resource.emit("exceedsLimit", {
8641
- operation: "upsert",
8642
- id,
8643
- totalSize,
8644
- limit: S3_METADATA_LIMIT_BYTES$3,
8645
- excess: totalSize - S3_METADATA_LIMIT_BYTES$3,
8646
- data
8647
- });
8636
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8637
+ throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
8648
8638
  }
8649
8639
  return { mappedData, body: "" };
8650
8640
  }
@@ -8652,33 +8642,53 @@ async function handleGet$3({ resource, metadata, body }) {
8652
8642
  return { metadata, body };
8653
8643
  }
8654
8644
 
8655
- var userManagement = /*#__PURE__*/Object.freeze({
8645
+ var enforceLimits = /*#__PURE__*/Object.freeze({
8656
8646
  __proto__: null,
8647
+ S3_METADATA_LIMIT_BYTES: S3_METADATA_LIMIT_BYTES,
8657
8648
  handleGet: handleGet$3,
8658
8649
  handleInsert: handleInsert$3,
8659
8650
  handleUpdate: handleUpdate$3,
8660
8651
  handleUpsert: handleUpsert$3
8661
8652
  });
8662
8653
 
8663
- const S3_METADATA_LIMIT_BYTES$2 = 2e3;
8664
8654
  async function handleInsert$2({ resource, data, mappedData }) {
8665
8655
  const totalSize = calculateTotalSize(mappedData);
8666
- if (totalSize > S3_METADATA_LIMIT_BYTES$2) {
8667
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES$2} bytes`);
8656
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8657
+ resource.emit("exceedsLimit", {
8658
+ operation: "insert",
8659
+ totalSize,
8660
+ limit: S3_METADATA_LIMIT_BYTES,
8661
+ excess: totalSize - S3_METADATA_LIMIT_BYTES,
8662
+ data
8663
+ });
8668
8664
  }
8669
8665
  return { mappedData, body: "" };
8670
8666
  }
8671
8667
  async function handleUpdate$2({ resource, id, data, mappedData }) {
8672
8668
  const totalSize = calculateTotalSize(mappedData);
8673
- if (totalSize > S3_METADATA_LIMIT_BYTES$2) {
8674
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES$2} bytes`);
8669
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8670
+ resource.emit("exceedsLimit", {
8671
+ operation: "update",
8672
+ id,
8673
+ totalSize,
8674
+ limit: S3_METADATA_LIMIT_BYTES,
8675
+ excess: totalSize - S3_METADATA_LIMIT_BYTES,
8676
+ data
8677
+ });
8675
8678
  }
8676
8679
  return { mappedData, body: "" };
8677
8680
  }
8678
8681
  async function handleUpsert$2({ resource, id, data, mappedData }) {
8679
8682
  const totalSize = calculateTotalSize(mappedData);
8680
- if (totalSize > S3_METADATA_LIMIT_BYTES$2) {
8681
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES$2} bytes`);
8683
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8684
+ resource.emit("exceedsLimit", {
8685
+ operation: "upsert",
8686
+ id,
8687
+ totalSize,
8688
+ limit: S3_METADATA_LIMIT_BYTES,
8689
+ excess: totalSize - S3_METADATA_LIMIT_BYTES,
8690
+ data
8691
+ });
8682
8692
  }
8683
8693
  return { mappedData, body: "" };
8684
8694
  }
@@ -8686,7 +8696,7 @@ async function handleGet$2({ resource, metadata, body }) {
8686
8696
  return { metadata, body };
8687
8697
  }
8688
8698
 
8689
- var enforceLimits = /*#__PURE__*/Object.freeze({
8699
+ var userManagement = /*#__PURE__*/Object.freeze({
8690
8700
  __proto__: null,
8691
8701
  handleGet: handleGet$2,
8692
8702
  handleInsert: handleInsert$2,
@@ -8694,7 +8704,6 @@ var enforceLimits = /*#__PURE__*/Object.freeze({
8694
8704
  handleUpsert: handleUpsert$2
8695
8705
  });
8696
8706
 
8697
- const S3_METADATA_LIMIT_BYTES$1 = 2e3;
8698
8707
  const TRUNCATE_SUFFIX = "...";
8699
8708
  const TRUNCATE_SUFFIX_BYTES = calculateUTF8Bytes(TRUNCATE_SUFFIX);
8700
8709
  async function handleInsert$1({ resource, data, mappedData }) {
@@ -8715,7 +8724,7 @@ function handleTruncate({ resource, data, mappedData }) {
8715
8724
  const result = {};
8716
8725
  let currentSize = 0;
8717
8726
  for (const [key, size] of sortedAttributes) {
8718
- const availableSpace = S3_METADATA_LIMIT_BYTES$1 - currentSize;
8727
+ const availableSpace = S3_METADATA_LIMIT_BYTES - currentSize;
8719
8728
  if (size <= availableSpace) {
8720
8729
  result[key] = mappedData[key];
8721
8730
  currentSize += size;
@@ -8735,7 +8744,7 @@ function handleTruncate({ resource, data, mappedData }) {
8735
8744
  }
8736
8745
  }
8737
8746
  result[key] = truncatedValue + TRUNCATE_SUFFIX;
8738
- currentSize = S3_METADATA_LIMIT_BYTES$1;
8747
+ currentSize = S3_METADATA_LIMIT_BYTES;
8739
8748
  break;
8740
8749
  } else {
8741
8750
  break;
@@ -8752,7 +8761,6 @@ var dataTruncate = /*#__PURE__*/Object.freeze({
8752
8761
  handleUpsert: handleUpsert$1
8753
8762
  });
8754
8763
 
8755
- const S3_METADATA_LIMIT_BYTES = 2e3;
8756
8764
  const OVERFLOW_FLAG = "$overflow";
8757
8765
  const OVERFLOW_FLAG_VALUE = "true";
8758
8766
  const OVERFLOW_FLAG_BYTES = calculateUTF8Bytes(OVERFLOW_FLAG) + calculateUTF8Bytes(OVERFLOW_FLAG_VALUE);
@@ -9095,13 +9103,29 @@ class Resource extends EventEmitter {
9095
9103
  return join(`resource=${this.name}`, `v=${this.version}`, `id=${id}`);
9096
9104
  }
9097
9105
  /**
9098
- * Get partition reference key for a specific partition
9099
- * @param {string} partitionName - Name of the partition
9100
- * @param {string} id - Resource ID
9101
- * @param {Object} data - Data object for partition value generation
9102
- * @returns {string|null} The partition reference S3 key path
9106
+ * Generate partition key for a resource in a specific partition
9107
+ * @param {Object} params - Partition key parameters
9108
+ * @param {string} params.partitionName - Name of the partition
9109
+ * @param {string} params.id - Resource ID
9110
+ * @param {Object} params.data - Resource data for partition value extraction
9111
+ * @returns {string|null} The partition key path or null if required fields are missing
9112
+ * @example
9113
+ * const partitionKey = resource.getPartitionKey({
9114
+ * partitionName: 'byUtmSource',
9115
+ * id: 'user-123',
9116
+ * data: { utm: { source: 'google' } }
9117
+ * });
9118
+ * // Returns: 'resource=users/partition=byUtmSource/utm.source=google/id=user-123'
9119
+ *
9120
+ * // Returns null if required field is missing
9121
+ * const nullKey = resource.getPartitionKey({
9122
+ * partitionName: 'byUtmSource',
9123
+ * id: 'user-123',
9124
+ * data: { name: 'John' } // Missing utm.source
9125
+ * });
9126
+ * // Returns: null
9103
9127
  */
9104
- getPartitionKey(partitionName, id, data) {
9128
+ getPartitionKey({ partitionName, id, data }) {
9105
9129
  const partition = this.options.partitions[partitionName];
9106
9130
  if (!partition) {
9107
9131
  throw new Error(`Partition '${partitionName}' not found`);
@@ -9132,15 +9156,36 @@ class Resource extends EventEmitter {
9132
9156
  return data[fieldPath];
9133
9157
  }
9134
9158
  const keys = fieldPath.split(".");
9135
- let value = data;
9159
+ let currentLevel = data;
9136
9160
  for (const key of keys) {
9137
- if (value === null || value === void 0 || typeof value !== "object") {
9161
+ if (!currentLevel || typeof currentLevel !== "object" || !(key in currentLevel)) {
9138
9162
  return void 0;
9139
9163
  }
9140
- value = value[key];
9164
+ currentLevel = currentLevel[key];
9141
9165
  }
9142
- return value;
9166
+ return currentLevel;
9143
9167
  }
9168
+ /**
9169
+ * Insert a new resource object
9170
+ * @param {Object} params - Insert parameters
9171
+ * @param {string} [params.id] - Resource ID (auto-generated if not provided)
9172
+ * @param {...Object} params - Resource attributes (any additional properties)
9173
+ * @returns {Promise<Object>} The inserted resource object with all attributes and generated ID
9174
+ * @example
9175
+ * // Insert with auto-generated ID
9176
+ * const user = await resource.insert({
9177
+ * name: 'John Doe',
9178
+ * email: 'john@example.com',
9179
+ * age: 30
9180
+ * });
9181
+ *
9182
+ * // Insert with custom ID
9183
+ * const user = await resource.insert({
9184
+ * id: 'custom-id-123',
9185
+ * name: 'Jane Smith',
9186
+ * email: 'jane@example.com'
9187
+ * });
9188
+ */
9144
9189
  async insert({ id, ...attributes }) {
9145
9190
  if (this.options.timestamps) {
9146
9191
  attributes.createdAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -9179,39 +9224,68 @@ class Resource extends EventEmitter {
9179
9224
  this.emit("insert", final);
9180
9225
  return final;
9181
9226
  }
9227
+ /**
9228
+ * Retrieve a resource object by ID
9229
+ * @param {string} id - Resource ID
9230
+ * @returns {Promise<Object>} The resource object with all attributes and metadata
9231
+ * @example
9232
+ * const user = await resource.get('user-123');
9233
+ * console.log(user.name); // 'John Doe'
9234
+ * console.log(user._lastModified); // Date object
9235
+ * console.log(user._hasContent); // boolean
9236
+ */
9182
9237
  async get(id) {
9183
9238
  const key = this.getResourceKey(id);
9184
- const request = await this.client.headObject(key);
9185
- const objectVersion = this.extractVersionFromKey(key) || this.version;
9186
- const schema = await this.getSchemaForVersion(objectVersion);
9187
- let metadata = await schema.unmapper(request.Metadata);
9188
- const behaviorImpl = getBehavior(this.behavior);
9189
- let body = "";
9190
- if (request.ContentLength > 0) {
9191
- try {
9192
- const fullObject = await this.client.getObject(key);
9193
- body = await streamToString(fullObject.Body);
9194
- } catch (error) {
9195
- body = "";
9239
+ try {
9240
+ const request = await this.client.headObject(key);
9241
+ const objectVersion = this.extractVersionFromKey(key) || this.version;
9242
+ const schema = await this.getSchemaForVersion(objectVersion);
9243
+ let metadata = await schema.unmapper(request.Metadata);
9244
+ const behaviorImpl = getBehavior(this.behavior);
9245
+ let body = "";
9246
+ if (request.ContentLength > 0) {
9247
+ try {
9248
+ const fullObject = await this.client.getObject(key);
9249
+ body = await streamToString(fullObject.Body);
9250
+ } catch (error) {
9251
+ console.warn(`Failed to read body for resource ${id}:`, error.message);
9252
+ body = "";
9253
+ }
9196
9254
  }
9255
+ const { metadata: processedMetadata } = await behaviorImpl.handleGet({
9256
+ resource: this,
9257
+ metadata,
9258
+ body
9259
+ });
9260
+ let data = processedMetadata;
9261
+ data.id = id;
9262
+ data._contentLength = request.ContentLength;
9263
+ data._lastModified = request.LastModified;
9264
+ data._hasContent = request.ContentLength > 0;
9265
+ data._mimeType = request.ContentType || null;
9266
+ if (request.VersionId) data._versionId = request.VersionId;
9267
+ if (request.Expiration) data._expiresAt = request.Expiration;
9268
+ data._definitionHash = this.getDefinitionHash();
9269
+ this.emit("get", data);
9270
+ return data;
9271
+ } catch (error) {
9272
+ const enhancedError = new Error(`Failed to get resource with id '${id}': ${error.message}`);
9273
+ enhancedError.originalError = error;
9274
+ enhancedError.resourceId = id;
9275
+ enhancedError.resourceKey = key;
9276
+ throw enhancedError;
9197
9277
  }
9198
- const { metadata: processedMetadata } = await behaviorImpl.handleGet({
9199
- resource: this,
9200
- metadata,
9201
- body
9202
- });
9203
- let data = processedMetadata;
9204
- data.id = id;
9205
- data._contentLength = request.ContentLength;
9206
- data._lastModified = request.LastModified;
9207
- data._hasContent = request.ContentLength > 0;
9208
- data._mimeType = request.ContentType || null;
9209
- if (request.VersionId) data._versionId = request.VersionId;
9210
- if (request.Expiration) data._expiresAt = request.Expiration;
9211
- data._definitionHash = this.getDefinitionHash();
9212
- this.emit("get", data);
9213
- return data;
9214
9278
  }
9279
+ /**
9280
+ * Check if a resource exists by ID
9281
+ * @param {string} id - Resource ID
9282
+ * @returns {Promise<boolean>} True if resource exists, false otherwise
9283
+ * @example
9284
+ * const exists = await resource.exists('user-123');
9285
+ * if (exists) {
9286
+ * console.log('User exists');
9287
+ * }
9288
+ */
9215
9289
  async exists(id) {
9216
9290
  try {
9217
9291
  const key = this.getResourceKey(id);
@@ -9221,6 +9295,24 @@ class Resource extends EventEmitter {
9221
9295
  return false;
9222
9296
  }
9223
9297
  }
9298
+ /**
9299
+ * Update an existing resource object
9300
+ * @param {string} id - Resource ID
9301
+ * @param {Object} attributes - Attributes to update (partial update supported)
9302
+ * @returns {Promise<Object>} The updated resource object with all attributes
9303
+ * @example
9304
+ * // Update specific fields
9305
+ * const updatedUser = await resource.update('user-123', {
9306
+ * name: 'John Updated',
9307
+ * age: 31
9308
+ * });
9309
+ *
9310
+ * // Update with timestamps (if enabled)
9311
+ * const updatedUser = await resource.update('user-123', {
9312
+ * email: 'newemail@example.com'
9313
+ * });
9314
+ * console.log(updatedUser.updatedAt); // ISO timestamp
9315
+ */
9224
9316
  async update(id, attributes) {
9225
9317
  const live = await this.get(id);
9226
9318
  if (this.options.timestamps) {
@@ -9277,6 +9369,14 @@ class Resource extends EventEmitter {
9277
9369
  this.emit("update", preProcessedData, validated);
9278
9370
  return validated;
9279
9371
  }
9372
+ /**
9373
+ * Delete a resource object by ID
9374
+ * @param {string} id - Resource ID
9375
+ * @returns {Promise<Object>} S3 delete response
9376
+ * @example
9377
+ * await resource.delete('user-123');
9378
+ * console.log('User deleted successfully');
9379
+ */
9280
9380
  async delete(id) {
9281
9381
  let objectData;
9282
9382
  try {
@@ -9291,6 +9391,20 @@ class Resource extends EventEmitter {
9291
9391
  this.emit("delete", id);
9292
9392
  return response;
9293
9393
  }
9394
+ /**
9395
+ * Insert or update a resource object (upsert operation)
9396
+ * @param {Object} params - Upsert parameters
9397
+ * @param {string} params.id - Resource ID (required for upsert)
9398
+ * @param {...Object} params - Resource attributes (any additional properties)
9399
+ * @returns {Promise<Object>} The inserted or updated resource object
9400
+ * @example
9401
+ * // Will insert if doesn't exist, update if exists
9402
+ * const user = await resource.upsert({
9403
+ * id: 'user-123',
9404
+ * name: 'John Doe',
9405
+ * email: 'john@example.com'
9406
+ * });
9407
+ */
9294
9408
  async upsert({ id, ...attributes }) {
9295
9409
  const exists = await this.exists(id);
9296
9410
  if (exists) {
@@ -9298,6 +9412,28 @@ class Resource extends EventEmitter {
9298
9412
  }
9299
9413
  return this.insert({ id, ...attributes });
9300
9414
  }
9415
+ /**
9416
+ * Count resources with optional partition filtering
9417
+ * @param {Object} [params] - Count parameters
9418
+ * @param {string} [params.partition] - Partition name to count in
9419
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9420
+ * @returns {Promise<number>} Total count of matching resources
9421
+ * @example
9422
+ * // Count all resources
9423
+ * const total = await resource.count();
9424
+ *
9425
+ * // Count in specific partition
9426
+ * const googleUsers = await resource.count({
9427
+ * partition: 'byUtmSource',
9428
+ * partitionValues: { 'utm.source': 'google' }
9429
+ * });
9430
+ *
9431
+ * // Count in multi-field partition
9432
+ * const usElectronics = await resource.count({
9433
+ * partition: 'byCategoryRegion',
9434
+ * partitionValues: { category: 'electronics', region: 'US' }
9435
+ * });
9436
+ */
9301
9437
  async count({ partition = null, partitionValues = {} } = {}) {
9302
9438
  let prefix;
9303
9439
  if (partition && Object.keys(partitionValues).length > 0) {
@@ -9328,6 +9464,19 @@ class Resource extends EventEmitter {
9328
9464
  this.emit("count", count);
9329
9465
  return count;
9330
9466
  }
9467
+ /**
9468
+ * Insert multiple resources in parallel
9469
+ * @param {Object[]} objects - Array of resource objects to insert
9470
+ * @returns {Promise<Object[]>} Array of inserted resource objects
9471
+ * @example
9472
+ * const users = [
9473
+ * { name: 'John', email: 'john@example.com' },
9474
+ * { name: 'Jane', email: 'jane@example.com' },
9475
+ * { name: 'Bob', email: 'bob@example.com' }
9476
+ * ];
9477
+ * const insertedUsers = await resource.insertMany(users);
9478
+ * console.log(`Inserted ${insertedUsers.length} users`);
9479
+ */
9331
9480
  async insertMany(objects) {
9332
9481
  const { results } = await promisePool.PromisePool.for(objects).withConcurrency(this.parallelism).handleError(async (error, content) => {
9333
9482
  this.emit("error", error, content);
@@ -9339,6 +9488,15 @@ class Resource extends EventEmitter {
9339
9488
  this.emit("insertMany", objects.length);
9340
9489
  return results;
9341
9490
  }
9491
+ /**
9492
+ * Delete multiple resources by their IDs in parallel
9493
+ * @param {string[]} ids - Array of resource IDs to delete
9494
+ * @returns {Promise<Object[]>} Array of S3 delete responses
9495
+ * @example
9496
+ * const deletedIds = ['user-1', 'user-2', 'user-3'];
9497
+ * const results = await resource.deleteMany(deletedIds);
9498
+ * console.log(`Deleted ${deletedIds.length} users`);
9499
+ */
9342
9500
  async deleteMany(ids) {
9343
9501
  const packages = lodashEs.chunk(
9344
9502
  ids.map((id) => this.getResourceKey(id)),
@@ -9397,7 +9555,35 @@ class Resource extends EventEmitter {
9397
9555
  });
9398
9556
  return { deletedCount, resource: this.name };
9399
9557
  }
9400
- async listIds({ partition = null, partitionValues = {} } = {}) {
9558
+ /**
9559
+ * List resource IDs with optional partition filtering and pagination
9560
+ * @param {Object} [params] - List parameters
9561
+ * @param {string} [params.partition] - Partition name to list from
9562
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9563
+ * @param {number} [params.limit] - Maximum number of results to return
9564
+ * @param {number} [params.offset=0] - Offset for pagination
9565
+ * @returns {Promise<string[]>} Array of resource IDs (strings)
9566
+ * @example
9567
+ * // List all IDs
9568
+ * const allIds = await resource.listIds();
9569
+ *
9570
+ * // List IDs with pagination
9571
+ * const firstPageIds = await resource.listIds({ limit: 10, offset: 0 });
9572
+ * const secondPageIds = await resource.listIds({ limit: 10, offset: 10 });
9573
+ *
9574
+ * // List IDs from specific partition
9575
+ * const googleUserIds = await resource.listIds({
9576
+ * partition: 'byUtmSource',
9577
+ * partitionValues: { 'utm.source': 'google' }
9578
+ * });
9579
+ *
9580
+ * // List IDs from multi-field partition
9581
+ * const usElectronicsIds = await resource.listIds({
9582
+ * partition: 'byCategoryRegion',
9583
+ * partitionValues: { category: 'electronics', region: 'US' }
9584
+ * });
9585
+ */
9586
+ async listIds({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9401
9587
  let prefix;
9402
9588
  if (partition && Object.keys(partitionValues).length > 0) {
9403
9589
  const partitionDef = this.options.partitions[partition];
@@ -9421,8 +9607,11 @@ class Resource extends EventEmitter {
9421
9607
  } else {
9422
9608
  prefix = `resource=${this.name}/v=${this.version}`;
9423
9609
  }
9424
- const keys = await this.client.getAllKeys({
9425
- prefix
9610
+ const keys = await this.client.getKeysPage({
9611
+ prefix,
9612
+ offset,
9613
+ amount: limit || 1e3
9614
+ // Default to 1000 if no limit specified
9426
9615
  });
9427
9616
  const ids = keys.map((key) => {
9428
9617
  const parts = key.split("/");
@@ -9433,13 +9622,36 @@ class Resource extends EventEmitter {
9433
9622
  return ids;
9434
9623
  }
9435
9624
  /**
9436
- * List objects by partition name and values
9437
- * @param {Object} partitionOptions - Partition options
9438
- * @param {Object} options - Listing options
9439
- * @returns {Array} Array of objects
9625
+ * List resource objects with optional partition filtering and pagination
9626
+ * @param {Object} [params] - List parameters
9627
+ * @param {string} [params.partition] - Partition name to list from
9628
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9629
+ * @param {number} [params.limit] - Maximum number of results to return
9630
+ * @param {number} [params.offset=0] - Offset for pagination
9631
+ * @returns {Promise<Object[]>} Array of resource objects with all attributes
9632
+ * @example
9633
+ * // List all resources
9634
+ * const allUsers = await resource.list();
9635
+ *
9636
+ * // List with pagination
9637
+ * const firstPage = await resource.list({ limit: 10, offset: 0 });
9638
+ * const secondPage = await resource.list({ limit: 10, offset: 10 });
9639
+ *
9640
+ * // List from specific partition
9641
+ * const googleUsers = await resource.list({
9642
+ * partition: 'byUtmSource',
9643
+ * partitionValues: { 'utm.source': 'google' }
9644
+ * });
9645
+ *
9646
+ * // List from partition with pagination
9647
+ * const googleUsersPage = await resource.list({
9648
+ * partition: 'byUtmSource',
9649
+ * partitionValues: { 'utm.source': 'google' },
9650
+ * limit: 5,
9651
+ * offset: 0
9652
+ * });
9440
9653
  */
9441
- async listByPartition({ partition = null, partitionValues = {} } = {}, options = {}) {
9442
- const { limit, offset = 0 } = options;
9654
+ async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9443
9655
  if (!partition) {
9444
9656
  const ids2 = await this.listIds({ partition, partitionValues });
9445
9657
  let filteredIds2 = ids2.slice(offset);
@@ -9449,7 +9661,7 @@ class Resource extends EventEmitter {
9449
9661
  const { results: results2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).process(async (id) => {
9450
9662
  return await this.get(id);
9451
9663
  });
9452
- this.emit("listByPartition", { partition, partitionValues, count: results2.length });
9664
+ this.emit("list", { partition, partitionValues, count: results2.length });
9453
9665
  return results2;
9454
9666
  }
9455
9667
  const partitionDef = this.options.partitions[partition];
@@ -9482,11 +9694,19 @@ class Resource extends EventEmitter {
9482
9694
  filteredIds = filteredIds.slice(0, limit);
9483
9695
  }
9484
9696
  const { results } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).process(async (id) => {
9485
- return await this.getFromPartition(id, partition, partitionValues);
9697
+ return await this.getFromPartition({ id, partitionName: partition, partitionValues });
9486
9698
  });
9487
- this.emit("listByPartition", { partition, partitionValues, count: results.length });
9699
+ this.emit("list", { partition, partitionValues, count: results.length });
9488
9700
  return results;
9489
9701
  }
9702
+ /**
9703
+ * Get multiple resources by their IDs
9704
+ * @param {string[]} ids - Array of resource IDs
9705
+ * @returns {Promise<Object[]>} Array of resource objects
9706
+ * @example
9707
+ * const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
9708
+ * users.forEach(user => console.log(user.name));
9709
+ */
9490
9710
  async getMany(ids) {
9491
9711
  const { results } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).process(async (id) => {
9492
9712
  this.emit("id", id);
@@ -9497,6 +9717,13 @@ class Resource extends EventEmitter {
9497
9717
  this.emit("getMany", ids.length);
9498
9718
  return results;
9499
9719
  }
9720
+ /**
9721
+ * Get all resources (equivalent to list() without pagination)
9722
+ * @returns {Promise<Object[]>} Array of all resource objects
9723
+ * @example
9724
+ * const allUsers = await resource.getAll();
9725
+ * console.log(`Total users: ${allUsers.length}`);
9726
+ */
9500
9727
  async getAll() {
9501
9728
  let ids = await this.listIds();
9502
9729
  if (ids.length === 0) return [];
@@ -9507,20 +9734,65 @@ class Resource extends EventEmitter {
9507
9734
  this.emit("getAll", results.length);
9508
9735
  return results;
9509
9736
  }
9510
- async page(offset = 0, size = 100, { partition = null, partitionValues = {} } = {}) {
9511
- const allIds = await this.listIds({ partition, partitionValues });
9512
- const totalItems = allIds.length;
9513
- const totalPages = Math.ceil(totalItems / size);
9514
- const paginatedIds = allIds.slice(offset * size, (offset + 1) * size);
9515
- const items = await Promise.all(
9516
- paginatedIds.map((id) => this.get(id))
9517
- );
9737
+ /**
9738
+ * Get a page of resources with pagination metadata
9739
+ * @param {Object} [params] - Page parameters
9740
+ * @param {number} [params.offset=0] - Offset for pagination
9741
+ * @param {number} [params.size=100] - Page size
9742
+ * @param {string} [params.partition] - Partition name to page from
9743
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9744
+ * @param {boolean} [params.skipCount=false] - Skip total count for performance (useful for large collections)
9745
+ * @returns {Promise<Object>} Page result with items and pagination info
9746
+ * @example
9747
+ * // Get first page of all resources
9748
+ * const page = await resource.page({ offset: 0, size: 10 });
9749
+ * console.log(`Page ${page.page + 1} of ${page.totalPages}`);
9750
+ * console.log(`Showing ${page.items.length} of ${page.totalItems} total`);
9751
+ *
9752
+ * // Get page from specific partition
9753
+ * const googlePage = await resource.page({
9754
+ * partition: 'byUtmSource',
9755
+ * partitionValues: { 'utm.source': 'google' },
9756
+ * offset: 0,
9757
+ * size: 5
9758
+ * });
9759
+ *
9760
+ * // Skip count for performance in large collections
9761
+ * const fastPage = await resource.page({
9762
+ * offset: 0,
9763
+ * size: 100,
9764
+ * skipCount: true
9765
+ * });
9766
+ * console.log(`Got ${fastPage.items.length} items`); // totalItems will be null
9767
+ */
9768
+ async page({ offset = 0, size = 100, partition = null, partitionValues = {}, skipCount = false } = {}) {
9769
+ let totalItems = null;
9770
+ let totalPages = null;
9771
+ if (!skipCount) {
9772
+ totalItems = await this.count({ partition, partitionValues });
9773
+ totalPages = Math.ceil(totalItems / size);
9774
+ }
9775
+ const page = Math.floor(offset / size);
9776
+ const items = await this.list({
9777
+ partition,
9778
+ partitionValues,
9779
+ limit: size,
9780
+ offset
9781
+ });
9518
9782
  const result = {
9519
9783
  items,
9520
9784
  totalItems,
9521
- page: offset,
9785
+ page,
9522
9786
  pageSize: size,
9523
- totalPages
9787
+ totalPages,
9788
+ // Add additional metadata for debugging
9789
+ _debug: {
9790
+ requestedSize: size,
9791
+ requestedOffset: offset,
9792
+ actualItemsReturned: items.length,
9793
+ skipCount,
9794
+ hasTotalItems: totalItems !== null
9795
+ }
9524
9796
  };
9525
9797
  this.emit("page", result);
9526
9798
  return result;
@@ -9534,36 +9806,62 @@ class Resource extends EventEmitter {
9534
9806
  return stream.build();
9535
9807
  }
9536
9808
  /**
9537
- * Store binary content associated with a resource
9538
- * @param {string} id - Resource ID
9539
- * @param {Buffer} buffer - Binary content
9540
- * @param {string} contentType - Optional content type
9809
+ * Set binary content for a resource
9810
+ * @param {Object} params - Content parameters
9811
+ * @param {string} params.id - Resource ID
9812
+ * @param {Buffer|string} params.buffer - Content buffer or string
9813
+ * @param {string} [params.contentType='application/octet-stream'] - Content type
9814
+ * @returns {Promise<Object>} Updated resource data
9815
+ * @example
9816
+ * // Set image content
9817
+ * const imageBuffer = fs.readFileSync('image.jpg');
9818
+ * await resource.setContent({
9819
+ * id: 'user-123',
9820
+ * buffer: imageBuffer,
9821
+ * contentType: 'image/jpeg'
9822
+ * });
9823
+ *
9824
+ * // Set text content
9825
+ * await resource.setContent({
9826
+ * id: 'document-456',
9827
+ * buffer: 'Hello World',
9828
+ * contentType: 'text/plain'
9829
+ * });
9541
9830
  */
9542
- async setContent(id, buffer, contentType = "application/octet-stream") {
9543
- if (!Buffer.isBuffer(buffer)) {
9544
- throw new Error("Content must be a Buffer");
9545
- }
9546
- const key = this.getResourceKey(id);
9547
- let existingMetadata = {};
9548
- try {
9549
- const existingObject = await this.client.headObject(key);
9550
- existingMetadata = existingObject.Metadata || {};
9551
- } catch (error) {
9552
- }
9553
- const response = await this.client.putObject({
9554
- key,
9831
+ async setContent({ id, buffer, contentType = "application/octet-stream" }) {
9832
+ const currentData = await this.get(id);
9833
+ if (!currentData) {
9834
+ throw new Error(`Resource with id '${id}' not found`);
9835
+ }
9836
+ const updatedData = {
9837
+ ...currentData,
9838
+ _hasContent: true,
9839
+ _contentLength: buffer.length,
9840
+ _mimeType: contentType
9841
+ };
9842
+ await this.client.putObject({
9843
+ key: this.getResourceKey(id),
9844
+ metadata: await this.schema.mapper(updatedData),
9555
9845
  body: buffer,
9556
- contentType,
9557
- metadata: existingMetadata
9558
- // Preserve existing metadata
9846
+ contentType
9559
9847
  });
9560
- this.emit("setContent", id, buffer.length, contentType);
9561
- return response;
9848
+ this.emit("setContent", { id, contentType, contentLength: buffer.length });
9849
+ return updatedData;
9562
9850
  }
9563
9851
  /**
9564
9852
  * Retrieve binary content associated with a resource
9565
9853
  * @param {string} id - Resource ID
9566
- * @returns {Object} Object with buffer and contentType
9854
+ * @returns {Promise<Object>} Object with buffer and contentType
9855
+ * @example
9856
+ * const content = await resource.content('user-123');
9857
+ * if (content.buffer) {
9858
+ * console.log('Content type:', content.contentType);
9859
+ * console.log('Content size:', content.buffer.length);
9860
+ * // Save to file
9861
+ * fs.writeFileSync('output.jpg', content.buffer);
9862
+ * } else {
9863
+ * console.log('No content found');
9864
+ * }
9567
9865
  */
9568
9866
  async content(id) {
9569
9867
  const key = this.getResourceKey(id);
@@ -9658,7 +9956,7 @@ class Resource extends EventEmitter {
9658
9956
  return;
9659
9957
  }
9660
9958
  for (const [partitionName, partition] of Object.entries(partitions)) {
9661
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
9959
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9662
9960
  if (partitionKey) {
9663
9961
  const mappedData = await this.schema.mapper(data);
9664
9962
  const behaviorImpl = getBehavior(this.behavior);
@@ -9690,7 +9988,7 @@ class Resource extends EventEmitter {
9690
9988
  }
9691
9989
  const keysToDelete = [];
9692
9990
  for (const [partitionName, partition] of Object.entries(partitions)) {
9693
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
9991
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9694
9992
  if (partitionKey) {
9695
9993
  keysToDelete.push(partitionKey);
9696
9994
  }
@@ -9704,20 +10002,72 @@ class Resource extends EventEmitter {
9704
10002
  }
9705
10003
  }
9706
10004
  /**
9707
- * Query documents with simple filtering
9708
- * @param {Object} filter - Filter criteria
9709
- * @returns {Array} Filtered documents
10005
+ * Query resources with simple filtering and pagination
10006
+ * @param {Object} [filter={}] - Filter criteria (exact field matches)
10007
+ * @param {Object} [options] - Query options
10008
+ * @param {number} [options.limit=100] - Maximum number of results
10009
+ * @param {number} [options.offset=0] - Offset for pagination
10010
+ * @param {string} [options.partition] - Partition name to query from
10011
+ * @param {Object} [options.partitionValues] - Partition field values to filter by
10012
+ * @returns {Promise<Object[]>} Array of filtered resource objects
10013
+ * @example
10014
+ * // Query all resources (no filter)
10015
+ * const allUsers = await resource.query();
10016
+ *
10017
+ * // Query with simple filter
10018
+ * const activeUsers = await resource.query({ status: 'active' });
10019
+ *
10020
+ * // Query with multiple filters
10021
+ * const usElectronics = await resource.query({
10022
+ * category: 'electronics',
10023
+ * region: 'US'
10024
+ * });
10025
+ *
10026
+ * // Query with pagination
10027
+ * const firstPage = await resource.query(
10028
+ * { status: 'active' },
10029
+ * { limit: 10, offset: 0 }
10030
+ * );
10031
+ *
10032
+ * // Query within partition
10033
+ * const googleUsers = await resource.query(
10034
+ * { status: 'active' },
10035
+ * {
10036
+ * partition: 'byUtmSource',
10037
+ * partitionValues: { 'utm.source': 'google' },
10038
+ * limit: 5
10039
+ * }
10040
+ * );
9710
10041
  */
9711
- async query(filter = {}) {
9712
- const allDocuments = await this.getAll();
10042
+ async query(filter = {}, { limit = 100, offset = 0, partition = null, partitionValues = {} } = {}) {
9713
10043
  if (Object.keys(filter).length === 0) {
9714
- return allDocuments;
9715
- }
9716
- return allDocuments.filter((doc) => {
9717
- return Object.entries(filter).every(([key, value]) => {
9718
- return doc[key] === value;
10044
+ return await this.list({ partition, partitionValues, limit, offset });
10045
+ }
10046
+ const results = [];
10047
+ let currentOffset = offset;
10048
+ const batchSize = Math.min(limit, 50);
10049
+ while (results.length < limit) {
10050
+ const batch = await this.list({
10051
+ partition,
10052
+ partitionValues,
10053
+ limit: batchSize,
10054
+ offset: currentOffset
9719
10055
  });
9720
- });
10056
+ if (batch.length === 0) {
10057
+ break;
10058
+ }
10059
+ const filteredBatch = batch.filter((doc) => {
10060
+ return Object.entries(filter).every(([key, value]) => {
10061
+ return doc[key] === value;
10062
+ });
10063
+ });
10064
+ results.push(...filteredBatch);
10065
+ currentOffset += batchSize;
10066
+ if (batch.length < batchSize) {
10067
+ break;
10068
+ }
10069
+ }
10070
+ return results.slice(0, limit);
9721
10071
  }
9722
10072
  /**
9723
10073
  * Update partition objects to keep them in sync
@@ -9729,7 +10079,7 @@ class Resource extends EventEmitter {
9729
10079
  return;
9730
10080
  }
9731
10081
  for (const [partitionName, partition] of Object.entries(partitions)) {
9732
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
10082
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9733
10083
  if (partitionKey) {
9734
10084
  const mappedData = await this.schema.mapper(data);
9735
10085
  const behaviorImpl = getBehavior(this.behavior);
@@ -9756,13 +10106,30 @@ class Resource extends EventEmitter {
9756
10106
  }
9757
10107
  }
9758
10108
  /**
9759
- * Get object directly from a specific partition
9760
- * @param {string} id - Resource ID
9761
- * @param {string} partitionName - Name of the partition
9762
- * @param {Object} partitionValues - Values for partition fields
9763
- * @returns {Object} The resource data
10109
+ * Get a resource object directly from a specific partition
10110
+ * @param {Object} params - Partition parameters
10111
+ * @param {string} params.id - Resource ID
10112
+ * @param {string} params.partitionName - Name of the partition
10113
+ * @param {Object} params.partitionValues - Values for partition fields
10114
+ * @returns {Promise<Object>} The resource object with partition metadata
10115
+ * @example
10116
+ * // Get user from UTM source partition
10117
+ * const user = await resource.getFromPartition({
10118
+ * id: 'user-123',
10119
+ * partitionName: 'byUtmSource',
10120
+ * partitionValues: { 'utm.source': 'google' }
10121
+ * });
10122
+ * console.log(user._partition); // 'byUtmSource'
10123
+ * console.log(user._partitionValues); // { 'utm.source': 'google' }
10124
+ *
10125
+ * // Get product from multi-field partition
10126
+ * const product = await resource.getFromPartition({
10127
+ * id: 'product-456',
10128
+ * partitionName: 'byCategoryRegion',
10129
+ * partitionValues: { category: 'electronics', region: 'US' }
10130
+ * });
9764
10131
  */
9765
- async getFromPartition(id, partitionName, partitionValues = {}) {
10132
+ async getFromPartition({ id, partitionName, partitionValues = {} }) {
9766
10133
  const partition = this.options.partitions[partitionName];
9767
10134
  if (!partition) {
9768
10135
  throw new Error(`Partition '${partitionName}' not found`);
@@ -9821,7 +10188,7 @@ class Database extends EventEmitter {
9821
10188
  this.version = "1";
9822
10189
  this.s3dbVersion = (() => {
9823
10190
  try {
9824
- return true ? "4.1.4" : "latest";
10191
+ return true ? "4.1.7" : "latest";
9825
10192
  } catch (e) {
9826
10193
  return "latest";
9827
10194
  }