s3db.js 4.1.7 → 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/README.md CHANGED
@@ -283,7 +283,7 @@ const googleUserIds = await users.listIds({
283
283
  partitionValues: { 'utm.source': 'google' }
284
284
  });
285
285
 
286
- // Paginar resultados
286
+ // Paginar resultados com contagem total
287
287
  const page = await users.page({
288
288
  partition: 'byUtmSource',
289
289
  partitionValues: { 'utm.source': 'google' },
@@ -291,7 +291,20 @@ const page = await users.page({
291
291
  size: 10
292
292
  });
293
293
  console.log(page.items); // Array de usuários
294
- console.log(page.totalItems, page.totalPages);
294
+ console.log(page.totalItems, page.totalPages); // Contagem total e páginas
295
+ console.log(page.page, page.pageSize); // Página atual e tamanho
296
+
297
+ // Paginar resultados sem contagem total (mais rápido para grandes coleções)
298
+ const fastPage = await users.page({
299
+ partition: 'byUtmSource',
300
+ partitionValues: { 'utm.source': 'google' },
301
+ offset: 0,
302
+ size: 10,
303
+ skipCount: true // Pula a contagem total para melhor performance
304
+ });
305
+ console.log(fastPage.items); // Array de usuários
306
+ console.log(fastPage.totalItems); // null (não contado)
307
+ console.log(fastPage._debug); // Informações de debug
295
308
 
296
309
  // Contar documentos em uma partição
297
310
  const count = await users.count({
@@ -960,6 +973,73 @@ const count = await users.count();
960
973
  console.log(`Total users: ${count}`);
961
974
  ```
962
975
 
976
+ #### Page Documents
977
+
978
+ The `page()` method provides efficient pagination with optional total count for performance optimization.
979
+
980
+ ```javascript
981
+ // Basic pagination with total count
982
+ const page = await users.page({
983
+ offset: 0,
984
+ size: 10
985
+ });
986
+
987
+ console.log(page.items); // Array of user objects
988
+ console.log(page.totalItems); // Total number of items
989
+ console.log(page.totalPages); // Total number of pages
990
+ console.log(page.page); // Current page number (0-based)
991
+ console.log(page.pageSize); // Items per page
992
+ console.log(page._debug); // Debug information
993
+
994
+ // Pagination with partition filtering
995
+ const googleUsersPage = await users.page({
996
+ partition: 'byUtmSource',
997
+ partitionValues: { 'utm.source': 'google' },
998
+ offset: 0,
999
+ size: 5
1000
+ });
1001
+
1002
+ // Skip total count for better performance on large collections
1003
+ const fastPage = await users.page({
1004
+ offset: 0,
1005
+ size: 100,
1006
+ skipCount: true // Skips counting total items
1007
+ });
1008
+
1009
+ console.log(fastPage.totalItems); // null (not counted)
1010
+ console.log(fastPage.totalPages); // null (not calculated)
1011
+ console.log(fastPage._debug.skipCount); // true
1012
+ ```
1013
+
1014
+ **Page Response Structure:**
1015
+
1016
+ ```javascript
1017
+ {
1018
+ items: Array, // Array of document objects
1019
+ totalItems: number, // Total count (null if skipCount: true)
1020
+ page: number, // Current page number (0-based)
1021
+ pageSize: number, // Number of items per page
1022
+ totalPages: number, // Total pages (null if skipCount: true)
1023
+ _debug: { // Debug information
1024
+ requestedSize: number,
1025
+ requestedOffset: number,
1026
+ actualItemsReturned: number,
1027
+ skipCount: boolean,
1028
+ hasTotalItems: boolean
1029
+ }
1030
+ }
1031
+ ```
1032
+
1033
+ **Parameters:**
1034
+
1035
+ | Parameter | Type | Default | Description |
1036
+ |-----------|------|---------|-------------|
1037
+ | `offset` | `number` | `0` | Number of items to skip |
1038
+ | `size` | `number` | `100` | Number of items per page |
1039
+ | `partition` | `string` | `null` | Partition name to filter by |
1040
+ | `partitionValues` | `object` | `{}` | Partition field values |
1041
+ | `skipCount` | `boolean` | `false` | Skip total count for performance |
1042
+
963
1043
  ### Bulk Operations
964
1044
 
965
1045
  #### Insert Many
@@ -1680,7 +1760,7 @@ await products.insert({
1680
1760
  | `listIds()` | Get array of document IDs | ✅ `{ partition, partitionValues }` |
1681
1761
  | `count()` | Count documents | ✅ `{ partition, partitionValues }` |
1682
1762
  | `listByPartition()` | List documents by partition | ✅ `{ partition, partitionValues }` |
1683
- | `page()` | Paginate documents | ✅ `{ partition, partitionValues }` |
1763
+ | `page()` | Paginate documents | ✅ `{ partition, partitionValues, skipCount }` |
1684
1764
  | `getFromPartition()` | Get single document from partition | ✅ Direct partition access |
1685
1765
  | `query()` | Filter documents in memory | ❌ No partition support |
1686
1766
 
@@ -1767,19 +1847,6 @@ const s3db = new S3db({
1767
1847
  - **`enforce-limits`**: Strict validation to prevent oversized documents
1768
1848
  - **`user-management`**: Default behavior with warnings and monitoring
1769
1849
 
1770
- ### ✅ Recent Improvements
1771
-
1772
- **🔧 Enhanced Data Serialization (v3.3.2+)**
1773
-
1774
- s3db.js now handles complex data structures robustly:
1775
-
1776
- - **Empty Arrays**: `[]` correctly serialized and preserved
1777
- - **Null Arrays**: `null` values maintained without corruption
1778
- - **Special Characters**: Arrays with pipe `|` characters properly escaped
1779
- - **Empty Objects**: `{}` correctly mapped and stored
1780
- - **Null Objects**: `null` object values preserved during serialization
1781
- - **Nested Structures**: Complex nested objects with mixed empty/null values supported
1782
-
1783
1850
  ### Best Practices
1784
1851
 
1785
1852
  #### 1. Design for Document Storage
package/dist/s3db.cjs.js CHANGED
@@ -9236,36 +9236,45 @@ class Resource extends EventEmitter {
9236
9236
  */
9237
9237
  async get(id) {
9238
9238
  const key = this.getResourceKey(id);
9239
- const request = await this.client.headObject(key);
9240
- const objectVersion = this.extractVersionFromKey(key) || this.version;
9241
- const schema = await this.getSchemaForVersion(objectVersion);
9242
- let metadata = await schema.unmapper(request.Metadata);
9243
- const behaviorImpl = getBehavior(this.behavior);
9244
- let body = "";
9245
- if (request.ContentLength > 0) {
9246
- try {
9247
- const fullObject = await this.client.getObject(key);
9248
- body = await streamToString(fullObject.Body);
9249
- } catch (error) {
9250
- body = "";
9239
+ try {
9240
+ const request = await this.client.headObject(key);
9241
+ const objectVersion = this.extractVersionFromKey(key) || this.version;
9242
+ const schema = await this.getSchemaForVersion(objectVersion);
9243
+ let metadata = await schema.unmapper(request.Metadata);
9244
+ const behaviorImpl = getBehavior(this.behavior);
9245
+ let body = "";
9246
+ if (request.ContentLength > 0) {
9247
+ try {
9248
+ const fullObject = await this.client.getObject(key);
9249
+ body = await streamToString(fullObject.Body);
9250
+ } catch (error) {
9251
+ console.warn(`Failed to read body for resource ${id}:`, error.message);
9252
+ body = "";
9253
+ }
9251
9254
  }
9255
+ const { metadata: processedMetadata } = await behaviorImpl.handleGet({
9256
+ resource: this,
9257
+ metadata,
9258
+ body
9259
+ });
9260
+ let data = processedMetadata;
9261
+ data.id = id;
9262
+ data._contentLength = request.ContentLength;
9263
+ data._lastModified = request.LastModified;
9264
+ data._hasContent = request.ContentLength > 0;
9265
+ data._mimeType = request.ContentType || null;
9266
+ if (request.VersionId) data._versionId = request.VersionId;
9267
+ if (request.Expiration) data._expiresAt = request.Expiration;
9268
+ data._definitionHash = this.getDefinitionHash();
9269
+ this.emit("get", data);
9270
+ return data;
9271
+ } catch (error) {
9272
+ const enhancedError = new Error(`Failed to get resource with id '${id}': ${error.message}`);
9273
+ enhancedError.originalError = error;
9274
+ enhancedError.resourceId = id;
9275
+ enhancedError.resourceKey = key;
9276
+ throw enhancedError;
9252
9277
  }
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
9278
  }
9270
9279
  /**
9271
9280
  * Check if a resource exists by ID
@@ -9547,15 +9556,21 @@ class Resource extends EventEmitter {
9547
9556
  return { deletedCount, resource: this.name };
9548
9557
  }
9549
9558
  /**
9550
- * List resource IDs with optional partition filtering
9559
+ * List resource IDs with optional partition filtering and pagination
9551
9560
  * @param {Object} [params] - List parameters
9552
9561
  * @param {string} [params.partition] - Partition name to list from
9553
9562
  * @param {Object} [params.partitionValues] - Partition field values to filter by
9563
+ * @param {number} [params.limit] - Maximum number of results to return
9564
+ * @param {number} [params.offset=0] - Offset for pagination
9554
9565
  * @returns {Promise<string[]>} Array of resource IDs (strings)
9555
9566
  * @example
9556
9567
  * // List all IDs
9557
9568
  * const allIds = await resource.listIds();
9558
9569
  *
9570
+ * // List IDs with pagination
9571
+ * const firstPageIds = await resource.listIds({ limit: 10, offset: 0 });
9572
+ * const secondPageIds = await resource.listIds({ limit: 10, offset: 10 });
9573
+ *
9559
9574
  * // List IDs from specific partition
9560
9575
  * const googleUserIds = await resource.listIds({
9561
9576
  * partition: 'byUtmSource',
@@ -9568,7 +9583,7 @@ class Resource extends EventEmitter {
9568
9583
  * partitionValues: { category: 'electronics', region: 'US' }
9569
9584
  * });
9570
9585
  */
9571
- async listIds({ partition = null, partitionValues = {} } = {}) {
9586
+ async listIds({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9572
9587
  let prefix;
9573
9588
  if (partition && Object.keys(partitionValues).length > 0) {
9574
9589
  const partitionDef = this.options.partitions[partition];
@@ -9592,8 +9607,11 @@ class Resource extends EventEmitter {
9592
9607
  } else {
9593
9608
  prefix = `resource=${this.name}/v=${this.version}`;
9594
9609
  }
9595
- const keys = await this.client.getAllKeys({
9596
- prefix
9610
+ const keys = await this.client.getKeysPage({
9611
+ prefix,
9612
+ offset,
9613
+ amount: limit || 1e3
9614
+ // Default to 1000 if no limit specified
9597
9615
  });
9598
9616
  const ids = keys.map((key) => {
9599
9617
  const parts = key.split("/");
@@ -9723,6 +9741,7 @@ class Resource extends EventEmitter {
9723
9741
  * @param {number} [params.size=100] - Page size
9724
9742
  * @param {string} [params.partition] - Partition name to page from
9725
9743
  * @param {Object} [params.partitionValues] - Partition field values to filter by
9744
+ * @param {boolean} [params.skipCount=false] - Skip total count for performance (useful for large collections)
9726
9745
  * @returns {Promise<Object>} Page result with items and pagination info
9727
9746
  * @example
9728
9747
  * // Get first page of all resources
@@ -9737,22 +9756,43 @@ class Resource extends EventEmitter {
9737
9756
  * offset: 0,
9738
9757
  * size: 5
9739
9758
  * });
9759
+ *
9760
+ * // Skip count for performance in large collections
9761
+ * const fastPage = await resource.page({
9762
+ * offset: 0,
9763
+ * size: 100,
9764
+ * skipCount: true
9765
+ * });
9766
+ * console.log(`Got ${fastPage.items.length} items`); // totalItems will be null
9740
9767
  */
9741
- async page({ offset = 0, size = 100, partition = null, partitionValues = {} } = {}) {
9742
- const ids = await this.listIds({ partition, partitionValues });
9743
- const totalItems = ids.length;
9744
- const totalPages = Math.ceil(totalItems / size);
9768
+ async page({ offset = 0, size = 100, partition = null, partitionValues = {}, skipCount = false } = {}) {
9769
+ let totalItems = null;
9770
+ let totalPages = null;
9771
+ if (!skipCount) {
9772
+ totalItems = await this.count({ partition, partitionValues });
9773
+ totalPages = Math.ceil(totalItems / size);
9774
+ }
9745
9775
  const page = Math.floor(offset / size);
9746
- const pageIds = ids.slice(offset, offset + size);
9747
- const items = await Promise.all(
9748
- pageIds.map((id) => this.get(id))
9749
- );
9776
+ const items = await this.list({
9777
+ partition,
9778
+ partitionValues,
9779
+ limit: size,
9780
+ offset
9781
+ });
9750
9782
  const result = {
9751
9783
  items,
9752
9784
  totalItems,
9753
9785
  page,
9754
9786
  pageSize: size,
9755
- totalPages
9787
+ totalPages,
9788
+ // Add additional metadata for debugging
9789
+ _debug: {
9790
+ requestedSize: size,
9791
+ requestedOffset: offset,
9792
+ actualItemsReturned: items.length,
9793
+ skipCount,
9794
+ hasTotalItems: totalItems !== null
9795
+ }
9756
9796
  };
9757
9797
  this.emit("page", result);
9758
9798
  return result;
@@ -10148,7 +10188,7 @@ class Database extends EventEmitter {
10148
10188
  this.version = "1";
10149
10189
  this.s3dbVersion = (() => {
10150
10190
  try {
10151
- return true ? "4.1.6" : "latest";
10191
+ return true ? "4.1.7" : "latest";
10152
10192
  } catch (e) {
10153
10193
  return "latest";
10154
10194
  }