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