s3db.js 4.1.5 → 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.iife.js CHANGED
@@ -8560,6 +8560,14 @@ ${JSON.stringify(validation, null, 2)}`
8560
8560
  }
8561
8561
  return bytes;
8562
8562
  }
8563
+ function calculateAttributeNamesSize(mappedObject) {
8564
+ let totalSize = 0;
8565
+ for (const key of Object.keys(mappedObject)) {
8566
+ if (key === "_v") continue;
8567
+ totalSize += calculateUTF8Bytes(key);
8568
+ }
8569
+ return totalSize;
8570
+ }
8563
8571
  function transformValue(value) {
8564
8572
  if (value === null || value === void 0) {
8565
8573
  return "";
@@ -8594,49 +8602,31 @@ ${JSON.stringify(validation, null, 2)}`
8594
8602
  return sizes;
8595
8603
  }
8596
8604
  function calculateTotalSize(mappedObject) {
8597
- const sizes = calculateAttributeSizes(mappedObject);
8598
- return Object.values(sizes).reduce((total, size) => total + size, 0);
8605
+ const valueSizes = calculateAttributeSizes(mappedObject);
8606
+ const valueTotal = Object.values(valueSizes).reduce((total, size) => total + size, 0);
8607
+ const namesSize = calculateAttributeNamesSize(mappedObject);
8608
+ return valueTotal + namesSize;
8599
8609
  }
8600
8610
 
8601
- const S3_METADATA_LIMIT_BYTES$3 = 2e3;
8611
+ const S3_METADATA_LIMIT_BYTES = 2048;
8602
8612
  async function handleInsert$3({ resource, data, mappedData }) {
8603
8613
  const totalSize = calculateTotalSize(mappedData);
8604
- if (totalSize > S3_METADATA_LIMIT_BYTES$3) {
8605
- resource.emit("exceedsLimit", {
8606
- operation: "insert",
8607
- totalSize,
8608
- limit: S3_METADATA_LIMIT_BYTES$3,
8609
- excess: totalSize - S3_METADATA_LIMIT_BYTES$3,
8610
- data
8611
- });
8614
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8615
+ throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
8612
8616
  }
8613
8617
  return { mappedData, body: "" };
8614
8618
  }
8615
8619
  async function handleUpdate$3({ resource, id, data, mappedData }) {
8616
8620
  const totalSize = calculateTotalSize(mappedData);
8617
- if (totalSize > S3_METADATA_LIMIT_BYTES$3) {
8618
- resource.emit("exceedsLimit", {
8619
- operation: "update",
8620
- id,
8621
- totalSize,
8622
- limit: S3_METADATA_LIMIT_BYTES$3,
8623
- excess: totalSize - S3_METADATA_LIMIT_BYTES$3,
8624
- data
8625
- });
8621
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8622
+ throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
8626
8623
  }
8627
8624
  return { mappedData, body: "" };
8628
8625
  }
8629
8626
  async function handleUpsert$3({ resource, id, data, mappedData }) {
8630
8627
  const totalSize = calculateTotalSize(mappedData);
8631
- if (totalSize > S3_METADATA_LIMIT_BYTES$3) {
8632
- resource.emit("exceedsLimit", {
8633
- operation: "upsert",
8634
- id,
8635
- totalSize,
8636
- limit: S3_METADATA_LIMIT_BYTES$3,
8637
- excess: totalSize - S3_METADATA_LIMIT_BYTES$3,
8638
- data
8639
- });
8628
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8629
+ throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
8640
8630
  }
8641
8631
  return { mappedData, body: "" };
8642
8632
  }
@@ -8644,33 +8634,53 @@ ${JSON.stringify(validation, null, 2)}`
8644
8634
  return { metadata, body };
8645
8635
  }
8646
8636
 
8647
- var userManagement = /*#__PURE__*/Object.freeze({
8637
+ var enforceLimits = /*#__PURE__*/Object.freeze({
8648
8638
  __proto__: null,
8639
+ S3_METADATA_LIMIT_BYTES: S3_METADATA_LIMIT_BYTES,
8649
8640
  handleGet: handleGet$3,
8650
8641
  handleInsert: handleInsert$3,
8651
8642
  handleUpdate: handleUpdate$3,
8652
8643
  handleUpsert: handleUpsert$3
8653
8644
  });
8654
8645
 
8655
- const S3_METADATA_LIMIT_BYTES$2 = 2e3;
8656
8646
  async function handleInsert$2({ resource, data, mappedData }) {
8657
8647
  const totalSize = calculateTotalSize(mappedData);
8658
- if (totalSize > S3_METADATA_LIMIT_BYTES$2) {
8659
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES$2} bytes`);
8648
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8649
+ resource.emit("exceedsLimit", {
8650
+ operation: "insert",
8651
+ totalSize,
8652
+ limit: S3_METADATA_LIMIT_BYTES,
8653
+ excess: totalSize - S3_METADATA_LIMIT_BYTES,
8654
+ data
8655
+ });
8660
8656
  }
8661
8657
  return { mappedData, body: "" };
8662
8658
  }
8663
8659
  async function handleUpdate$2({ resource, id, data, mappedData }) {
8664
8660
  const totalSize = calculateTotalSize(mappedData);
8665
- if (totalSize > S3_METADATA_LIMIT_BYTES$2) {
8666
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES$2} bytes`);
8661
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8662
+ resource.emit("exceedsLimit", {
8663
+ operation: "update",
8664
+ id,
8665
+ totalSize,
8666
+ limit: S3_METADATA_LIMIT_BYTES,
8667
+ excess: totalSize - S3_METADATA_LIMIT_BYTES,
8668
+ data
8669
+ });
8667
8670
  }
8668
8671
  return { mappedData, body: "" };
8669
8672
  }
8670
8673
  async function handleUpsert$2({ resource, id, data, mappedData }) {
8671
8674
  const totalSize = calculateTotalSize(mappedData);
8672
- if (totalSize > S3_METADATA_LIMIT_BYTES$2) {
8673
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES$2} bytes`);
8675
+ if (totalSize > S3_METADATA_LIMIT_BYTES) {
8676
+ resource.emit("exceedsLimit", {
8677
+ operation: "upsert",
8678
+ id,
8679
+ totalSize,
8680
+ limit: S3_METADATA_LIMIT_BYTES,
8681
+ excess: totalSize - S3_METADATA_LIMIT_BYTES,
8682
+ data
8683
+ });
8674
8684
  }
8675
8685
  return { mappedData, body: "" };
8676
8686
  }
@@ -8678,7 +8688,7 @@ ${JSON.stringify(validation, null, 2)}`
8678
8688
  return { metadata, body };
8679
8689
  }
8680
8690
 
8681
- var enforceLimits = /*#__PURE__*/Object.freeze({
8691
+ var userManagement = /*#__PURE__*/Object.freeze({
8682
8692
  __proto__: null,
8683
8693
  handleGet: handleGet$2,
8684
8694
  handleInsert: handleInsert$2,
@@ -8686,7 +8696,6 @@ ${JSON.stringify(validation, null, 2)}`
8686
8696
  handleUpsert: handleUpsert$2
8687
8697
  });
8688
8698
 
8689
- const S3_METADATA_LIMIT_BYTES$1 = 2e3;
8690
8699
  const TRUNCATE_SUFFIX = "...";
8691
8700
  const TRUNCATE_SUFFIX_BYTES = calculateUTF8Bytes(TRUNCATE_SUFFIX);
8692
8701
  async function handleInsert$1({ resource, data, mappedData }) {
@@ -8707,7 +8716,7 @@ ${JSON.stringify(validation, null, 2)}`
8707
8716
  const result = {};
8708
8717
  let currentSize = 0;
8709
8718
  for (const [key, size] of sortedAttributes) {
8710
- const availableSpace = S3_METADATA_LIMIT_BYTES$1 - currentSize;
8719
+ const availableSpace = S3_METADATA_LIMIT_BYTES - currentSize;
8711
8720
  if (size <= availableSpace) {
8712
8721
  result[key] = mappedData[key];
8713
8722
  currentSize += size;
@@ -8727,7 +8736,7 @@ ${JSON.stringify(validation, null, 2)}`
8727
8736
  }
8728
8737
  }
8729
8738
  result[key] = truncatedValue + TRUNCATE_SUFFIX;
8730
- currentSize = S3_METADATA_LIMIT_BYTES$1;
8739
+ currentSize = S3_METADATA_LIMIT_BYTES;
8731
8740
  break;
8732
8741
  } else {
8733
8742
  break;
@@ -8744,7 +8753,6 @@ ${JSON.stringify(validation, null, 2)}`
8744
8753
  handleUpsert: handleUpsert$1
8745
8754
  });
8746
8755
 
8747
- const S3_METADATA_LIMIT_BYTES = 2e3;
8748
8756
  const OVERFLOW_FLAG = "$overflow";
8749
8757
  const OVERFLOW_FLAG_VALUE = "true";
8750
8758
  const OVERFLOW_FLAG_BYTES = calculateUTF8Bytes(OVERFLOW_FLAG) + calculateUTF8Bytes(OVERFLOW_FLAG_VALUE);
@@ -9087,13 +9095,29 @@ ${JSON.stringify(validation, null, 2)}`
9087
9095
  return join(`resource=${this.name}`, `v=${this.version}`, `id=${id}`);
9088
9096
  }
9089
9097
  /**
9090
- * Get partition reference key for a specific partition
9091
- * @param {string} partitionName - Name of the partition
9092
- * @param {string} id - Resource ID
9093
- * @param {Object} data - Data object for partition value generation
9094
- * @returns {string|null} The partition reference S3 key path
9098
+ * Generate partition key for a resource in a specific partition
9099
+ * @param {Object} params - Partition key parameters
9100
+ * @param {string} params.partitionName - Name of the partition
9101
+ * @param {string} params.id - Resource ID
9102
+ * @param {Object} params.data - Resource data for partition value extraction
9103
+ * @returns {string|null} The partition key path or null if required fields are missing
9104
+ * @example
9105
+ * const partitionKey = resource.getPartitionKey({
9106
+ * partitionName: 'byUtmSource',
9107
+ * id: 'user-123',
9108
+ * data: { utm: { source: 'google' } }
9109
+ * });
9110
+ * // Returns: 'resource=users/partition=byUtmSource/utm.source=google/id=user-123'
9111
+ *
9112
+ * // Returns null if required field is missing
9113
+ * const nullKey = resource.getPartitionKey({
9114
+ * partitionName: 'byUtmSource',
9115
+ * id: 'user-123',
9116
+ * data: { name: 'John' } // Missing utm.source
9117
+ * });
9118
+ * // Returns: null
9095
9119
  */
9096
- getPartitionKey(partitionName, id, data) {
9120
+ getPartitionKey({ partitionName, id, data }) {
9097
9121
  const partition = this.options.partitions[partitionName];
9098
9122
  if (!partition) {
9099
9123
  throw new Error(`Partition '${partitionName}' not found`);
@@ -9124,15 +9148,36 @@ ${JSON.stringify(validation, null, 2)}`
9124
9148
  return data[fieldPath];
9125
9149
  }
9126
9150
  const keys = fieldPath.split(".");
9127
- let value = data;
9151
+ let currentLevel = data;
9128
9152
  for (const key of keys) {
9129
- if (value === null || value === void 0 || typeof value !== "object") {
9153
+ if (!currentLevel || typeof currentLevel !== "object" || !(key in currentLevel)) {
9130
9154
  return void 0;
9131
9155
  }
9132
- value = value[key];
9156
+ currentLevel = currentLevel[key];
9133
9157
  }
9134
- return value;
9158
+ return currentLevel;
9135
9159
  }
9160
+ /**
9161
+ * Insert a new resource object
9162
+ * @param {Object} params - Insert parameters
9163
+ * @param {string} [params.id] - Resource ID (auto-generated if not provided)
9164
+ * @param {...Object} params - Resource attributes (any additional properties)
9165
+ * @returns {Promise<Object>} The inserted resource object with all attributes and generated ID
9166
+ * @example
9167
+ * // Insert with auto-generated ID
9168
+ * const user = await resource.insert({
9169
+ * name: 'John Doe',
9170
+ * email: 'john@example.com',
9171
+ * age: 30
9172
+ * });
9173
+ *
9174
+ * // Insert with custom ID
9175
+ * const user = await resource.insert({
9176
+ * id: 'custom-id-123',
9177
+ * name: 'Jane Smith',
9178
+ * email: 'jane@example.com'
9179
+ * });
9180
+ */
9136
9181
  async insert({ id, ...attributes }) {
9137
9182
  if (this.options.timestamps) {
9138
9183
  attributes.createdAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -9171,6 +9216,16 @@ ${JSON.stringify(validation, null, 2)}`
9171
9216
  this.emit("insert", final);
9172
9217
  return final;
9173
9218
  }
9219
+ /**
9220
+ * Retrieve a resource object by ID
9221
+ * @param {string} id - Resource ID
9222
+ * @returns {Promise<Object>} The resource object with all attributes and metadata
9223
+ * @example
9224
+ * const user = await resource.get('user-123');
9225
+ * console.log(user.name); // 'John Doe'
9226
+ * console.log(user._lastModified); // Date object
9227
+ * console.log(user._hasContent); // boolean
9228
+ */
9174
9229
  async get(id) {
9175
9230
  const key = this.getResourceKey(id);
9176
9231
  const request = await this.client.headObject(key);
@@ -9204,6 +9259,16 @@ ${JSON.stringify(validation, null, 2)}`
9204
9259
  this.emit("get", data);
9205
9260
  return data;
9206
9261
  }
9262
+ /**
9263
+ * Check if a resource exists by ID
9264
+ * @param {string} id - Resource ID
9265
+ * @returns {Promise<boolean>} True if resource exists, false otherwise
9266
+ * @example
9267
+ * const exists = await resource.exists('user-123');
9268
+ * if (exists) {
9269
+ * console.log('User exists');
9270
+ * }
9271
+ */
9207
9272
  async exists(id) {
9208
9273
  try {
9209
9274
  const key = this.getResourceKey(id);
@@ -9213,6 +9278,24 @@ ${JSON.stringify(validation, null, 2)}`
9213
9278
  return false;
9214
9279
  }
9215
9280
  }
9281
+ /**
9282
+ * Update an existing resource object
9283
+ * @param {string} id - Resource ID
9284
+ * @param {Object} attributes - Attributes to update (partial update supported)
9285
+ * @returns {Promise<Object>} The updated resource object with all attributes
9286
+ * @example
9287
+ * // Update specific fields
9288
+ * const updatedUser = await resource.update('user-123', {
9289
+ * name: 'John Updated',
9290
+ * age: 31
9291
+ * });
9292
+ *
9293
+ * // Update with timestamps (if enabled)
9294
+ * const updatedUser = await resource.update('user-123', {
9295
+ * email: 'newemail@example.com'
9296
+ * });
9297
+ * console.log(updatedUser.updatedAt); // ISO timestamp
9298
+ */
9216
9299
  async update(id, attributes) {
9217
9300
  const live = await this.get(id);
9218
9301
  if (this.options.timestamps) {
@@ -9269,6 +9352,14 @@ ${JSON.stringify(validation, null, 2)}`
9269
9352
  this.emit("update", preProcessedData, validated);
9270
9353
  return validated;
9271
9354
  }
9355
+ /**
9356
+ * Delete a resource object by ID
9357
+ * @param {string} id - Resource ID
9358
+ * @returns {Promise<Object>} S3 delete response
9359
+ * @example
9360
+ * await resource.delete('user-123');
9361
+ * console.log('User deleted successfully');
9362
+ */
9272
9363
  async delete(id) {
9273
9364
  let objectData;
9274
9365
  try {
@@ -9283,6 +9374,20 @@ ${JSON.stringify(validation, null, 2)}`
9283
9374
  this.emit("delete", id);
9284
9375
  return response;
9285
9376
  }
9377
+ /**
9378
+ * Insert or update a resource object (upsert operation)
9379
+ * @param {Object} params - Upsert parameters
9380
+ * @param {string} params.id - Resource ID (required for upsert)
9381
+ * @param {...Object} params - Resource attributes (any additional properties)
9382
+ * @returns {Promise<Object>} The inserted or updated resource object
9383
+ * @example
9384
+ * // Will insert if doesn't exist, update if exists
9385
+ * const user = await resource.upsert({
9386
+ * id: 'user-123',
9387
+ * name: 'John Doe',
9388
+ * email: 'john@example.com'
9389
+ * });
9390
+ */
9286
9391
  async upsert({ id, ...attributes }) {
9287
9392
  const exists = await this.exists(id);
9288
9393
  if (exists) {
@@ -9290,6 +9395,28 @@ ${JSON.stringify(validation, null, 2)}`
9290
9395
  }
9291
9396
  return this.insert({ id, ...attributes });
9292
9397
  }
9398
+ /**
9399
+ * Count resources with optional partition filtering
9400
+ * @param {Object} [params] - Count parameters
9401
+ * @param {string} [params.partition] - Partition name to count in
9402
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9403
+ * @returns {Promise<number>} Total count of matching resources
9404
+ * @example
9405
+ * // Count all resources
9406
+ * const total = await resource.count();
9407
+ *
9408
+ * // Count in specific partition
9409
+ * const googleUsers = await resource.count({
9410
+ * partition: 'byUtmSource',
9411
+ * partitionValues: { 'utm.source': 'google' }
9412
+ * });
9413
+ *
9414
+ * // Count in multi-field partition
9415
+ * const usElectronics = await resource.count({
9416
+ * partition: 'byCategoryRegion',
9417
+ * partitionValues: { category: 'electronics', region: 'US' }
9418
+ * });
9419
+ */
9293
9420
  async count({ partition = null, partitionValues = {} } = {}) {
9294
9421
  let prefix;
9295
9422
  if (partition && Object.keys(partitionValues).length > 0) {
@@ -9320,6 +9447,19 @@ ${JSON.stringify(validation, null, 2)}`
9320
9447
  this.emit("count", count);
9321
9448
  return count;
9322
9449
  }
9450
+ /**
9451
+ * Insert multiple resources in parallel
9452
+ * @param {Object[]} objects - Array of resource objects to insert
9453
+ * @returns {Promise<Object[]>} Array of inserted resource objects
9454
+ * @example
9455
+ * const users = [
9456
+ * { name: 'John', email: 'john@example.com' },
9457
+ * { name: 'Jane', email: 'jane@example.com' },
9458
+ * { name: 'Bob', email: 'bob@example.com' }
9459
+ * ];
9460
+ * const insertedUsers = await resource.insertMany(users);
9461
+ * console.log(`Inserted ${insertedUsers.length} users`);
9462
+ */
9323
9463
  async insertMany(objects) {
9324
9464
  const { results } = await promisePool.PromisePool.for(objects).withConcurrency(this.parallelism).handleError(async (error, content) => {
9325
9465
  this.emit("error", error, content);
@@ -9331,6 +9471,15 @@ ${JSON.stringify(validation, null, 2)}`
9331
9471
  this.emit("insertMany", objects.length);
9332
9472
  return results;
9333
9473
  }
9474
+ /**
9475
+ * Delete multiple resources by their IDs in parallel
9476
+ * @param {string[]} ids - Array of resource IDs to delete
9477
+ * @returns {Promise<Object[]>} Array of S3 delete responses
9478
+ * @example
9479
+ * const deletedIds = ['user-1', 'user-2', 'user-3'];
9480
+ * const results = await resource.deleteMany(deletedIds);
9481
+ * console.log(`Deleted ${deletedIds.length} users`);
9482
+ */
9334
9483
  async deleteMany(ids) {
9335
9484
  const packages = lodashEs.chunk(
9336
9485
  ids.map((id) => this.getResourceKey(id)),
@@ -9389,6 +9538,28 @@ ${JSON.stringify(validation, null, 2)}`
9389
9538
  });
9390
9539
  return { deletedCount, resource: this.name };
9391
9540
  }
9541
+ /**
9542
+ * List resource IDs with optional partition filtering
9543
+ * @param {Object} [params] - List parameters
9544
+ * @param {string} [params.partition] - Partition name to list from
9545
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9546
+ * @returns {Promise<string[]>} Array of resource IDs (strings)
9547
+ * @example
9548
+ * // List all IDs
9549
+ * const allIds = await resource.listIds();
9550
+ *
9551
+ * // List IDs from specific partition
9552
+ * const googleUserIds = await resource.listIds({
9553
+ * partition: 'byUtmSource',
9554
+ * partitionValues: { 'utm.source': 'google' }
9555
+ * });
9556
+ *
9557
+ * // List IDs from multi-field partition
9558
+ * const usElectronicsIds = await resource.listIds({
9559
+ * partition: 'byCategoryRegion',
9560
+ * partitionValues: { category: 'electronics', region: 'US' }
9561
+ * });
9562
+ */
9392
9563
  async listIds({ partition = null, partitionValues = {} } = {}) {
9393
9564
  let prefix;
9394
9565
  if (partition && Object.keys(partitionValues).length > 0) {
@@ -9425,13 +9596,36 @@ ${JSON.stringify(validation, null, 2)}`
9425
9596
  return ids;
9426
9597
  }
9427
9598
  /**
9428
- * List objects by partition name and values
9429
- * @param {Object} partitionOptions - Partition options
9430
- * @param {Object} options - Listing options
9431
- * @returns {Array} Array of objects
9599
+ * List resource objects with optional partition filtering and pagination
9600
+ * @param {Object} [params] - List parameters
9601
+ * @param {string} [params.partition] - Partition name to list from
9602
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9603
+ * @param {number} [params.limit] - Maximum number of results to return
9604
+ * @param {number} [params.offset=0] - Offset for pagination
9605
+ * @returns {Promise<Object[]>} Array of resource objects with all attributes
9606
+ * @example
9607
+ * // List all resources
9608
+ * const allUsers = await resource.list();
9609
+ *
9610
+ * // List with pagination
9611
+ * const firstPage = await resource.list({ limit: 10, offset: 0 });
9612
+ * const secondPage = await resource.list({ limit: 10, offset: 10 });
9613
+ *
9614
+ * // List from specific partition
9615
+ * const googleUsers = await resource.list({
9616
+ * partition: 'byUtmSource',
9617
+ * partitionValues: { 'utm.source': 'google' }
9618
+ * });
9619
+ *
9620
+ * // List from partition with pagination
9621
+ * const googleUsersPage = await resource.list({
9622
+ * partition: 'byUtmSource',
9623
+ * partitionValues: { 'utm.source': 'google' },
9624
+ * limit: 5,
9625
+ * offset: 0
9626
+ * });
9432
9627
  */
9433
- async listByPartition({ partition = null, partitionValues = {} } = {}, options = {}) {
9434
- const { limit, offset = 0 } = options;
9628
+ async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9435
9629
  if (!partition) {
9436
9630
  const ids2 = await this.listIds({ partition, partitionValues });
9437
9631
  let filteredIds2 = ids2.slice(offset);
@@ -9441,7 +9635,7 @@ ${JSON.stringify(validation, null, 2)}`
9441
9635
  const { results: results2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).process(async (id) => {
9442
9636
  return await this.get(id);
9443
9637
  });
9444
- this.emit("listByPartition", { partition, partitionValues, count: results2.length });
9638
+ this.emit("list", { partition, partitionValues, count: results2.length });
9445
9639
  return results2;
9446
9640
  }
9447
9641
  const partitionDef = this.options.partitions[partition];
@@ -9474,11 +9668,19 @@ ${JSON.stringify(validation, null, 2)}`
9474
9668
  filteredIds = filteredIds.slice(0, limit);
9475
9669
  }
9476
9670
  const { results } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).process(async (id) => {
9477
- return await this.getFromPartition(id, partition, partitionValues);
9671
+ return await this.getFromPartition({ id, partitionName: partition, partitionValues });
9478
9672
  });
9479
- this.emit("listByPartition", { partition, partitionValues, count: results.length });
9673
+ this.emit("list", { partition, partitionValues, count: results.length });
9480
9674
  return results;
9481
9675
  }
9676
+ /**
9677
+ * Get multiple resources by their IDs
9678
+ * @param {string[]} ids - Array of resource IDs
9679
+ * @returns {Promise<Object[]>} Array of resource objects
9680
+ * @example
9681
+ * const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
9682
+ * users.forEach(user => console.log(user.name));
9683
+ */
9482
9684
  async getMany(ids) {
9483
9685
  const { results } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).process(async (id) => {
9484
9686
  this.emit("id", id);
@@ -9489,6 +9691,13 @@ ${JSON.stringify(validation, null, 2)}`
9489
9691
  this.emit("getMany", ids.length);
9490
9692
  return results;
9491
9693
  }
9694
+ /**
9695
+ * Get all resources (equivalent to list() without pagination)
9696
+ * @returns {Promise<Object[]>} Array of all resource objects
9697
+ * @example
9698
+ * const allUsers = await resource.getAll();
9699
+ * console.log(`Total users: ${allUsers.length}`);
9700
+ */
9492
9701
  async getAll() {
9493
9702
  let ids = await this.listIds();
9494
9703
  if (ids.length === 0) return [];
@@ -9499,18 +9708,41 @@ ${JSON.stringify(validation, null, 2)}`
9499
9708
  this.emit("getAll", results.length);
9500
9709
  return results;
9501
9710
  }
9502
- async page(offset = 0, size = 100, { partition = null, partitionValues = {} } = {}) {
9503
- const allIds = await this.listIds({ partition, partitionValues });
9504
- const totalItems = allIds.length;
9711
+ /**
9712
+ * Get a page of resources with pagination metadata
9713
+ * @param {Object} [params] - Page parameters
9714
+ * @param {number} [params.offset=0] - Offset for pagination
9715
+ * @param {number} [params.size=100] - Page size
9716
+ * @param {string} [params.partition] - Partition name to page from
9717
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9718
+ * @returns {Promise<Object>} Page result with items and pagination info
9719
+ * @example
9720
+ * // Get first page of all resources
9721
+ * const page = await resource.page({ offset: 0, size: 10 });
9722
+ * console.log(`Page ${page.page + 1} of ${page.totalPages}`);
9723
+ * console.log(`Showing ${page.items.length} of ${page.totalItems} total`);
9724
+ *
9725
+ * // Get page from specific partition
9726
+ * const googlePage = await resource.page({
9727
+ * partition: 'byUtmSource',
9728
+ * partitionValues: { 'utm.source': 'google' },
9729
+ * offset: 0,
9730
+ * size: 5
9731
+ * });
9732
+ */
9733
+ async page({ offset = 0, size = 100, partition = null, partitionValues = {} } = {}) {
9734
+ const ids = await this.listIds({ partition, partitionValues });
9735
+ const totalItems = ids.length;
9505
9736
  const totalPages = Math.ceil(totalItems / size);
9506
- const paginatedIds = allIds.slice(offset * size, (offset + 1) * size);
9737
+ const page = Math.floor(offset / size);
9738
+ const pageIds = ids.slice(offset, offset + size);
9507
9739
  const items = await Promise.all(
9508
- paginatedIds.map((id) => this.get(id))
9740
+ pageIds.map((id) => this.get(id))
9509
9741
  );
9510
9742
  const result = {
9511
9743
  items,
9512
9744
  totalItems,
9513
- page: offset,
9745
+ page,
9514
9746
  pageSize: size,
9515
9747
  totalPages
9516
9748
  };
@@ -9526,36 +9758,62 @@ ${JSON.stringify(validation, null, 2)}`
9526
9758
  return stream.build();
9527
9759
  }
9528
9760
  /**
9529
- * Store binary content associated with a resource
9530
- * @param {string} id - Resource ID
9531
- * @param {Buffer} buffer - Binary content
9532
- * @param {string} contentType - Optional content type
9761
+ * Set binary content for a resource
9762
+ * @param {Object} params - Content parameters
9763
+ * @param {string} params.id - Resource ID
9764
+ * @param {Buffer|string} params.buffer - Content buffer or string
9765
+ * @param {string} [params.contentType='application/octet-stream'] - Content type
9766
+ * @returns {Promise<Object>} Updated resource data
9767
+ * @example
9768
+ * // Set image content
9769
+ * const imageBuffer = fs.readFileSync('image.jpg');
9770
+ * await resource.setContent({
9771
+ * id: 'user-123',
9772
+ * buffer: imageBuffer,
9773
+ * contentType: 'image/jpeg'
9774
+ * });
9775
+ *
9776
+ * // Set text content
9777
+ * await resource.setContent({
9778
+ * id: 'document-456',
9779
+ * buffer: 'Hello World',
9780
+ * contentType: 'text/plain'
9781
+ * });
9533
9782
  */
9534
- async setContent(id, buffer, contentType = "application/octet-stream") {
9535
- if (!Buffer.isBuffer(buffer)) {
9536
- throw new Error("Content must be a Buffer");
9537
- }
9538
- const key = this.getResourceKey(id);
9539
- let existingMetadata = {};
9540
- try {
9541
- const existingObject = await this.client.headObject(key);
9542
- existingMetadata = existingObject.Metadata || {};
9543
- } catch (error) {
9544
- }
9545
- const response = await this.client.putObject({
9546
- key,
9783
+ async setContent({ id, buffer, contentType = "application/octet-stream" }) {
9784
+ const currentData = await this.get(id);
9785
+ if (!currentData) {
9786
+ throw new Error(`Resource with id '${id}' not found`);
9787
+ }
9788
+ const updatedData = {
9789
+ ...currentData,
9790
+ _hasContent: true,
9791
+ _contentLength: buffer.length,
9792
+ _mimeType: contentType
9793
+ };
9794
+ await this.client.putObject({
9795
+ key: this.getResourceKey(id),
9796
+ metadata: await this.schema.mapper(updatedData),
9547
9797
  body: buffer,
9548
- contentType,
9549
- metadata: existingMetadata
9550
- // Preserve existing metadata
9798
+ contentType
9551
9799
  });
9552
- this.emit("setContent", id, buffer.length, contentType);
9553
- return response;
9800
+ this.emit("setContent", { id, contentType, contentLength: buffer.length });
9801
+ return updatedData;
9554
9802
  }
9555
9803
  /**
9556
9804
  * Retrieve binary content associated with a resource
9557
9805
  * @param {string} id - Resource ID
9558
- * @returns {Object} Object with buffer and contentType
9806
+ * @returns {Promise<Object>} Object with buffer and contentType
9807
+ * @example
9808
+ * const content = await resource.content('user-123');
9809
+ * if (content.buffer) {
9810
+ * console.log('Content type:', content.contentType);
9811
+ * console.log('Content size:', content.buffer.length);
9812
+ * // Save to file
9813
+ * fs.writeFileSync('output.jpg', content.buffer);
9814
+ * } else {
9815
+ * console.log('No content found');
9816
+ * }
9559
9817
  */
9560
9818
  async content(id) {
9561
9819
  const key = this.getResourceKey(id);
@@ -9650,7 +9908,7 @@ ${JSON.stringify(validation, null, 2)}`
9650
9908
  return;
9651
9909
  }
9652
9910
  for (const [partitionName, partition] of Object.entries(partitions)) {
9653
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
9911
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9654
9912
  if (partitionKey) {
9655
9913
  const mappedData = await this.schema.mapper(data);
9656
9914
  const behaviorImpl = getBehavior(this.behavior);
@@ -9682,7 +9940,7 @@ ${JSON.stringify(validation, null, 2)}`
9682
9940
  }
9683
9941
  const keysToDelete = [];
9684
9942
  for (const [partitionName, partition] of Object.entries(partitions)) {
9685
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
9943
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9686
9944
  if (partitionKey) {
9687
9945
  keysToDelete.push(partitionKey);
9688
9946
  }
@@ -9696,20 +9954,72 @@ ${JSON.stringify(validation, null, 2)}`
9696
9954
  }
9697
9955
  }
9698
9956
  /**
9699
- * Query documents with simple filtering
9700
- * @param {Object} filter - Filter criteria
9701
- * @returns {Array} Filtered documents
9957
+ * Query resources with simple filtering and pagination
9958
+ * @param {Object} [filter={}] - Filter criteria (exact field matches)
9959
+ * @param {Object} [options] - Query options
9960
+ * @param {number} [options.limit=100] - Maximum number of results
9961
+ * @param {number} [options.offset=0] - Offset for pagination
9962
+ * @param {string} [options.partition] - Partition name to query from
9963
+ * @param {Object} [options.partitionValues] - Partition field values to filter by
9964
+ * @returns {Promise<Object[]>} Array of filtered resource objects
9965
+ * @example
9966
+ * // Query all resources (no filter)
9967
+ * const allUsers = await resource.query();
9968
+ *
9969
+ * // Query with simple filter
9970
+ * const activeUsers = await resource.query({ status: 'active' });
9971
+ *
9972
+ * // Query with multiple filters
9973
+ * const usElectronics = await resource.query({
9974
+ * category: 'electronics',
9975
+ * region: 'US'
9976
+ * });
9977
+ *
9978
+ * // Query with pagination
9979
+ * const firstPage = await resource.query(
9980
+ * { status: 'active' },
9981
+ * { limit: 10, offset: 0 }
9982
+ * );
9983
+ *
9984
+ * // Query within partition
9985
+ * const googleUsers = await resource.query(
9986
+ * { status: 'active' },
9987
+ * {
9988
+ * partition: 'byUtmSource',
9989
+ * partitionValues: { 'utm.source': 'google' },
9990
+ * limit: 5
9991
+ * }
9992
+ * );
9702
9993
  */
9703
- async query(filter = {}) {
9704
- const allDocuments = await this.getAll();
9994
+ async query(filter = {}, { limit = 100, offset = 0, partition = null, partitionValues = {} } = {}) {
9705
9995
  if (Object.keys(filter).length === 0) {
9706
- return allDocuments;
9707
- }
9708
- return allDocuments.filter((doc) => {
9709
- return Object.entries(filter).every(([key, value]) => {
9710
- return doc[key] === value;
9996
+ return await this.list({ partition, partitionValues, limit, offset });
9997
+ }
9998
+ const results = [];
9999
+ let currentOffset = offset;
10000
+ const batchSize = Math.min(limit, 50);
10001
+ while (results.length < limit) {
10002
+ const batch = await this.list({
10003
+ partition,
10004
+ partitionValues,
10005
+ limit: batchSize,
10006
+ offset: currentOffset
9711
10007
  });
9712
- });
10008
+ if (batch.length === 0) {
10009
+ break;
10010
+ }
10011
+ const filteredBatch = batch.filter((doc) => {
10012
+ return Object.entries(filter).every(([key, value]) => {
10013
+ return doc[key] === value;
10014
+ });
10015
+ });
10016
+ results.push(...filteredBatch);
10017
+ currentOffset += batchSize;
10018
+ if (batch.length < batchSize) {
10019
+ break;
10020
+ }
10021
+ }
10022
+ return results.slice(0, limit);
9713
10023
  }
9714
10024
  /**
9715
10025
  * Update partition objects to keep them in sync
@@ -9721,7 +10031,7 @@ ${JSON.stringify(validation, null, 2)}`
9721
10031
  return;
9722
10032
  }
9723
10033
  for (const [partitionName, partition] of Object.entries(partitions)) {
9724
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
10034
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9725
10035
  if (partitionKey) {
9726
10036
  const mappedData = await this.schema.mapper(data);
9727
10037
  const behaviorImpl = getBehavior(this.behavior);
@@ -9748,13 +10058,30 @@ ${JSON.stringify(validation, null, 2)}`
9748
10058
  }
9749
10059
  }
9750
10060
  /**
9751
- * Get object directly from a specific partition
9752
- * @param {string} id - Resource ID
9753
- * @param {string} partitionName - Name of the partition
9754
- * @param {Object} partitionValues - Values for partition fields
9755
- * @returns {Object} The resource data
10061
+ * Get a resource object directly from a specific partition
10062
+ * @param {Object} params - Partition parameters
10063
+ * @param {string} params.id - Resource ID
10064
+ * @param {string} params.partitionName - Name of the partition
10065
+ * @param {Object} params.partitionValues - Values for partition fields
10066
+ * @returns {Promise<Object>} The resource object with partition metadata
10067
+ * @example
10068
+ * // Get user from UTM source partition
10069
+ * const user = await resource.getFromPartition({
10070
+ * id: 'user-123',
10071
+ * partitionName: 'byUtmSource',
10072
+ * partitionValues: { 'utm.source': 'google' }
10073
+ * });
10074
+ * console.log(user._partition); // 'byUtmSource'
10075
+ * console.log(user._partitionValues); // { 'utm.source': 'google' }
10076
+ *
10077
+ * // Get product from multi-field partition
10078
+ * const product = await resource.getFromPartition({
10079
+ * id: 'product-456',
10080
+ * partitionName: 'byCategoryRegion',
10081
+ * partitionValues: { category: 'electronics', region: 'US' }
10082
+ * });
9756
10083
  */
9757
- async getFromPartition(id, partitionName, partitionValues = {}) {
10084
+ async getFromPartition({ id, partitionName, partitionValues = {} }) {
9758
10085
  const partition = this.options.partitions[partitionName];
9759
10086
  if (!partition) {
9760
10087
  throw new Error(`Partition '${partitionName}' not found`);
@@ -9813,7 +10140,7 @@ ${JSON.stringify(validation, null, 2)}`
9813
10140
  this.version = "1";
9814
10141
  this.s3dbVersion = (() => {
9815
10142
  try {
9816
- return true ? "4.1.4" : "latest";
10143
+ return true ? "4.1.6" : "latest";
9817
10144
  } catch (e) {
9818
10145
  return "latest";
9819
10146
  }