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.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,39 +9216,68 @@ ${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
- const request = await this.client.headObject(key);
9177
- const objectVersion = this.extractVersionFromKey(key) || this.version;
9178
- const schema = await this.getSchemaForVersion(objectVersion);
9179
- let metadata = await schema.unmapper(request.Metadata);
9180
- const behaviorImpl = getBehavior(this.behavior);
9181
- let body = "";
9182
- if (request.ContentLength > 0) {
9183
- try {
9184
- const fullObject = await this.client.getObject(key);
9185
- body = await streamToString(fullObject.Body);
9186
- } catch (error) {
9187
- body = "";
9231
+ try {
9232
+ const request = await this.client.headObject(key);
9233
+ const objectVersion = this.extractVersionFromKey(key) || this.version;
9234
+ const schema = await this.getSchemaForVersion(objectVersion);
9235
+ let metadata = await schema.unmapper(request.Metadata);
9236
+ const behaviorImpl = getBehavior(this.behavior);
9237
+ let body = "";
9238
+ if (request.ContentLength > 0) {
9239
+ try {
9240
+ const fullObject = await this.client.getObject(key);
9241
+ body = await streamToString(fullObject.Body);
9242
+ } catch (error) {
9243
+ console.warn(`Failed to read body for resource ${id}:`, error.message);
9244
+ body = "";
9245
+ }
9188
9246
  }
9247
+ const { metadata: processedMetadata } = await behaviorImpl.handleGet({
9248
+ resource: this,
9249
+ metadata,
9250
+ body
9251
+ });
9252
+ let data = processedMetadata;
9253
+ data.id = id;
9254
+ data._contentLength = request.ContentLength;
9255
+ data._lastModified = request.LastModified;
9256
+ data._hasContent = request.ContentLength > 0;
9257
+ data._mimeType = request.ContentType || null;
9258
+ if (request.VersionId) data._versionId = request.VersionId;
9259
+ if (request.Expiration) data._expiresAt = request.Expiration;
9260
+ data._definitionHash = this.getDefinitionHash();
9261
+ this.emit("get", data);
9262
+ return data;
9263
+ } catch (error) {
9264
+ const enhancedError = new Error(`Failed to get resource with id '${id}': ${error.message}`);
9265
+ enhancedError.originalError = error;
9266
+ enhancedError.resourceId = id;
9267
+ enhancedError.resourceKey = key;
9268
+ throw enhancedError;
9189
9269
  }
9190
- const { metadata: processedMetadata } = await behaviorImpl.handleGet({
9191
- resource: this,
9192
- metadata,
9193
- body
9194
- });
9195
- let data = processedMetadata;
9196
- data.id = id;
9197
- data._contentLength = request.ContentLength;
9198
- data._lastModified = request.LastModified;
9199
- data._hasContent = request.ContentLength > 0;
9200
- data._mimeType = request.ContentType || null;
9201
- if (request.VersionId) data._versionId = request.VersionId;
9202
- if (request.Expiration) data._expiresAt = request.Expiration;
9203
- data._definitionHash = this.getDefinitionHash();
9204
- this.emit("get", data);
9205
- return data;
9206
9270
  }
9271
+ /**
9272
+ * Check if a resource exists by ID
9273
+ * @param {string} id - Resource ID
9274
+ * @returns {Promise<boolean>} True if resource exists, false otherwise
9275
+ * @example
9276
+ * const exists = await resource.exists('user-123');
9277
+ * if (exists) {
9278
+ * console.log('User exists');
9279
+ * }
9280
+ */
9207
9281
  async exists(id) {
9208
9282
  try {
9209
9283
  const key = this.getResourceKey(id);
@@ -9213,6 +9287,24 @@ ${JSON.stringify(validation, null, 2)}`
9213
9287
  return false;
9214
9288
  }
9215
9289
  }
9290
+ /**
9291
+ * Update an existing resource object
9292
+ * @param {string} id - Resource ID
9293
+ * @param {Object} attributes - Attributes to update (partial update supported)
9294
+ * @returns {Promise<Object>} The updated resource object with all attributes
9295
+ * @example
9296
+ * // Update specific fields
9297
+ * const updatedUser = await resource.update('user-123', {
9298
+ * name: 'John Updated',
9299
+ * age: 31
9300
+ * });
9301
+ *
9302
+ * // Update with timestamps (if enabled)
9303
+ * const updatedUser = await resource.update('user-123', {
9304
+ * email: 'newemail@example.com'
9305
+ * });
9306
+ * console.log(updatedUser.updatedAt); // ISO timestamp
9307
+ */
9216
9308
  async update(id, attributes) {
9217
9309
  const live = await this.get(id);
9218
9310
  if (this.options.timestamps) {
@@ -9269,6 +9361,14 @@ ${JSON.stringify(validation, null, 2)}`
9269
9361
  this.emit("update", preProcessedData, validated);
9270
9362
  return validated;
9271
9363
  }
9364
+ /**
9365
+ * Delete a resource object by ID
9366
+ * @param {string} id - Resource ID
9367
+ * @returns {Promise<Object>} S3 delete response
9368
+ * @example
9369
+ * await resource.delete('user-123');
9370
+ * console.log('User deleted successfully');
9371
+ */
9272
9372
  async delete(id) {
9273
9373
  let objectData;
9274
9374
  try {
@@ -9283,6 +9383,20 @@ ${JSON.stringify(validation, null, 2)}`
9283
9383
  this.emit("delete", id);
9284
9384
  return response;
9285
9385
  }
9386
+ /**
9387
+ * Insert or update a resource object (upsert operation)
9388
+ * @param {Object} params - Upsert parameters
9389
+ * @param {string} params.id - Resource ID (required for upsert)
9390
+ * @param {...Object} params - Resource attributes (any additional properties)
9391
+ * @returns {Promise<Object>} The inserted or updated resource object
9392
+ * @example
9393
+ * // Will insert if doesn't exist, update if exists
9394
+ * const user = await resource.upsert({
9395
+ * id: 'user-123',
9396
+ * name: 'John Doe',
9397
+ * email: 'john@example.com'
9398
+ * });
9399
+ */
9286
9400
  async upsert({ id, ...attributes }) {
9287
9401
  const exists = await this.exists(id);
9288
9402
  if (exists) {
@@ -9290,6 +9404,28 @@ ${JSON.stringify(validation, null, 2)}`
9290
9404
  }
9291
9405
  return this.insert({ id, ...attributes });
9292
9406
  }
9407
+ /**
9408
+ * Count resources with optional partition filtering
9409
+ * @param {Object} [params] - Count parameters
9410
+ * @param {string} [params.partition] - Partition name to count in
9411
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9412
+ * @returns {Promise<number>} Total count of matching resources
9413
+ * @example
9414
+ * // Count all resources
9415
+ * const total = await resource.count();
9416
+ *
9417
+ * // Count in specific partition
9418
+ * const googleUsers = await resource.count({
9419
+ * partition: 'byUtmSource',
9420
+ * partitionValues: { 'utm.source': 'google' }
9421
+ * });
9422
+ *
9423
+ * // Count in multi-field partition
9424
+ * const usElectronics = await resource.count({
9425
+ * partition: 'byCategoryRegion',
9426
+ * partitionValues: { category: 'electronics', region: 'US' }
9427
+ * });
9428
+ */
9293
9429
  async count({ partition = null, partitionValues = {} } = {}) {
9294
9430
  let prefix;
9295
9431
  if (partition && Object.keys(partitionValues).length > 0) {
@@ -9320,6 +9456,19 @@ ${JSON.stringify(validation, null, 2)}`
9320
9456
  this.emit("count", count);
9321
9457
  return count;
9322
9458
  }
9459
+ /**
9460
+ * Insert multiple resources in parallel
9461
+ * @param {Object[]} objects - Array of resource objects to insert
9462
+ * @returns {Promise<Object[]>} Array of inserted resource objects
9463
+ * @example
9464
+ * const users = [
9465
+ * { name: 'John', email: 'john@example.com' },
9466
+ * { name: 'Jane', email: 'jane@example.com' },
9467
+ * { name: 'Bob', email: 'bob@example.com' }
9468
+ * ];
9469
+ * const insertedUsers = await resource.insertMany(users);
9470
+ * console.log(`Inserted ${insertedUsers.length} users`);
9471
+ */
9323
9472
  async insertMany(objects) {
9324
9473
  const { results } = await promisePool.PromisePool.for(objects).withConcurrency(this.parallelism).handleError(async (error, content) => {
9325
9474
  this.emit("error", error, content);
@@ -9331,6 +9480,15 @@ ${JSON.stringify(validation, null, 2)}`
9331
9480
  this.emit("insertMany", objects.length);
9332
9481
  return results;
9333
9482
  }
9483
+ /**
9484
+ * Delete multiple resources by their IDs in parallel
9485
+ * @param {string[]} ids - Array of resource IDs to delete
9486
+ * @returns {Promise<Object[]>} Array of S3 delete responses
9487
+ * @example
9488
+ * const deletedIds = ['user-1', 'user-2', 'user-3'];
9489
+ * const results = await resource.deleteMany(deletedIds);
9490
+ * console.log(`Deleted ${deletedIds.length} users`);
9491
+ */
9334
9492
  async deleteMany(ids) {
9335
9493
  const packages = lodashEs.chunk(
9336
9494
  ids.map((id) => this.getResourceKey(id)),
@@ -9389,7 +9547,35 @@ ${JSON.stringify(validation, null, 2)}`
9389
9547
  });
9390
9548
  return { deletedCount, resource: this.name };
9391
9549
  }
9392
- async listIds({ partition = null, partitionValues = {} } = {}) {
9550
+ /**
9551
+ * List resource IDs with optional partition filtering and pagination
9552
+ * @param {Object} [params] - List parameters
9553
+ * @param {string} [params.partition] - Partition name to list from
9554
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9555
+ * @param {number} [params.limit] - Maximum number of results to return
9556
+ * @param {number} [params.offset=0] - Offset for pagination
9557
+ * @returns {Promise<string[]>} Array of resource IDs (strings)
9558
+ * @example
9559
+ * // List all IDs
9560
+ * const allIds = await resource.listIds();
9561
+ *
9562
+ * // List IDs with pagination
9563
+ * const firstPageIds = await resource.listIds({ limit: 10, offset: 0 });
9564
+ * const secondPageIds = await resource.listIds({ limit: 10, offset: 10 });
9565
+ *
9566
+ * // List IDs from specific partition
9567
+ * const googleUserIds = await resource.listIds({
9568
+ * partition: 'byUtmSource',
9569
+ * partitionValues: { 'utm.source': 'google' }
9570
+ * });
9571
+ *
9572
+ * // List IDs from multi-field partition
9573
+ * const usElectronicsIds = await resource.listIds({
9574
+ * partition: 'byCategoryRegion',
9575
+ * partitionValues: { category: 'electronics', region: 'US' }
9576
+ * });
9577
+ */
9578
+ async listIds({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9393
9579
  let prefix;
9394
9580
  if (partition && Object.keys(partitionValues).length > 0) {
9395
9581
  const partitionDef = this.options.partitions[partition];
@@ -9413,8 +9599,11 @@ ${JSON.stringify(validation, null, 2)}`
9413
9599
  } else {
9414
9600
  prefix = `resource=${this.name}/v=${this.version}`;
9415
9601
  }
9416
- const keys = await this.client.getAllKeys({
9417
- prefix
9602
+ const keys = await this.client.getKeysPage({
9603
+ prefix,
9604
+ offset,
9605
+ amount: limit || 1e3
9606
+ // Default to 1000 if no limit specified
9418
9607
  });
9419
9608
  const ids = keys.map((key) => {
9420
9609
  const parts = key.split("/");
@@ -9425,13 +9614,36 @@ ${JSON.stringify(validation, null, 2)}`
9425
9614
  return ids;
9426
9615
  }
9427
9616
  /**
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
9617
+ * List resource objects with optional partition filtering and pagination
9618
+ * @param {Object} [params] - List parameters
9619
+ * @param {string} [params.partition] - Partition name to list from
9620
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9621
+ * @param {number} [params.limit] - Maximum number of results to return
9622
+ * @param {number} [params.offset=0] - Offset for pagination
9623
+ * @returns {Promise<Object[]>} Array of resource objects with all attributes
9624
+ * @example
9625
+ * // List all resources
9626
+ * const allUsers = await resource.list();
9627
+ *
9628
+ * // List with pagination
9629
+ * const firstPage = await resource.list({ limit: 10, offset: 0 });
9630
+ * const secondPage = await resource.list({ limit: 10, offset: 10 });
9631
+ *
9632
+ * // List from specific partition
9633
+ * const googleUsers = await resource.list({
9634
+ * partition: 'byUtmSource',
9635
+ * partitionValues: { 'utm.source': 'google' }
9636
+ * });
9637
+ *
9638
+ * // List from partition with pagination
9639
+ * const googleUsersPage = await resource.list({
9640
+ * partition: 'byUtmSource',
9641
+ * partitionValues: { 'utm.source': 'google' },
9642
+ * limit: 5,
9643
+ * offset: 0
9644
+ * });
9432
9645
  */
9433
- async listByPartition({ partition = null, partitionValues = {} } = {}, options = {}) {
9434
- const { limit, offset = 0 } = options;
9646
+ async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9435
9647
  if (!partition) {
9436
9648
  const ids2 = await this.listIds({ partition, partitionValues });
9437
9649
  let filteredIds2 = ids2.slice(offset);
@@ -9441,7 +9653,7 @@ ${JSON.stringify(validation, null, 2)}`
9441
9653
  const { results: results2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).process(async (id) => {
9442
9654
  return await this.get(id);
9443
9655
  });
9444
- this.emit("listByPartition", { partition, partitionValues, count: results2.length });
9656
+ this.emit("list", { partition, partitionValues, count: results2.length });
9445
9657
  return results2;
9446
9658
  }
9447
9659
  const partitionDef = this.options.partitions[partition];
@@ -9474,11 +9686,19 @@ ${JSON.stringify(validation, null, 2)}`
9474
9686
  filteredIds = filteredIds.slice(0, limit);
9475
9687
  }
9476
9688
  const { results } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).process(async (id) => {
9477
- return await this.getFromPartition(id, partition, partitionValues);
9689
+ return await this.getFromPartition({ id, partitionName: partition, partitionValues });
9478
9690
  });
9479
- this.emit("listByPartition", { partition, partitionValues, count: results.length });
9691
+ this.emit("list", { partition, partitionValues, count: results.length });
9480
9692
  return results;
9481
9693
  }
9694
+ /**
9695
+ * Get multiple resources by their IDs
9696
+ * @param {string[]} ids - Array of resource IDs
9697
+ * @returns {Promise<Object[]>} Array of resource objects
9698
+ * @example
9699
+ * const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
9700
+ * users.forEach(user => console.log(user.name));
9701
+ */
9482
9702
  async getMany(ids) {
9483
9703
  const { results } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).process(async (id) => {
9484
9704
  this.emit("id", id);
@@ -9489,6 +9709,13 @@ ${JSON.stringify(validation, null, 2)}`
9489
9709
  this.emit("getMany", ids.length);
9490
9710
  return results;
9491
9711
  }
9712
+ /**
9713
+ * Get all resources (equivalent to list() without pagination)
9714
+ * @returns {Promise<Object[]>} Array of all resource objects
9715
+ * @example
9716
+ * const allUsers = await resource.getAll();
9717
+ * console.log(`Total users: ${allUsers.length}`);
9718
+ */
9492
9719
  async getAll() {
9493
9720
  let ids = await this.listIds();
9494
9721
  if (ids.length === 0) return [];
@@ -9499,20 +9726,65 @@ ${JSON.stringify(validation, null, 2)}`
9499
9726
  this.emit("getAll", results.length);
9500
9727
  return results;
9501
9728
  }
9502
- async page(offset = 0, size = 100, { partition = null, partitionValues = {} } = {}) {
9503
- const allIds = await this.listIds({ partition, partitionValues });
9504
- const totalItems = allIds.length;
9505
- const totalPages = Math.ceil(totalItems / size);
9506
- const paginatedIds = allIds.slice(offset * size, (offset + 1) * size);
9507
- const items = await Promise.all(
9508
- paginatedIds.map((id) => this.get(id))
9509
- );
9729
+ /**
9730
+ * Get a page of resources with pagination metadata
9731
+ * @param {Object} [params] - Page parameters
9732
+ * @param {number} [params.offset=0] - Offset for pagination
9733
+ * @param {number} [params.size=100] - Page size
9734
+ * @param {string} [params.partition] - Partition name to page from
9735
+ * @param {Object} [params.partitionValues] - Partition field values to filter by
9736
+ * @param {boolean} [params.skipCount=false] - Skip total count for performance (useful for large collections)
9737
+ * @returns {Promise<Object>} Page result with items and pagination info
9738
+ * @example
9739
+ * // Get first page of all resources
9740
+ * const page = await resource.page({ offset: 0, size: 10 });
9741
+ * console.log(`Page ${page.page + 1} of ${page.totalPages}`);
9742
+ * console.log(`Showing ${page.items.length} of ${page.totalItems} total`);
9743
+ *
9744
+ * // Get page from specific partition
9745
+ * const googlePage = await resource.page({
9746
+ * partition: 'byUtmSource',
9747
+ * partitionValues: { 'utm.source': 'google' },
9748
+ * offset: 0,
9749
+ * size: 5
9750
+ * });
9751
+ *
9752
+ * // Skip count for performance in large collections
9753
+ * const fastPage = await resource.page({
9754
+ * offset: 0,
9755
+ * size: 100,
9756
+ * skipCount: true
9757
+ * });
9758
+ * console.log(`Got ${fastPage.items.length} items`); // totalItems will be null
9759
+ */
9760
+ async page({ offset = 0, size = 100, partition = null, partitionValues = {}, skipCount = false } = {}) {
9761
+ let totalItems = null;
9762
+ let totalPages = null;
9763
+ if (!skipCount) {
9764
+ totalItems = await this.count({ partition, partitionValues });
9765
+ totalPages = Math.ceil(totalItems / size);
9766
+ }
9767
+ const page = Math.floor(offset / size);
9768
+ const items = await this.list({
9769
+ partition,
9770
+ partitionValues,
9771
+ limit: size,
9772
+ offset
9773
+ });
9510
9774
  const result = {
9511
9775
  items,
9512
9776
  totalItems,
9513
- page: offset,
9777
+ page,
9514
9778
  pageSize: size,
9515
- totalPages
9779
+ totalPages,
9780
+ // Add additional metadata for debugging
9781
+ _debug: {
9782
+ requestedSize: size,
9783
+ requestedOffset: offset,
9784
+ actualItemsReturned: items.length,
9785
+ skipCount,
9786
+ hasTotalItems: totalItems !== null
9787
+ }
9516
9788
  };
9517
9789
  this.emit("page", result);
9518
9790
  return result;
@@ -9526,36 +9798,62 @@ ${JSON.stringify(validation, null, 2)}`
9526
9798
  return stream.build();
9527
9799
  }
9528
9800
  /**
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
9801
+ * Set binary content for a resource
9802
+ * @param {Object} params - Content parameters
9803
+ * @param {string} params.id - Resource ID
9804
+ * @param {Buffer|string} params.buffer - Content buffer or string
9805
+ * @param {string} [params.contentType='application/octet-stream'] - Content type
9806
+ * @returns {Promise<Object>} Updated resource data
9807
+ * @example
9808
+ * // Set image content
9809
+ * const imageBuffer = fs.readFileSync('image.jpg');
9810
+ * await resource.setContent({
9811
+ * id: 'user-123',
9812
+ * buffer: imageBuffer,
9813
+ * contentType: 'image/jpeg'
9814
+ * });
9815
+ *
9816
+ * // Set text content
9817
+ * await resource.setContent({
9818
+ * id: 'document-456',
9819
+ * buffer: 'Hello World',
9820
+ * contentType: 'text/plain'
9821
+ * });
9533
9822
  */
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,
9823
+ async setContent({ id, buffer, contentType = "application/octet-stream" }) {
9824
+ const currentData = await this.get(id);
9825
+ if (!currentData) {
9826
+ throw new Error(`Resource with id '${id}' not found`);
9827
+ }
9828
+ const updatedData = {
9829
+ ...currentData,
9830
+ _hasContent: true,
9831
+ _contentLength: buffer.length,
9832
+ _mimeType: contentType
9833
+ };
9834
+ await this.client.putObject({
9835
+ key: this.getResourceKey(id),
9836
+ metadata: await this.schema.mapper(updatedData),
9547
9837
  body: buffer,
9548
- contentType,
9549
- metadata: existingMetadata
9550
- // Preserve existing metadata
9838
+ contentType
9551
9839
  });
9552
- this.emit("setContent", id, buffer.length, contentType);
9553
- return response;
9840
+ this.emit("setContent", { id, contentType, contentLength: buffer.length });
9841
+ return updatedData;
9554
9842
  }
9555
9843
  /**
9556
9844
  * Retrieve binary content associated with a resource
9557
9845
  * @param {string} id - Resource ID
9558
- * @returns {Object} Object with buffer and contentType
9846
+ * @returns {Promise<Object>} Object with buffer and contentType
9847
+ * @example
9848
+ * const content = await resource.content('user-123');
9849
+ * if (content.buffer) {
9850
+ * console.log('Content type:', content.contentType);
9851
+ * console.log('Content size:', content.buffer.length);
9852
+ * // Save to file
9853
+ * fs.writeFileSync('output.jpg', content.buffer);
9854
+ * } else {
9855
+ * console.log('No content found');
9856
+ * }
9559
9857
  */
9560
9858
  async content(id) {
9561
9859
  const key = this.getResourceKey(id);
@@ -9650,7 +9948,7 @@ ${JSON.stringify(validation, null, 2)}`
9650
9948
  return;
9651
9949
  }
9652
9950
  for (const [partitionName, partition] of Object.entries(partitions)) {
9653
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
9951
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9654
9952
  if (partitionKey) {
9655
9953
  const mappedData = await this.schema.mapper(data);
9656
9954
  const behaviorImpl = getBehavior(this.behavior);
@@ -9682,7 +9980,7 @@ ${JSON.stringify(validation, null, 2)}`
9682
9980
  }
9683
9981
  const keysToDelete = [];
9684
9982
  for (const [partitionName, partition] of Object.entries(partitions)) {
9685
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
9983
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9686
9984
  if (partitionKey) {
9687
9985
  keysToDelete.push(partitionKey);
9688
9986
  }
@@ -9696,20 +9994,72 @@ ${JSON.stringify(validation, null, 2)}`
9696
9994
  }
9697
9995
  }
9698
9996
  /**
9699
- * Query documents with simple filtering
9700
- * @param {Object} filter - Filter criteria
9701
- * @returns {Array} Filtered documents
9997
+ * Query resources with simple filtering and pagination
9998
+ * @param {Object} [filter={}] - Filter criteria (exact field matches)
9999
+ * @param {Object} [options] - Query options
10000
+ * @param {number} [options.limit=100] - Maximum number of results
10001
+ * @param {number} [options.offset=0] - Offset for pagination
10002
+ * @param {string} [options.partition] - Partition name to query from
10003
+ * @param {Object} [options.partitionValues] - Partition field values to filter by
10004
+ * @returns {Promise<Object[]>} Array of filtered resource objects
10005
+ * @example
10006
+ * // Query all resources (no filter)
10007
+ * const allUsers = await resource.query();
10008
+ *
10009
+ * // Query with simple filter
10010
+ * const activeUsers = await resource.query({ status: 'active' });
10011
+ *
10012
+ * // Query with multiple filters
10013
+ * const usElectronics = await resource.query({
10014
+ * category: 'electronics',
10015
+ * region: 'US'
10016
+ * });
10017
+ *
10018
+ * // Query with pagination
10019
+ * const firstPage = await resource.query(
10020
+ * { status: 'active' },
10021
+ * { limit: 10, offset: 0 }
10022
+ * );
10023
+ *
10024
+ * // Query within partition
10025
+ * const googleUsers = await resource.query(
10026
+ * { status: 'active' },
10027
+ * {
10028
+ * partition: 'byUtmSource',
10029
+ * partitionValues: { 'utm.source': 'google' },
10030
+ * limit: 5
10031
+ * }
10032
+ * );
9702
10033
  */
9703
- async query(filter = {}) {
9704
- const allDocuments = await this.getAll();
10034
+ async query(filter = {}, { limit = 100, offset = 0, partition = null, partitionValues = {} } = {}) {
9705
10035
  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;
10036
+ return await this.list({ partition, partitionValues, limit, offset });
10037
+ }
10038
+ const results = [];
10039
+ let currentOffset = offset;
10040
+ const batchSize = Math.min(limit, 50);
10041
+ while (results.length < limit) {
10042
+ const batch = await this.list({
10043
+ partition,
10044
+ partitionValues,
10045
+ limit: batchSize,
10046
+ offset: currentOffset
9711
10047
  });
9712
- });
10048
+ if (batch.length === 0) {
10049
+ break;
10050
+ }
10051
+ const filteredBatch = batch.filter((doc) => {
10052
+ return Object.entries(filter).every(([key, value]) => {
10053
+ return doc[key] === value;
10054
+ });
10055
+ });
10056
+ results.push(...filteredBatch);
10057
+ currentOffset += batchSize;
10058
+ if (batch.length < batchSize) {
10059
+ break;
10060
+ }
10061
+ }
10062
+ return results.slice(0, limit);
9713
10063
  }
9714
10064
  /**
9715
10065
  * Update partition objects to keep them in sync
@@ -9721,7 +10071,7 @@ ${JSON.stringify(validation, null, 2)}`
9721
10071
  return;
9722
10072
  }
9723
10073
  for (const [partitionName, partition] of Object.entries(partitions)) {
9724
- const partitionKey = this.getPartitionKey(partitionName, data.id, data);
10074
+ const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
9725
10075
  if (partitionKey) {
9726
10076
  const mappedData = await this.schema.mapper(data);
9727
10077
  const behaviorImpl = getBehavior(this.behavior);
@@ -9748,13 +10098,30 @@ ${JSON.stringify(validation, null, 2)}`
9748
10098
  }
9749
10099
  }
9750
10100
  /**
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
10101
+ * Get a resource object directly from a specific partition
10102
+ * @param {Object} params - Partition parameters
10103
+ * @param {string} params.id - Resource ID
10104
+ * @param {string} params.partitionName - Name of the partition
10105
+ * @param {Object} params.partitionValues - Values for partition fields
10106
+ * @returns {Promise<Object>} The resource object with partition metadata
10107
+ * @example
10108
+ * // Get user from UTM source partition
10109
+ * const user = await resource.getFromPartition({
10110
+ * id: 'user-123',
10111
+ * partitionName: 'byUtmSource',
10112
+ * partitionValues: { 'utm.source': 'google' }
10113
+ * });
10114
+ * console.log(user._partition); // 'byUtmSource'
10115
+ * console.log(user._partitionValues); // { 'utm.source': 'google' }
10116
+ *
10117
+ * // Get product from multi-field partition
10118
+ * const product = await resource.getFromPartition({
10119
+ * id: 'product-456',
10120
+ * partitionName: 'byCategoryRegion',
10121
+ * partitionValues: { category: 'electronics', region: 'US' }
10122
+ * });
9756
10123
  */
9757
- async getFromPartition(id, partitionName, partitionValues = {}) {
10124
+ async getFromPartition({ id, partitionName, partitionValues = {} }) {
9758
10125
  const partition = this.options.partitions[partitionName];
9759
10126
  if (!partition) {
9760
10127
  throw new Error(`Partition '${partitionName}' not found`);
@@ -9813,7 +10180,7 @@ ${JSON.stringify(validation, null, 2)}`
9813
10180
  this.version = "1";
9814
10181
  this.s3dbVersion = (() => {
9815
10182
  try {
9816
- return true ? "4.1.4" : "latest";
10183
+ return true ? "4.1.7" : "latest";
9817
10184
  } catch (e) {
9818
10185
  return "latest";
9819
10186
  }