s3db.js 4.1.6 → 4.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/s3db.cjs.js +438 -111
- package/dist/s3db.cjs.min.js +9 -9
- package/dist/s3db.es.js +438 -111
- package/dist/s3db.es.min.js +9 -9
- package/dist/s3db.iife.js +438 -111
- package/dist/s3db.iife.min.js +9 -9
- 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,6 +9224,16 @@ 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
9239
|
const request = await this.client.headObject(key);
|
|
@@ -9212,6 +9267,16 @@ class Resource extends EventEmitter {
|
|
|
9212
9267
|
this.emit("get", data);
|
|
9213
9268
|
return data;
|
|
9214
9269
|
}
|
|
9270
|
+
/**
|
|
9271
|
+
* Check if a resource exists by ID
|
|
9272
|
+
* @param {string} id - Resource ID
|
|
9273
|
+
* @returns {Promise<boolean>} True if resource exists, false otherwise
|
|
9274
|
+
* @example
|
|
9275
|
+
* const exists = await resource.exists('user-123');
|
|
9276
|
+
* if (exists) {
|
|
9277
|
+
* console.log('User exists');
|
|
9278
|
+
* }
|
|
9279
|
+
*/
|
|
9215
9280
|
async exists(id) {
|
|
9216
9281
|
try {
|
|
9217
9282
|
const key = this.getResourceKey(id);
|
|
@@ -9221,6 +9286,24 @@ class Resource extends EventEmitter {
|
|
|
9221
9286
|
return false;
|
|
9222
9287
|
}
|
|
9223
9288
|
}
|
|
9289
|
+
/**
|
|
9290
|
+
* Update an existing resource object
|
|
9291
|
+
* @param {string} id - Resource ID
|
|
9292
|
+
* @param {Object} attributes - Attributes to update (partial update supported)
|
|
9293
|
+
* @returns {Promise<Object>} The updated resource object with all attributes
|
|
9294
|
+
* @example
|
|
9295
|
+
* // Update specific fields
|
|
9296
|
+
* const updatedUser = await resource.update('user-123', {
|
|
9297
|
+
* name: 'John Updated',
|
|
9298
|
+
* age: 31
|
|
9299
|
+
* });
|
|
9300
|
+
*
|
|
9301
|
+
* // Update with timestamps (if enabled)
|
|
9302
|
+
* const updatedUser = await resource.update('user-123', {
|
|
9303
|
+
* email: 'newemail@example.com'
|
|
9304
|
+
* });
|
|
9305
|
+
* console.log(updatedUser.updatedAt); // ISO timestamp
|
|
9306
|
+
*/
|
|
9224
9307
|
async update(id, attributes) {
|
|
9225
9308
|
const live = await this.get(id);
|
|
9226
9309
|
if (this.options.timestamps) {
|
|
@@ -9277,6 +9360,14 @@ class Resource extends EventEmitter {
|
|
|
9277
9360
|
this.emit("update", preProcessedData, validated);
|
|
9278
9361
|
return validated;
|
|
9279
9362
|
}
|
|
9363
|
+
/**
|
|
9364
|
+
* Delete a resource object by ID
|
|
9365
|
+
* @param {string} id - Resource ID
|
|
9366
|
+
* @returns {Promise<Object>} S3 delete response
|
|
9367
|
+
* @example
|
|
9368
|
+
* await resource.delete('user-123');
|
|
9369
|
+
* console.log('User deleted successfully');
|
|
9370
|
+
*/
|
|
9280
9371
|
async delete(id) {
|
|
9281
9372
|
let objectData;
|
|
9282
9373
|
try {
|
|
@@ -9291,6 +9382,20 @@ class Resource extends EventEmitter {
|
|
|
9291
9382
|
this.emit("delete", id);
|
|
9292
9383
|
return response;
|
|
9293
9384
|
}
|
|
9385
|
+
/**
|
|
9386
|
+
* Insert or update a resource object (upsert operation)
|
|
9387
|
+
* @param {Object} params - Upsert parameters
|
|
9388
|
+
* @param {string} params.id - Resource ID (required for upsert)
|
|
9389
|
+
* @param {...Object} params - Resource attributes (any additional properties)
|
|
9390
|
+
* @returns {Promise<Object>} The inserted or updated resource object
|
|
9391
|
+
* @example
|
|
9392
|
+
* // Will insert if doesn't exist, update if exists
|
|
9393
|
+
* const user = await resource.upsert({
|
|
9394
|
+
* id: 'user-123',
|
|
9395
|
+
* name: 'John Doe',
|
|
9396
|
+
* email: 'john@example.com'
|
|
9397
|
+
* });
|
|
9398
|
+
*/
|
|
9294
9399
|
async upsert({ id, ...attributes }) {
|
|
9295
9400
|
const exists = await this.exists(id);
|
|
9296
9401
|
if (exists) {
|
|
@@ -9298,6 +9403,28 @@ class Resource extends EventEmitter {
|
|
|
9298
9403
|
}
|
|
9299
9404
|
return this.insert({ id, ...attributes });
|
|
9300
9405
|
}
|
|
9406
|
+
/**
|
|
9407
|
+
* Count resources with optional partition filtering
|
|
9408
|
+
* @param {Object} [params] - Count parameters
|
|
9409
|
+
* @param {string} [params.partition] - Partition name to count in
|
|
9410
|
+
* @param {Object} [params.partitionValues] - Partition field values to filter by
|
|
9411
|
+
* @returns {Promise<number>} Total count of matching resources
|
|
9412
|
+
* @example
|
|
9413
|
+
* // Count all resources
|
|
9414
|
+
* const total = await resource.count();
|
|
9415
|
+
*
|
|
9416
|
+
* // Count in specific partition
|
|
9417
|
+
* const googleUsers = await resource.count({
|
|
9418
|
+
* partition: 'byUtmSource',
|
|
9419
|
+
* partitionValues: { 'utm.source': 'google' }
|
|
9420
|
+
* });
|
|
9421
|
+
*
|
|
9422
|
+
* // Count in multi-field partition
|
|
9423
|
+
* const usElectronics = await resource.count({
|
|
9424
|
+
* partition: 'byCategoryRegion',
|
|
9425
|
+
* partitionValues: { category: 'electronics', region: 'US' }
|
|
9426
|
+
* });
|
|
9427
|
+
*/
|
|
9301
9428
|
async count({ partition = null, partitionValues = {} } = {}) {
|
|
9302
9429
|
let prefix;
|
|
9303
9430
|
if (partition && Object.keys(partitionValues).length > 0) {
|
|
@@ -9328,6 +9455,19 @@ class Resource extends EventEmitter {
|
|
|
9328
9455
|
this.emit("count", count);
|
|
9329
9456
|
return count;
|
|
9330
9457
|
}
|
|
9458
|
+
/**
|
|
9459
|
+
* Insert multiple resources in parallel
|
|
9460
|
+
* @param {Object[]} objects - Array of resource objects to insert
|
|
9461
|
+
* @returns {Promise<Object[]>} Array of inserted resource objects
|
|
9462
|
+
* @example
|
|
9463
|
+
* const users = [
|
|
9464
|
+
* { name: 'John', email: 'john@example.com' },
|
|
9465
|
+
* { name: 'Jane', email: 'jane@example.com' },
|
|
9466
|
+
* { name: 'Bob', email: 'bob@example.com' }
|
|
9467
|
+
* ];
|
|
9468
|
+
* const insertedUsers = await resource.insertMany(users);
|
|
9469
|
+
* console.log(`Inserted ${insertedUsers.length} users`);
|
|
9470
|
+
*/
|
|
9331
9471
|
async insertMany(objects) {
|
|
9332
9472
|
const { results } = await promisePool.PromisePool.for(objects).withConcurrency(this.parallelism).handleError(async (error, content) => {
|
|
9333
9473
|
this.emit("error", error, content);
|
|
@@ -9339,6 +9479,15 @@ class Resource extends EventEmitter {
|
|
|
9339
9479
|
this.emit("insertMany", objects.length);
|
|
9340
9480
|
return results;
|
|
9341
9481
|
}
|
|
9482
|
+
/**
|
|
9483
|
+
* Delete multiple resources by their IDs in parallel
|
|
9484
|
+
* @param {string[]} ids - Array of resource IDs to delete
|
|
9485
|
+
* @returns {Promise<Object[]>} Array of S3 delete responses
|
|
9486
|
+
* @example
|
|
9487
|
+
* const deletedIds = ['user-1', 'user-2', 'user-3'];
|
|
9488
|
+
* const results = await resource.deleteMany(deletedIds);
|
|
9489
|
+
* console.log(`Deleted ${deletedIds.length} users`);
|
|
9490
|
+
*/
|
|
9342
9491
|
async deleteMany(ids) {
|
|
9343
9492
|
const packages = lodashEs.chunk(
|
|
9344
9493
|
ids.map((id) => this.getResourceKey(id)),
|
|
@@ -9397,6 +9546,28 @@ class Resource extends EventEmitter {
|
|
|
9397
9546
|
});
|
|
9398
9547
|
return { deletedCount, resource: this.name };
|
|
9399
9548
|
}
|
|
9549
|
+
/**
|
|
9550
|
+
* List resource IDs with optional partition filtering
|
|
9551
|
+
* @param {Object} [params] - List parameters
|
|
9552
|
+
* @param {string} [params.partition] - Partition name to list from
|
|
9553
|
+
* @param {Object} [params.partitionValues] - Partition field values to filter by
|
|
9554
|
+
* @returns {Promise<string[]>} Array of resource IDs (strings)
|
|
9555
|
+
* @example
|
|
9556
|
+
* // List all IDs
|
|
9557
|
+
* const allIds = await resource.listIds();
|
|
9558
|
+
*
|
|
9559
|
+
* // List IDs from specific partition
|
|
9560
|
+
* const googleUserIds = await resource.listIds({
|
|
9561
|
+
* partition: 'byUtmSource',
|
|
9562
|
+
* partitionValues: { 'utm.source': 'google' }
|
|
9563
|
+
* });
|
|
9564
|
+
*
|
|
9565
|
+
* // List IDs from multi-field partition
|
|
9566
|
+
* const usElectronicsIds = await resource.listIds({
|
|
9567
|
+
* partition: 'byCategoryRegion',
|
|
9568
|
+
* partitionValues: { category: 'electronics', region: 'US' }
|
|
9569
|
+
* });
|
|
9570
|
+
*/
|
|
9400
9571
|
async listIds({ partition = null, partitionValues = {} } = {}) {
|
|
9401
9572
|
let prefix;
|
|
9402
9573
|
if (partition && Object.keys(partitionValues).length > 0) {
|
|
@@ -9433,13 +9604,36 @@ class Resource extends EventEmitter {
|
|
|
9433
9604
|
return ids;
|
|
9434
9605
|
}
|
|
9435
9606
|
/**
|
|
9436
|
-
* List objects
|
|
9437
|
-
* @param {Object}
|
|
9438
|
-
* @param {
|
|
9439
|
-
* @
|
|
9607
|
+
* List resource objects with optional partition filtering and pagination
|
|
9608
|
+
* @param {Object} [params] - List parameters
|
|
9609
|
+
* @param {string} [params.partition] - Partition name to list from
|
|
9610
|
+
* @param {Object} [params.partitionValues] - Partition field values to filter by
|
|
9611
|
+
* @param {number} [params.limit] - Maximum number of results to return
|
|
9612
|
+
* @param {number} [params.offset=0] - Offset for pagination
|
|
9613
|
+
* @returns {Promise<Object[]>} Array of resource objects with all attributes
|
|
9614
|
+
* @example
|
|
9615
|
+
* // List all resources
|
|
9616
|
+
* const allUsers = await resource.list();
|
|
9617
|
+
*
|
|
9618
|
+
* // List with pagination
|
|
9619
|
+
* const firstPage = await resource.list({ limit: 10, offset: 0 });
|
|
9620
|
+
* const secondPage = await resource.list({ limit: 10, offset: 10 });
|
|
9621
|
+
*
|
|
9622
|
+
* // List from specific partition
|
|
9623
|
+
* const googleUsers = await resource.list({
|
|
9624
|
+
* partition: 'byUtmSource',
|
|
9625
|
+
* partitionValues: { 'utm.source': 'google' }
|
|
9626
|
+
* });
|
|
9627
|
+
*
|
|
9628
|
+
* // List from partition with pagination
|
|
9629
|
+
* const googleUsersPage = await resource.list({
|
|
9630
|
+
* partition: 'byUtmSource',
|
|
9631
|
+
* partitionValues: { 'utm.source': 'google' },
|
|
9632
|
+
* limit: 5,
|
|
9633
|
+
* offset: 0
|
|
9634
|
+
* });
|
|
9440
9635
|
*/
|
|
9441
|
-
async
|
|
9442
|
-
const { limit, offset = 0 } = options;
|
|
9636
|
+
async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
|
|
9443
9637
|
if (!partition) {
|
|
9444
9638
|
const ids2 = await this.listIds({ partition, partitionValues });
|
|
9445
9639
|
let filteredIds2 = ids2.slice(offset);
|
|
@@ -9449,7 +9643,7 @@ class Resource extends EventEmitter {
|
|
|
9449
9643
|
const { results: results2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).process(async (id) => {
|
|
9450
9644
|
return await this.get(id);
|
|
9451
9645
|
});
|
|
9452
|
-
this.emit("
|
|
9646
|
+
this.emit("list", { partition, partitionValues, count: results2.length });
|
|
9453
9647
|
return results2;
|
|
9454
9648
|
}
|
|
9455
9649
|
const partitionDef = this.options.partitions[partition];
|
|
@@ -9482,11 +9676,19 @@ class Resource extends EventEmitter {
|
|
|
9482
9676
|
filteredIds = filteredIds.slice(0, limit);
|
|
9483
9677
|
}
|
|
9484
9678
|
const { results } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).process(async (id) => {
|
|
9485
|
-
return await this.getFromPartition(id, partition, partitionValues);
|
|
9679
|
+
return await this.getFromPartition({ id, partitionName: partition, partitionValues });
|
|
9486
9680
|
});
|
|
9487
|
-
this.emit("
|
|
9681
|
+
this.emit("list", { partition, partitionValues, count: results.length });
|
|
9488
9682
|
return results;
|
|
9489
9683
|
}
|
|
9684
|
+
/**
|
|
9685
|
+
* Get multiple resources by their IDs
|
|
9686
|
+
* @param {string[]} ids - Array of resource IDs
|
|
9687
|
+
* @returns {Promise<Object[]>} Array of resource objects
|
|
9688
|
+
* @example
|
|
9689
|
+
* const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
|
|
9690
|
+
* users.forEach(user => console.log(user.name));
|
|
9691
|
+
*/
|
|
9490
9692
|
async getMany(ids) {
|
|
9491
9693
|
const { results } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).process(async (id) => {
|
|
9492
9694
|
this.emit("id", id);
|
|
@@ -9497,6 +9699,13 @@ class Resource extends EventEmitter {
|
|
|
9497
9699
|
this.emit("getMany", ids.length);
|
|
9498
9700
|
return results;
|
|
9499
9701
|
}
|
|
9702
|
+
/**
|
|
9703
|
+
* Get all resources (equivalent to list() without pagination)
|
|
9704
|
+
* @returns {Promise<Object[]>} Array of all resource objects
|
|
9705
|
+
* @example
|
|
9706
|
+
* const allUsers = await resource.getAll();
|
|
9707
|
+
* console.log(`Total users: ${allUsers.length}`);
|
|
9708
|
+
*/
|
|
9500
9709
|
async getAll() {
|
|
9501
9710
|
let ids = await this.listIds();
|
|
9502
9711
|
if (ids.length === 0) return [];
|
|
@@ -9507,18 +9716,41 @@ class Resource extends EventEmitter {
|
|
|
9507
9716
|
this.emit("getAll", results.length);
|
|
9508
9717
|
return results;
|
|
9509
9718
|
}
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9719
|
+
/**
|
|
9720
|
+
* Get a page of resources with pagination metadata
|
|
9721
|
+
* @param {Object} [params] - Page parameters
|
|
9722
|
+
* @param {number} [params.offset=0] - Offset for pagination
|
|
9723
|
+
* @param {number} [params.size=100] - Page size
|
|
9724
|
+
* @param {string} [params.partition] - Partition name to page from
|
|
9725
|
+
* @param {Object} [params.partitionValues] - Partition field values to filter by
|
|
9726
|
+
* @returns {Promise<Object>} Page result with items and pagination info
|
|
9727
|
+
* @example
|
|
9728
|
+
* // Get first page of all resources
|
|
9729
|
+
* const page = await resource.page({ offset: 0, size: 10 });
|
|
9730
|
+
* console.log(`Page ${page.page + 1} of ${page.totalPages}`);
|
|
9731
|
+
* console.log(`Showing ${page.items.length} of ${page.totalItems} total`);
|
|
9732
|
+
*
|
|
9733
|
+
* // Get page from specific partition
|
|
9734
|
+
* const googlePage = await resource.page({
|
|
9735
|
+
* partition: 'byUtmSource',
|
|
9736
|
+
* partitionValues: { 'utm.source': 'google' },
|
|
9737
|
+
* offset: 0,
|
|
9738
|
+
* size: 5
|
|
9739
|
+
* });
|
|
9740
|
+
*/
|
|
9741
|
+
async page({ offset = 0, size = 100, partition = null, partitionValues = {} } = {}) {
|
|
9742
|
+
const ids = await this.listIds({ partition, partitionValues });
|
|
9743
|
+
const totalItems = ids.length;
|
|
9513
9744
|
const totalPages = Math.ceil(totalItems / size);
|
|
9514
|
-
const
|
|
9745
|
+
const page = Math.floor(offset / size);
|
|
9746
|
+
const pageIds = ids.slice(offset, offset + size);
|
|
9515
9747
|
const items = await Promise.all(
|
|
9516
|
-
|
|
9748
|
+
pageIds.map((id) => this.get(id))
|
|
9517
9749
|
);
|
|
9518
9750
|
const result = {
|
|
9519
9751
|
items,
|
|
9520
9752
|
totalItems,
|
|
9521
|
-
page
|
|
9753
|
+
page,
|
|
9522
9754
|
pageSize: size,
|
|
9523
9755
|
totalPages
|
|
9524
9756
|
};
|
|
@@ -9534,36 +9766,62 @@ class Resource extends EventEmitter {
|
|
|
9534
9766
|
return stream.build();
|
|
9535
9767
|
}
|
|
9536
9768
|
/**
|
|
9537
|
-
*
|
|
9538
|
-
* @param {
|
|
9539
|
-
* @param {
|
|
9540
|
-
* @param {string}
|
|
9769
|
+
* Set binary content for a resource
|
|
9770
|
+
* @param {Object} params - Content parameters
|
|
9771
|
+
* @param {string} params.id - Resource ID
|
|
9772
|
+
* @param {Buffer|string} params.buffer - Content buffer or string
|
|
9773
|
+
* @param {string} [params.contentType='application/octet-stream'] - Content type
|
|
9774
|
+
* @returns {Promise<Object>} Updated resource data
|
|
9775
|
+
* @example
|
|
9776
|
+
* // Set image content
|
|
9777
|
+
* const imageBuffer = fs.readFileSync('image.jpg');
|
|
9778
|
+
* await resource.setContent({
|
|
9779
|
+
* id: 'user-123',
|
|
9780
|
+
* buffer: imageBuffer,
|
|
9781
|
+
* contentType: 'image/jpeg'
|
|
9782
|
+
* });
|
|
9783
|
+
*
|
|
9784
|
+
* // Set text content
|
|
9785
|
+
* await resource.setContent({
|
|
9786
|
+
* id: 'document-456',
|
|
9787
|
+
* buffer: 'Hello World',
|
|
9788
|
+
* contentType: 'text/plain'
|
|
9789
|
+
* });
|
|
9541
9790
|
*/
|
|
9542
|
-
async setContent(id, buffer, contentType = "application/octet-stream") {
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
}
|
|
9553
|
-
|
|
9554
|
-
key,
|
|
9791
|
+
async setContent({ id, buffer, contentType = "application/octet-stream" }) {
|
|
9792
|
+
const currentData = await this.get(id);
|
|
9793
|
+
if (!currentData) {
|
|
9794
|
+
throw new Error(`Resource with id '${id}' not found`);
|
|
9795
|
+
}
|
|
9796
|
+
const updatedData = {
|
|
9797
|
+
...currentData,
|
|
9798
|
+
_hasContent: true,
|
|
9799
|
+
_contentLength: buffer.length,
|
|
9800
|
+
_mimeType: contentType
|
|
9801
|
+
};
|
|
9802
|
+
await this.client.putObject({
|
|
9803
|
+
key: this.getResourceKey(id),
|
|
9804
|
+
metadata: await this.schema.mapper(updatedData),
|
|
9555
9805
|
body: buffer,
|
|
9556
|
-
contentType
|
|
9557
|
-
metadata: existingMetadata
|
|
9558
|
-
// Preserve existing metadata
|
|
9806
|
+
contentType
|
|
9559
9807
|
});
|
|
9560
|
-
this.emit("setContent", id, buffer.length
|
|
9561
|
-
return
|
|
9808
|
+
this.emit("setContent", { id, contentType, contentLength: buffer.length });
|
|
9809
|
+
return updatedData;
|
|
9562
9810
|
}
|
|
9563
9811
|
/**
|
|
9564
9812
|
* Retrieve binary content associated with a resource
|
|
9565
9813
|
* @param {string} id - Resource ID
|
|
9566
|
-
* @returns {Object} Object with buffer and contentType
|
|
9814
|
+
* @returns {Promise<Object>} Object with buffer and contentType
|
|
9815
|
+
* @example
|
|
9816
|
+
* const content = await resource.content('user-123');
|
|
9817
|
+
* if (content.buffer) {
|
|
9818
|
+
* console.log('Content type:', content.contentType);
|
|
9819
|
+
* console.log('Content size:', content.buffer.length);
|
|
9820
|
+
* // Save to file
|
|
9821
|
+
* fs.writeFileSync('output.jpg', content.buffer);
|
|
9822
|
+
* } else {
|
|
9823
|
+
* console.log('No content found');
|
|
9824
|
+
* }
|
|
9567
9825
|
*/
|
|
9568
9826
|
async content(id) {
|
|
9569
9827
|
const key = this.getResourceKey(id);
|
|
@@ -9658,7 +9916,7 @@ class Resource extends EventEmitter {
|
|
|
9658
9916
|
return;
|
|
9659
9917
|
}
|
|
9660
9918
|
for (const [partitionName, partition] of Object.entries(partitions)) {
|
|
9661
|
-
const partitionKey = this.getPartitionKey(partitionName, data.id, data);
|
|
9919
|
+
const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
|
|
9662
9920
|
if (partitionKey) {
|
|
9663
9921
|
const mappedData = await this.schema.mapper(data);
|
|
9664
9922
|
const behaviorImpl = getBehavior(this.behavior);
|
|
@@ -9690,7 +9948,7 @@ class Resource extends EventEmitter {
|
|
|
9690
9948
|
}
|
|
9691
9949
|
const keysToDelete = [];
|
|
9692
9950
|
for (const [partitionName, partition] of Object.entries(partitions)) {
|
|
9693
|
-
const partitionKey = this.getPartitionKey(partitionName, data.id, data);
|
|
9951
|
+
const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
|
|
9694
9952
|
if (partitionKey) {
|
|
9695
9953
|
keysToDelete.push(partitionKey);
|
|
9696
9954
|
}
|
|
@@ -9704,20 +9962,72 @@ class Resource extends EventEmitter {
|
|
|
9704
9962
|
}
|
|
9705
9963
|
}
|
|
9706
9964
|
/**
|
|
9707
|
-
* Query
|
|
9708
|
-
* @param {Object} filter - Filter criteria
|
|
9709
|
-
* @
|
|
9965
|
+
* Query resources with simple filtering and pagination
|
|
9966
|
+
* @param {Object} [filter={}] - Filter criteria (exact field matches)
|
|
9967
|
+
* @param {Object} [options] - Query options
|
|
9968
|
+
* @param {number} [options.limit=100] - Maximum number of results
|
|
9969
|
+
* @param {number} [options.offset=0] - Offset for pagination
|
|
9970
|
+
* @param {string} [options.partition] - Partition name to query from
|
|
9971
|
+
* @param {Object} [options.partitionValues] - Partition field values to filter by
|
|
9972
|
+
* @returns {Promise<Object[]>} Array of filtered resource objects
|
|
9973
|
+
* @example
|
|
9974
|
+
* // Query all resources (no filter)
|
|
9975
|
+
* const allUsers = await resource.query();
|
|
9976
|
+
*
|
|
9977
|
+
* // Query with simple filter
|
|
9978
|
+
* const activeUsers = await resource.query({ status: 'active' });
|
|
9979
|
+
*
|
|
9980
|
+
* // Query with multiple filters
|
|
9981
|
+
* const usElectronics = await resource.query({
|
|
9982
|
+
* category: 'electronics',
|
|
9983
|
+
* region: 'US'
|
|
9984
|
+
* });
|
|
9985
|
+
*
|
|
9986
|
+
* // Query with pagination
|
|
9987
|
+
* const firstPage = await resource.query(
|
|
9988
|
+
* { status: 'active' },
|
|
9989
|
+
* { limit: 10, offset: 0 }
|
|
9990
|
+
* );
|
|
9991
|
+
*
|
|
9992
|
+
* // Query within partition
|
|
9993
|
+
* const googleUsers = await resource.query(
|
|
9994
|
+
* { status: 'active' },
|
|
9995
|
+
* {
|
|
9996
|
+
* partition: 'byUtmSource',
|
|
9997
|
+
* partitionValues: { 'utm.source': 'google' },
|
|
9998
|
+
* limit: 5
|
|
9999
|
+
* }
|
|
10000
|
+
* );
|
|
9710
10001
|
*/
|
|
9711
|
-
async query(filter = {}) {
|
|
9712
|
-
const allDocuments = await this.getAll();
|
|
10002
|
+
async query(filter = {}, { limit = 100, offset = 0, partition = null, partitionValues = {} } = {}) {
|
|
9713
10003
|
if (Object.keys(filter).length === 0) {
|
|
9714
|
-
return
|
|
9715
|
-
}
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
10004
|
+
return await this.list({ partition, partitionValues, limit, offset });
|
|
10005
|
+
}
|
|
10006
|
+
const results = [];
|
|
10007
|
+
let currentOffset = offset;
|
|
10008
|
+
const batchSize = Math.min(limit, 50);
|
|
10009
|
+
while (results.length < limit) {
|
|
10010
|
+
const batch = await this.list({
|
|
10011
|
+
partition,
|
|
10012
|
+
partitionValues,
|
|
10013
|
+
limit: batchSize,
|
|
10014
|
+
offset: currentOffset
|
|
9719
10015
|
});
|
|
9720
|
-
|
|
10016
|
+
if (batch.length === 0) {
|
|
10017
|
+
break;
|
|
10018
|
+
}
|
|
10019
|
+
const filteredBatch = batch.filter((doc) => {
|
|
10020
|
+
return Object.entries(filter).every(([key, value]) => {
|
|
10021
|
+
return doc[key] === value;
|
|
10022
|
+
});
|
|
10023
|
+
});
|
|
10024
|
+
results.push(...filteredBatch);
|
|
10025
|
+
currentOffset += batchSize;
|
|
10026
|
+
if (batch.length < batchSize) {
|
|
10027
|
+
break;
|
|
10028
|
+
}
|
|
10029
|
+
}
|
|
10030
|
+
return results.slice(0, limit);
|
|
9721
10031
|
}
|
|
9722
10032
|
/**
|
|
9723
10033
|
* Update partition objects to keep them in sync
|
|
@@ -9729,7 +10039,7 @@ class Resource extends EventEmitter {
|
|
|
9729
10039
|
return;
|
|
9730
10040
|
}
|
|
9731
10041
|
for (const [partitionName, partition] of Object.entries(partitions)) {
|
|
9732
|
-
const partitionKey = this.getPartitionKey(partitionName, data.id, data);
|
|
10042
|
+
const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
|
|
9733
10043
|
if (partitionKey) {
|
|
9734
10044
|
const mappedData = await this.schema.mapper(data);
|
|
9735
10045
|
const behaviorImpl = getBehavior(this.behavior);
|
|
@@ -9756,13 +10066,30 @@ class Resource extends EventEmitter {
|
|
|
9756
10066
|
}
|
|
9757
10067
|
}
|
|
9758
10068
|
/**
|
|
9759
|
-
* Get object directly from a specific partition
|
|
9760
|
-
* @param {
|
|
9761
|
-
* @param {string}
|
|
9762
|
-
* @param {
|
|
9763
|
-
* @
|
|
10069
|
+
* Get a resource object directly from a specific partition
|
|
10070
|
+
* @param {Object} params - Partition parameters
|
|
10071
|
+
* @param {string} params.id - Resource ID
|
|
10072
|
+
* @param {string} params.partitionName - Name of the partition
|
|
10073
|
+
* @param {Object} params.partitionValues - Values for partition fields
|
|
10074
|
+
* @returns {Promise<Object>} The resource object with partition metadata
|
|
10075
|
+
* @example
|
|
10076
|
+
* // Get user from UTM source partition
|
|
10077
|
+
* const user = await resource.getFromPartition({
|
|
10078
|
+
* id: 'user-123',
|
|
10079
|
+
* partitionName: 'byUtmSource',
|
|
10080
|
+
* partitionValues: { 'utm.source': 'google' }
|
|
10081
|
+
* });
|
|
10082
|
+
* console.log(user._partition); // 'byUtmSource'
|
|
10083
|
+
* console.log(user._partitionValues); // { 'utm.source': 'google' }
|
|
10084
|
+
*
|
|
10085
|
+
* // Get product from multi-field partition
|
|
10086
|
+
* const product = await resource.getFromPartition({
|
|
10087
|
+
* id: 'product-456',
|
|
10088
|
+
* partitionName: 'byCategoryRegion',
|
|
10089
|
+
* partitionValues: { category: 'electronics', region: 'US' }
|
|
10090
|
+
* });
|
|
9764
10091
|
*/
|
|
9765
|
-
async getFromPartition(id, partitionName, partitionValues = {}) {
|
|
10092
|
+
async getFromPartition({ id, partitionName, partitionValues = {} }) {
|
|
9766
10093
|
const partition = this.options.partitions[partitionName];
|
|
9767
10094
|
if (!partition) {
|
|
9768
10095
|
throw new Error(`Partition '${partitionName}' not found`);
|
|
@@ -9821,7 +10148,7 @@ class Database extends EventEmitter {
|
|
|
9821
10148
|
this.version = "1";
|
|
9822
10149
|
this.s3dbVersion = (() => {
|
|
9823
10150
|
try {
|
|
9824
|
-
return true ? "4.1.
|
|
10151
|
+
return true ? "4.1.6" : "latest";
|
|
9825
10152
|
} catch (e) {
|
|
9826
10153
|
return "latest";
|
|
9827
10154
|
}
|