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