s3db.js 4.1.6 → 4.1.7

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,6 +9224,16 @@ 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
9239
  const request = await this.client.headObject(key);
@@ -9212,6 +9267,16 @@ class Resource extends EventEmitter {
9212
9267
  this.emit("get", data);
9213
9268
  return data;
9214
9269
  }
9270
+ /**
9271
+ * Check if a resource exists by ID
9272
+ * @param {string} id - Resource ID
9273
+ * @returns {Promise<boolean>} True if resource exists, false otherwise
9274
+ * @example
9275
+ * const exists = await resource.exists('user-123');
9276
+ * if (exists) {
9277
+ * console.log('User exists');
9278
+ * }
9279
+ */
9215
9280
  async exists(id) {
9216
9281
  try {
9217
9282
  const key = this.getResourceKey(id);
@@ -9221,6 +9286,24 @@ class Resource extends EventEmitter {
9221
9286
  return false;
9222
9287
  }
9223
9288
  }
9289
+ /**
9290
+ * Update an existing resource object
9291
+ * @param {string} id - Resource ID
9292
+ * @param {Object} attributes - Attributes to update (partial update supported)
9293
+ * @returns {Promise<Object>} The updated resource object with all attributes
9294
+ * @example
9295
+ * // Update specific fields
9296
+ * const updatedUser = await resource.update('user-123', {
9297
+ * name: 'John Updated',
9298
+ * age: 31
9299
+ * });
9300
+ *
9301
+ * // Update with timestamps (if enabled)
9302
+ * const updatedUser = await resource.update('user-123', {
9303
+ * email: 'newemail@example.com'
9304
+ * });
9305
+ * console.log(updatedUser.updatedAt); // ISO timestamp
9306
+ */
9224
9307
  async update(id, attributes) {
9225
9308
  const live = await this.get(id);
9226
9309
  if (this.options.timestamps) {
@@ -9277,6 +9360,14 @@ class Resource extends EventEmitter {
9277
9360
  this.emit("update", preProcessedData, validated);
9278
9361
  return validated;
9279
9362
  }
9363
+ /**
9364
+ * Delete a resource object by ID
9365
+ * @param {string} id - Resource ID
9366
+ * @returns {Promise<Object>} S3 delete response
9367
+ * @example
9368
+ * await resource.delete('user-123');
9369
+ * console.log('User deleted successfully');
9370
+ */
9280
9371
  async delete(id) {
9281
9372
  let objectData;
9282
9373
  try {
@@ -9291,6 +9382,20 @@ class Resource extends EventEmitter {
9291
9382
  this.emit("delete", id);
9292
9383
  return response;
9293
9384
  }
9385
+ /**
9386
+ * Insert or update a resource object (upsert operation)
9387
+ * @param {Object} params - Upsert parameters
9388
+ * @param {string} params.id - Resource ID (required for upsert)
9389
+ * @param {...Object} params - Resource attributes (any additional properties)
9390
+ * @returns {Promise<Object>} The inserted or updated resource object
9391
+ * @example
9392
+ * // Will insert if doesn't exist, update if exists
9393
+ * const user = await resource.upsert({
9394
+ * id: 'user-123',
9395
+ * name: 'John Doe',
9396
+ * email: 'john@example.com'
9397
+ * });
9398
+ */
9294
9399
  async upsert({ id, ...attributes }) {
9295
9400
  const exists = await this.exists(id);
9296
9401
  if (exists) {
@@ -9298,6 +9403,28 @@ class Resource extends EventEmitter {
9298
9403
  }
9299
9404
  return this.insert({ id, ...attributes });
9300
9405
  }
9406
+ /**
9407
+ * Count resources with optional partition filtering
9408
+ * @param {Object} [params] - Count parameters
9409
+ * @param {string} [params.partition] - Partition name to count in
9410
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9411
+ * @returns {Promise<number>} Total count of matching resources
9412
+ * @example
9413
+ * // Count all resources
9414
+ * const total = await resource.count();
9415
+ *
9416
+ * // Count in specific partition
9417
+ * const googleUsers = await resource.count({
9418
+ * partition: 'byUtmSource',
9419
+ * partitionValues: { 'utm.source': 'google' }
9420
+ * });
9421
+ *
9422
+ * // Count in multi-field partition
9423
+ * const usElectronics = await resource.count({
9424
+ * partition: 'byCategoryRegion',
9425
+ * partitionValues: { category: 'electronics', region: 'US' }
9426
+ * });
9427
+ */
9301
9428
  async count({ partition = null, partitionValues = {} } = {}) {
9302
9429
  let prefix;
9303
9430
  if (partition && Object.keys(partitionValues).length > 0) {
@@ -9328,6 +9455,19 @@ class Resource extends EventEmitter {
9328
9455
  this.emit("count", count);
9329
9456
  return count;
9330
9457
  }
9458
+ /**
9459
+ * Insert multiple resources in parallel
9460
+ * @param {Object[]} objects - Array of resource objects to insert
9461
+ * @returns {Promise<Object[]>} Array of inserted resource objects
9462
+ * @example
9463
+ * const users = [
9464
+ * { name: 'John', email: 'john@example.com' },
9465
+ * { name: 'Jane', email: 'jane@example.com' },
9466
+ * { name: 'Bob', email: 'bob@example.com' }
9467
+ * ];
9468
+ * const insertedUsers = await resource.insertMany(users);
9469
+ * console.log(`Inserted ${insertedUsers.length} users`);
9470
+ */
9331
9471
  async insertMany(objects) {
9332
9472
  const { results } = await promisePool.PromisePool.for(objects).withConcurrency(this.parallelism).handleError(async (error, content) => {
9333
9473
  this.emit("error", error, content);
@@ -9339,6 +9479,15 @@ class Resource extends EventEmitter {
9339
9479
  this.emit("insertMany", objects.length);
9340
9480
  return results;
9341
9481
  }
9482
+ /**
9483
+ * Delete multiple resources by their IDs in parallel
9484
+ * @param {string[]} ids - Array of resource IDs to delete
9485
+ * @returns {Promise<Object[]>} Array of S3 delete responses
9486
+ * @example
9487
+ * const deletedIds = ['user-1', 'user-2', 'user-3'];
9488
+ * const results = await resource.deleteMany(deletedIds);
9489
+ * console.log(`Deleted ${deletedIds.length} users`);
9490
+ */
9342
9491
  async deleteMany(ids) {
9343
9492
  const packages = lodashEs.chunk(
9344
9493
  ids.map((id) => this.getResourceKey(id)),
@@ -9397,6 +9546,28 @@ class Resource extends EventEmitter {
9397
9546
  });
9398
9547
  return { deletedCount, resource: this.name };
9399
9548
  }
9549
+ /**
9550
+ * List resource IDs with optional partition filtering
9551
+ * @param {Object} [params] - List parameters
9552
+ * @param {string} [params.partition] - Partition name to list from
9553
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9554
+ * @returns {Promise<string[]>} Array of resource IDs (strings)
9555
+ * @example
9556
+ * // List all IDs
9557
+ * const allIds = await resource.listIds();
9558
+ *
9559
+ * // List IDs from specific partition
9560
+ * const googleUserIds = await resource.listIds({
9561
+ * partition: 'byUtmSource',
9562
+ * partitionValues: { 'utm.source': 'google' }
9563
+ * });
9564
+ *
9565
+ * // List IDs from multi-field partition
9566
+ * const usElectronicsIds = await resource.listIds({
9567
+ * partition: 'byCategoryRegion',
9568
+ * partitionValues: { category: 'electronics', region: 'US' }
9569
+ * });
9570
+ */
9400
9571
  async listIds({ partition = null, partitionValues = {} } = {}) {
9401
9572
  let prefix;
9402
9573
  if (partition && Object.keys(partitionValues).length > 0) {
@@ -9433,13 +9604,36 @@ class Resource extends EventEmitter {
9433
9604
  return ids;
9434
9605
  }
9435
9606
  /**
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
9607
+ * List resource objects with optional partition filtering and pagination
9608
+ * @param {Object} [params] - List parameters
9609
+ * @param {string} [params.partition] - Partition name to list from
9610
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9611
+ * @param {number} [params.limit] - Maximum number of results to return
9612
+ * @param {number} [params.offset=0] - Offset for pagination
9613
+ * @returns {Promise<Object[]>} Array of resource objects with all attributes
9614
+ * @example
9615
+ * // List all resources
9616
+ * const allUsers = await resource.list();
9617
+ *
9618
+ * // List with pagination
9619
+ * const firstPage = await resource.list({ limit: 10, offset: 0 });
9620
+ * const secondPage = await resource.list({ limit: 10, offset: 10 });
9621
+ *
9622
+ * // List from specific partition
9623
+ * const googleUsers = await resource.list({
9624
+ * partition: 'byUtmSource',
9625
+ * partitionValues: { 'utm.source': 'google' }
9626
+ * });
9627
+ *
9628
+ * // List from partition with pagination
9629
+ * const googleUsersPage = await resource.list({
9630
+ * partition: 'byUtmSource',
9631
+ * partitionValues: { 'utm.source': 'google' },
9632
+ * limit: 5,
9633
+ * offset: 0
9634
+ * });
9440
9635
  */
9441
- async listByPartition({ partition = null, partitionValues = {} } = {}, options = {}) {
9442
- const { limit, offset = 0 } = options;
9636
+ async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9443
9637
  if (!partition) {
9444
9638
  const ids2 = await this.listIds({ partition, partitionValues });
9445
9639
  let filteredIds2 = ids2.slice(offset);
@@ -9449,7 +9643,7 @@ class Resource extends EventEmitter {
9449
9643
  const { results: results2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).process(async (id) => {
9450
9644
  return await this.get(id);
9451
9645
  });
9452
- this.emit("listByPartition", { partition, partitionValues, count: results2.length });
9646
+ this.emit("list", { partition, partitionValues, count: results2.length });
9453
9647
  return results2;
9454
9648
  }
9455
9649
  const partitionDef = this.options.partitions[partition];
@@ -9482,11 +9676,19 @@ class Resource extends EventEmitter {
9482
9676
  filteredIds = filteredIds.slice(0, limit);
9483
9677
  }
9484
9678
  const { results } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).process(async (id) => {
9485
- return await this.getFromPartition(id, partition, partitionValues);
9679
+ return await this.getFromPartition({ id, partitionName: partition, partitionValues });
9486
9680
  });
9487
- this.emit("listByPartition", { partition, partitionValues, count: results.length });
9681
+ this.emit("list", { partition, partitionValues, count: results.length });
9488
9682
  return results;
9489
9683
  }
9684
+ /**
9685
+ * Get multiple resources by their IDs
9686
+ * @param {string[]} ids - Array of resource IDs
9687
+ * @returns {Promise<Object[]>} Array of resource objects
9688
+ * @example
9689
+ * const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
9690
+ * users.forEach(user => console.log(user.name));
9691
+ */
9490
9692
  async getMany(ids) {
9491
9693
  const { results } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).process(async (id) => {
9492
9694
  this.emit("id", id);
@@ -9497,6 +9699,13 @@ class Resource extends EventEmitter {
9497
9699
  this.emit("getMany", ids.length);
9498
9700
  return results;
9499
9701
  }
9702
+ /**
9703
+ * Get all resources (equivalent to list() without pagination)
9704
+ * @returns {Promise<Object[]>} Array of all resource objects
9705
+ * @example
9706
+ * const allUsers = await resource.getAll();
9707
+ * console.log(`Total users: ${allUsers.length}`);
9708
+ */
9500
9709
  async getAll() {
9501
9710
  let ids = await this.listIds();
9502
9711
  if (ids.length === 0) return [];
@@ -9507,18 +9716,41 @@ class Resource extends EventEmitter {
9507
9716
  this.emit("getAll", results.length);
9508
9717
  return results;
9509
9718
  }
9510
- async page(offset = 0, size = 100, { partition = null, partitionValues = {} } = {}) {
9511
- const allIds = await this.listIds({ partition, partitionValues });
9512
- const totalItems = allIds.length;
9719
+ /**
9720
+ * Get a page of resources with pagination metadata
9721
+ * @param {Object} [params] - Page parameters
9722
+ * @param {number} [params.offset=0] - Offset for pagination
9723
+ * @param {number} [params.size=100] - Page size
9724
+ * @param {string} [params.partition] - Partition name to page from
9725
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9726
+ * @returns {Promise<Object>} Page result with items and pagination info
9727
+ * @example
9728
+ * // Get first page of all resources
9729
+ * const page = await resource.page({ offset: 0, size: 10 });
9730
+ * console.log(`Page ${page.page + 1} of ${page.totalPages}`);
9731
+ * console.log(`Showing ${page.items.length} of ${page.totalItems} total`);
9732
+ *
9733
+ * // Get page from specific partition
9734
+ * const googlePage = await resource.page({
9735
+ * partition: 'byUtmSource',
9736
+ * partitionValues: { 'utm.source': 'google' },
9737
+ * offset: 0,
9738
+ * size: 5
9739
+ * });
9740
+ */
9741
+ async page({ offset = 0, size = 100, partition = null, partitionValues = {} } = {}) {
9742
+ const ids = await this.listIds({ partition, partitionValues });
9743
+ const totalItems = ids.length;
9513
9744
  const totalPages = Math.ceil(totalItems / size);
9514
- const paginatedIds = allIds.slice(offset * size, (offset + 1) * size);
9745
+ const page = Math.floor(offset / size);
9746
+ const pageIds = ids.slice(offset, offset + size);
9515
9747
  const items = await Promise.all(
9516
- paginatedIds.map((id) => this.get(id))
9748
+ pageIds.map((id) => this.get(id))
9517
9749
  );
9518
9750
  const result = {
9519
9751
  items,
9520
9752
  totalItems,
9521
- page: offset,
9753
+ page,
9522
9754
  pageSize: size,
9523
9755
  totalPages
9524
9756
  };
@@ -9534,36 +9766,62 @@ class Resource extends EventEmitter {
9534
9766
  return stream.build();
9535
9767
  }
9536
9768
  /**
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
9769
+ * Set binary content for a resource
9770
+ * @param {Object} params - Content parameters
9771
+ * @param {string} params.id - Resource ID
9772
+ * @param {Buffer|string} params.buffer - Content buffer or string
9773
+ * @param {string} [params.contentType='application/octet-stream'] - Content type
9774
+ * @returns {Promise<Object>} Updated resource data
9775
+ * @example
9776
+ * // Set image content
9777
+ * const imageBuffer = fs.readFileSync('image.jpg');
9778
+ * await resource.setContent({
9779
+ * id: 'user-123',
9780
+ * buffer: imageBuffer,
9781
+ * contentType: 'image/jpeg'
9782
+ * });
9783
+ *
9784
+ * // Set text content
9785
+ * await resource.setContent({
9786
+ * id: 'document-456',
9787
+ * buffer: 'Hello World',
9788
+ * contentType: 'text/plain'
9789
+ * });
9541
9790
  */
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,
9791
+ async setContent({ id, buffer, contentType = "application/octet-stream" }) {
9792
+ const currentData = await this.get(id);
9793
+ if (!currentData) {
9794
+ throw new Error(`Resource with id '${id}' not found`);
9795
+ }
9796
+ const updatedData = {
9797
+ ...currentData,
9798
+ _hasContent: true,
9799
+ _contentLength: buffer.length,
9800
+ _mimeType: contentType
9801
+ };
9802
+ await this.client.putObject({
9803
+ key: this.getResourceKey(id),
9804
+ metadata: await this.schema.mapper(updatedData),
9555
9805
  body: buffer,
9556
- contentType,
9557
- metadata: existingMetadata
9558
- // Preserve existing metadata
9806
+ contentType
9559
9807
  });
9560
- this.emit("setContent", id, buffer.length, contentType);
9561
- return response;
9808
+ this.emit("setContent", { id, contentType, contentLength: buffer.length });
9809
+ return updatedData;
9562
9810
  }
9563
9811
  /**
9564
9812
  * Retrieve binary content associated with a resource
9565
9813
  * @param {string} id - Resource ID
9566
- * @returns {Object} Object with buffer and contentType
9814
+ * @returns {Promise<Object>} Object with buffer and contentType
9815
+ * @example
9816
+ * const content = await resource.content('user-123');
9817
+ * if (content.buffer) {
9818
+ * console.log('Content type:', content.contentType);
9819
+ * console.log('Content size:', content.buffer.length);
9820
+ * // Save to file
9821
+ * fs.writeFileSync('output.jpg', content.buffer);
9822
+ * } else {
9823
+ * console.log('No content found');
9824
+ * }
9567
9825
  */
9568
9826
  async content(id) {
9569
9827
  const key = this.getResourceKey(id);
@@ -9658,7 +9916,7 @@ class Resource extends EventEmitter {
9658
9916
  return;
9659
9917
  }
9660
9918
  for (const [partitionName, partition] of Object.entries(partitions)) {
9661
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
9919
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9662
9920
  if (partitionKey) {
9663
9921
  const mappedData = await this.schema.mapper(data);
9664
9922
  const behaviorImpl = getBehavior(this.behavior);
@@ -9690,7 +9948,7 @@ class Resource extends EventEmitter {
9690
9948
  }
9691
9949
  const keysToDelete = [];
9692
9950
  for (const [partitionName, partition] of Object.entries(partitions)) {
9693
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
9951
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9694
9952
  if (partitionKey) {
9695
9953
  keysToDelete.push(partitionKey);
9696
9954
  }
@@ -9704,20 +9962,72 @@ class Resource extends EventEmitter {
9704
9962
  }
9705
9963
  }
9706
9964
  /**
9707
- * Query documents with simple filtering
9708
- * @param {Object} filter - Filter criteria
9709
- * @returns {Array} Filtered documents
9965
+ * Query resources with simple filtering and pagination
9966
+ * @param {Object} [filter={}] - Filter criteria (exact field matches)
9967
+ * @param {Object} [options] - Query options
9968
+ * @param {number} [options.limit=100] - Maximum number of results
9969
+ * @param {number} [options.offset=0] - Offset for pagination
9970
+ * @param {string} [options.partition] - Partition name to query from
9971
+ * @param {Object} [options.partitionValues] - Partition field values to filter by
9972
+ * @returns {Promise<Object[]>} Array of filtered resource objects
9973
+ * @example
9974
+ * // Query all resources (no filter)
9975
+ * const allUsers = await resource.query();
9976
+ *
9977
+ * // Query with simple filter
9978
+ * const activeUsers = await resource.query({ status: 'active' });
9979
+ *
9980
+ * // Query with multiple filters
9981
+ * const usElectronics = await resource.query({
9982
+ * category: 'electronics',
9983
+ * region: 'US'
9984
+ * });
9985
+ *
9986
+ * // Query with pagination
9987
+ * const firstPage = await resource.query(
9988
+ * { status: 'active' },
9989
+ * { limit: 10, offset: 0 }
9990
+ * );
9991
+ *
9992
+ * // Query within partition
9993
+ * const googleUsers = await resource.query(
9994
+ * { status: 'active' },
9995
+ * {
9996
+ * partition: 'byUtmSource',
9997
+ * partitionValues: { 'utm.source': 'google' },
9998
+ * limit: 5
9999
+ * }
10000
+ * );
9710
10001
  */
9711
- async query(filter = {}) {
9712
- const allDocuments = await this.getAll();
10002
+ async query(filter = {}, { limit = 100, offset = 0, partition = null, partitionValues = {} } = {}) {
9713
10003
  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;
10004
+ return await this.list({ partition, partitionValues, limit, offset });
10005
+ }
10006
+ const results = [];
10007
+ let currentOffset = offset;
10008
+ const batchSize = Math.min(limit, 50);
10009
+ while (results.length < limit) {
10010
+ const batch = await this.list({
10011
+ partition,
10012
+ partitionValues,
10013
+ limit: batchSize,
10014
+ offset: currentOffset
9719
10015
  });
9720
- });
10016
+ if (batch.length === 0) {
10017
+ break;
10018
+ }
10019
+ const filteredBatch = batch.filter((doc) => {
10020
+ return Object.entries(filter).every(([key, value]) => {
10021
+ return doc[key] === value;
10022
+ });
10023
+ });
10024
+ results.push(...filteredBatch);
10025
+ currentOffset += batchSize;
10026
+ if (batch.length < batchSize) {
10027
+ break;
10028
+ }
10029
+ }
10030
+ return results.slice(0, limit);
9721
10031
  }
9722
10032
  /**
9723
10033
  * Update partition objects to keep them in sync
@@ -9729,7 +10039,7 @@ class Resource extends EventEmitter {
9729
10039
  return;
9730
10040
  }
9731
10041
  for (const [partitionName, partition] of Object.entries(partitions)) {
9732
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
10042
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9733
10043
  if (partitionKey) {
9734
10044
  const mappedData = await this.schema.mapper(data);
9735
10045
  const behaviorImpl = getBehavior(this.behavior);
@@ -9756,13 +10066,30 @@ class Resource extends EventEmitter {
9756
10066
  }
9757
10067
  }
9758
10068
  /**
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
10069
+ * Get a resource object directly from a specific partition
10070
+ * @param {Object} params - Partition parameters
10071
+ * @param {string} params.id - Resource ID
10072
+ * @param {string} params.partitionName - Name of the partition
10073
+ * @param {Object} params.partitionValues - Values for partition fields
10074
+ * @returns {Promise<Object>} The resource object with partition metadata
10075
+ * @example
10076
+ * // Get user from UTM source partition
10077
+ * const user = await resource.getFromPartition({
10078
+ * id: 'user-123',
10079
+ * partitionName: 'byUtmSource',
10080
+ * partitionValues: { 'utm.source': 'google' }
10081
+ * });
10082
+ * console.log(user._partition); // 'byUtmSource'
10083
+ * console.log(user._partitionValues); // { 'utm.source': 'google' }
10084
+ *
10085
+ * // Get product from multi-field partition
10086
+ * const product = await resource.getFromPartition({
10087
+ * id: 'product-456',
10088
+ * partitionName: 'byCategoryRegion',
10089
+ * partitionValues: { category: 'electronics', region: 'US' }
10090
+ * });
9764
10091
  */
9765
- async getFromPartition(id, partitionName, partitionValues = {}) {
10092
+ async getFromPartition({ id, partitionName, partitionValues = {} }) {
9766
10093
  const partition = this.options.partitions[partitionName];
9767
10094
  if (!partition) {
9768
10095
  throw new Error(`Partition '${partitionName}' not found`);
@@ -9821,7 +10148,7 @@ class Database extends EventEmitter {
9821
10148
  this.version = "1";
9822
10149
  this.s3dbVersion = (() => {
9823
10150
  try {
9824
- return true ? "4.1.4" : "latest";
10151
+ return true ? "4.1.6" : "latest";
9825
10152
  } catch (e) {
9826
10153
  return "latest";
9827
10154
  }