s3db.js 11.2.2 → 11.2.4
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 +1650 -136
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1644 -137
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/behaviors/enforce-limits.js +28 -4
- package/src/behaviors/index.js +6 -1
- package/src/client.class.js +11 -1
- package/src/concerns/partition-queue.js +7 -1
- package/src/concerns/plugin-storage.js +75 -13
- package/src/database.class.js +22 -4
- package/src/errors.js +414 -24
- package/src/partition-drivers/base-partition-driver.js +12 -2
- package/src/partition-drivers/index.js +7 -1
- package/src/partition-drivers/memory-partition-driver.js +20 -5
- package/src/partition-drivers/sqs-partition-driver.js +6 -1
- package/src/plugins/audit.errors.js +46 -0
- package/src/plugins/backup/base-backup-driver.class.js +36 -6
- package/src/plugins/backup/filesystem-backup-driver.class.js +55 -7
- package/src/plugins/backup/index.js +40 -9
- package/src/plugins/backup/multi-backup-driver.class.js +69 -9
- package/src/plugins/backup/s3-backup-driver.class.js +48 -6
- package/src/plugins/backup.errors.js +45 -0
- package/src/plugins/cache/cache.class.js +8 -1
- package/src/plugins/cache/memory-cache.class.js +216 -33
- package/src/plugins/cache.errors.js +47 -0
- package/src/plugins/cache.plugin.js +94 -3
- package/src/plugins/eventual-consistency/analytics.js +145 -0
- package/src/plugins/eventual-consistency/index.js +203 -1
- package/src/plugins/fulltext.errors.js +46 -0
- package/src/plugins/fulltext.plugin.js +15 -3
- package/src/plugins/metrics.errors.js +46 -0
- package/src/plugins/queue-consumer.plugin.js +31 -4
- package/src/plugins/queue.errors.js +46 -0
- package/src/plugins/replicator.errors.js +46 -0
- package/src/plugins/replicator.plugin.js +40 -5
- package/src/plugins/replicators/base-replicator.class.js +19 -3
- package/src/plugins/replicators/index.js +9 -3
- package/src/plugins/replicators/s3db-replicator.class.js +38 -8
- package/src/plugins/scheduler.errors.js +46 -0
- package/src/plugins/scheduler.plugin.js +79 -19
- package/src/plugins/state-machine.errors.js +47 -0
- package/src/plugins/state-machine.plugin.js +86 -17
- package/src/resource.class.js +8 -1
- package/src/stream/index.js +6 -1
- package/src/stream/resource-reader.class.js +6 -1
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { calculateTotalSize } from '../concerns/calculator.js';
|
|
2
2
|
import { calculateEffectiveLimit } from '../concerns/calculator.js';
|
|
3
|
+
import { MetadataLimitError } from '../errors.js';
|
|
3
4
|
|
|
4
5
|
export const S3_METADATA_LIMIT_BYTES = 2047;
|
|
5
6
|
|
|
@@ -146,9 +147,16 @@ export async function handleInsert({ resource, data, mappedData, originalData })
|
|
|
146
147
|
});
|
|
147
148
|
|
|
148
149
|
if (totalSize > effectiveLimit) {
|
|
149
|
-
throw new
|
|
150
|
+
throw new MetadataLimitError('Metadata size exceeds 2KB limit on insert', {
|
|
151
|
+
totalSize,
|
|
152
|
+
effectiveLimit,
|
|
153
|
+
absoluteLimit: S3_METADATA_LIMIT_BYTES,
|
|
154
|
+
excess: totalSize - effectiveLimit,
|
|
155
|
+
resourceName: resource.name,
|
|
156
|
+
operation: 'insert'
|
|
157
|
+
});
|
|
150
158
|
}
|
|
151
|
-
|
|
159
|
+
|
|
152
160
|
// If data fits in metadata, store only in metadata
|
|
153
161
|
return { mappedData, body: "" };
|
|
154
162
|
}
|
|
@@ -167,7 +175,15 @@ export async function handleUpdate({ resource, id, data, mappedData, originalDat
|
|
|
167
175
|
});
|
|
168
176
|
|
|
169
177
|
if (totalSize > effectiveLimit) {
|
|
170
|
-
throw new
|
|
178
|
+
throw new MetadataLimitError('Metadata size exceeds 2KB limit on update', {
|
|
179
|
+
totalSize,
|
|
180
|
+
effectiveLimit,
|
|
181
|
+
absoluteLimit: S3_METADATA_LIMIT_BYTES,
|
|
182
|
+
excess: totalSize - effectiveLimit,
|
|
183
|
+
resourceName: resource.name,
|
|
184
|
+
operation: 'update',
|
|
185
|
+
id
|
|
186
|
+
});
|
|
171
187
|
}
|
|
172
188
|
return { mappedData, body: JSON.stringify(mappedData) };
|
|
173
189
|
}
|
|
@@ -186,7 +202,15 @@ export async function handleUpsert({ resource, id, data, mappedData }) {
|
|
|
186
202
|
});
|
|
187
203
|
|
|
188
204
|
if (totalSize > effectiveLimit) {
|
|
189
|
-
throw new
|
|
205
|
+
throw new MetadataLimitError('Metadata size exceeds 2KB limit on upsert', {
|
|
206
|
+
totalSize,
|
|
207
|
+
effectiveLimit,
|
|
208
|
+
absoluteLimit: S3_METADATA_LIMIT_BYTES,
|
|
209
|
+
excess: totalSize - effectiveLimit,
|
|
210
|
+
resourceName: resource.name,
|
|
211
|
+
operation: 'upsert',
|
|
212
|
+
id
|
|
213
|
+
});
|
|
190
214
|
}
|
|
191
215
|
return { mappedData, body: "" };
|
|
192
216
|
}
|
package/src/behaviors/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as enforceLimits from './enforce-limits.js';
|
|
|
3
3
|
import * as dataTruncate from './truncate-data.js';
|
|
4
4
|
import * as bodyOverflow from './body-overflow.js';
|
|
5
5
|
import * as bodyOnly from './body-only.js';
|
|
6
|
+
import { BehaviorError } from '../errors.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Available behaviors for Resource metadata handling
|
|
@@ -23,7 +24,11 @@ export const behaviors = {
|
|
|
23
24
|
export function getBehavior(behaviorName) {
|
|
24
25
|
const behavior = behaviors[behaviorName];
|
|
25
26
|
if (!behavior) {
|
|
26
|
-
throw new
|
|
27
|
+
throw new BehaviorError(`Unknown behavior: ${behaviorName}`, {
|
|
28
|
+
behavior: behaviorName,
|
|
29
|
+
availableBehaviors: Object.keys(behaviors),
|
|
30
|
+
operation: 'getBehavior'
|
|
31
|
+
});
|
|
27
32
|
}
|
|
28
33
|
return behavior;
|
|
29
34
|
}
|
package/src/client.class.js
CHANGED
|
@@ -544,7 +544,17 @@ export class Client extends EventEmitter {
|
|
|
544
544
|
});
|
|
545
545
|
this.emit("moveAllObjects", { results, errors }, { prefixFrom, prefixTo });
|
|
546
546
|
if (errors.length > 0) {
|
|
547
|
-
throw new
|
|
547
|
+
throw new UnknownError("Some objects could not be moved", {
|
|
548
|
+
bucket: this.config.bucket,
|
|
549
|
+
operation: 'moveAllObjects',
|
|
550
|
+
prefixFrom,
|
|
551
|
+
prefixTo,
|
|
552
|
+
totalKeys: keys.length,
|
|
553
|
+
failedCount: errors.length,
|
|
554
|
+
successCount: results.length,
|
|
555
|
+
errors: errors.map(e => ({ message: e.message, raw: e.raw })),
|
|
556
|
+
suggestion: 'Check S3 permissions and retry failed objects individually'
|
|
557
|
+
});
|
|
548
558
|
}
|
|
549
559
|
return results;
|
|
550
560
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
+
import { PartitionDriverError } from '../errors.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Robust partition operation queue with retry and persistence
|
|
@@ -107,7 +108,12 @@ export class PartitionQueue extends EventEmitter {
|
|
|
107
108
|
case 'delete':
|
|
108
109
|
return await resource.deletePartitionReferences(data);
|
|
109
110
|
default:
|
|
110
|
-
throw new
|
|
111
|
+
throw new PartitionDriverError(`Unknown partition operation type: ${type}`, {
|
|
112
|
+
driver: 'PartitionQueue',
|
|
113
|
+
operation: type,
|
|
114
|
+
availableOperations: ['create', 'update', 'delete'],
|
|
115
|
+
suggestion: 'Use one of the supported partition operations: create, update, or delete'
|
|
116
|
+
});
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { metadataEncode, metadataDecode } from './metadata-encoding.js';
|
|
32
32
|
import { calculateEffectiveLimit, calculateUTF8Bytes } from './calculator.js';
|
|
33
33
|
import { tryFn } from './try-fn.js';
|
|
34
|
+
import { PluginStorageError, MetadataLimitError, BehaviorError } from '../errors.js';
|
|
34
35
|
|
|
35
36
|
const S3_METADATA_LIMIT = 2047; // AWS S3 metadata limit in bytes
|
|
36
37
|
|
|
@@ -41,10 +42,17 @@ export class PluginStorage {
|
|
|
41
42
|
*/
|
|
42
43
|
constructor(client, pluginSlug) {
|
|
43
44
|
if (!client) {
|
|
44
|
-
throw new
|
|
45
|
+
throw new PluginStorageError('PluginStorage requires a client instance', {
|
|
46
|
+
operation: 'constructor',
|
|
47
|
+
pluginSlug,
|
|
48
|
+
suggestion: 'Pass a valid S3db Client instance when creating PluginStorage'
|
|
49
|
+
});
|
|
45
50
|
}
|
|
46
51
|
if (!pluginSlug) {
|
|
47
|
-
throw new
|
|
52
|
+
throw new PluginStorageError('PluginStorage requires a pluginSlug', {
|
|
53
|
+
operation: 'constructor',
|
|
54
|
+
suggestion: 'Provide a plugin slug (e.g., "eventual-consistency", "cache", "audit")'
|
|
55
|
+
});
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
this.client = client;
|
|
@@ -113,7 +121,15 @@ export class PluginStorage {
|
|
|
113
121
|
const [ok, err] = await tryFn(() => this.client.putObject(putParams));
|
|
114
122
|
|
|
115
123
|
if (!ok) {
|
|
116
|
-
throw new
|
|
124
|
+
throw new PluginStorageError(`Failed to save plugin data`, {
|
|
125
|
+
pluginSlug: this.pluginSlug,
|
|
126
|
+
key,
|
|
127
|
+
operation: 'set',
|
|
128
|
+
behavior,
|
|
129
|
+
ttl,
|
|
130
|
+
original: err,
|
|
131
|
+
suggestion: 'Check S3 permissions and key format'
|
|
132
|
+
});
|
|
117
133
|
}
|
|
118
134
|
}
|
|
119
135
|
|
|
@@ -139,7 +155,13 @@ export class PluginStorage {
|
|
|
139
155
|
if (err.name === 'NoSuchKey' || err.Code === 'NoSuchKey') {
|
|
140
156
|
return null;
|
|
141
157
|
}
|
|
142
|
-
throw new
|
|
158
|
+
throw new PluginStorageError(`Failed to retrieve plugin data`, {
|
|
159
|
+
pluginSlug: this.pluginSlug,
|
|
160
|
+
key,
|
|
161
|
+
operation: 'get',
|
|
162
|
+
original: err,
|
|
163
|
+
suggestion: 'Check if the key exists and S3 permissions are correct'
|
|
164
|
+
});
|
|
143
165
|
}
|
|
144
166
|
|
|
145
167
|
// Metadata is already decoded by Client, but values are strings
|
|
@@ -162,7 +184,13 @@ export class PluginStorage {
|
|
|
162
184
|
data = { ...parsedMetadata, ...body };
|
|
163
185
|
}
|
|
164
186
|
} catch (parseErr) {
|
|
165
|
-
throw new
|
|
187
|
+
throw new PluginStorageError(`Failed to parse JSON body`, {
|
|
188
|
+
pluginSlug: this.pluginSlug,
|
|
189
|
+
key,
|
|
190
|
+
operation: 'get',
|
|
191
|
+
original: parseErr,
|
|
192
|
+
suggestion: 'Body content may be corrupted. Check S3 object integrity'
|
|
193
|
+
});
|
|
166
194
|
}
|
|
167
195
|
}
|
|
168
196
|
|
|
@@ -248,7 +276,15 @@ export class PluginStorage {
|
|
|
248
276
|
);
|
|
249
277
|
|
|
250
278
|
if (!ok) {
|
|
251
|
-
throw new
|
|
279
|
+
throw new PluginStorageError(`Failed to list plugin data`, {
|
|
280
|
+
pluginSlug: this.pluginSlug,
|
|
281
|
+
operation: 'list',
|
|
282
|
+
prefix,
|
|
283
|
+
fullPrefix,
|
|
284
|
+
limit,
|
|
285
|
+
original: err,
|
|
286
|
+
suggestion: 'Check S3 permissions and bucket configuration'
|
|
287
|
+
});
|
|
252
288
|
}
|
|
253
289
|
|
|
254
290
|
// Remove keyPrefix from keys
|
|
@@ -277,7 +313,16 @@ export class PluginStorage {
|
|
|
277
313
|
);
|
|
278
314
|
|
|
279
315
|
if (!ok) {
|
|
280
|
-
throw new
|
|
316
|
+
throw new PluginStorageError(`Failed to list resource data`, {
|
|
317
|
+
pluginSlug: this.pluginSlug,
|
|
318
|
+
operation: 'listForResource',
|
|
319
|
+
resourceName,
|
|
320
|
+
subPrefix,
|
|
321
|
+
fullPrefix,
|
|
322
|
+
limit,
|
|
323
|
+
original: err,
|
|
324
|
+
suggestion: 'Check resource name and S3 permissions'
|
|
325
|
+
});
|
|
281
326
|
}
|
|
282
327
|
|
|
283
328
|
// Remove keyPrefix from keys
|
|
@@ -456,7 +501,13 @@ export class PluginStorage {
|
|
|
456
501
|
const [ok, err] = await tryFn(() => this.client.deleteObject(key));
|
|
457
502
|
|
|
458
503
|
if (!ok) {
|
|
459
|
-
throw new
|
|
504
|
+
throw new PluginStorageError(`Failed to delete plugin data`, {
|
|
505
|
+
pluginSlug: this.pluginSlug,
|
|
506
|
+
key,
|
|
507
|
+
operation: 'delete',
|
|
508
|
+
original: err,
|
|
509
|
+
suggestion: 'Check S3 delete permissions'
|
|
510
|
+
});
|
|
460
511
|
}
|
|
461
512
|
}
|
|
462
513
|
|
|
@@ -686,10 +737,15 @@ export class PluginStorage {
|
|
|
686
737
|
currentSize += keySize + valueSize;
|
|
687
738
|
|
|
688
739
|
if (currentSize > effectiveLimit) {
|
|
689
|
-
throw new
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
740
|
+
throw new MetadataLimitError(`Data exceeds metadata limit with enforce-limits behavior`, {
|
|
741
|
+
totalSize: currentSize,
|
|
742
|
+
effectiveLimit,
|
|
743
|
+
absoluteLimit: S3_METADATA_LIMIT,
|
|
744
|
+
excess: currentSize - effectiveLimit,
|
|
745
|
+
operation: 'PluginStorage.set',
|
|
746
|
+
pluginSlug: this.pluginSlug,
|
|
747
|
+
suggestion: "Use 'body-overflow' or 'body-only' behavior to handle large data"
|
|
748
|
+
});
|
|
693
749
|
}
|
|
694
750
|
|
|
695
751
|
metadata[key] = jsonValue;
|
|
@@ -698,7 +754,13 @@ export class PluginStorage {
|
|
|
698
754
|
}
|
|
699
755
|
|
|
700
756
|
default:
|
|
701
|
-
throw new
|
|
757
|
+
throw new BehaviorError(`Unknown behavior: ${behavior}`, {
|
|
758
|
+
behavior,
|
|
759
|
+
availableBehaviors: ['body-overflow', 'body-only', 'enforce-limits'],
|
|
760
|
+
operation: 'PluginStorage._applyBehavior',
|
|
761
|
+
pluginSlug: this.pluginSlug,
|
|
762
|
+
suggestion: "Use 'body-overflow', 'body-only', or 'enforce-limits'"
|
|
763
|
+
});
|
|
702
764
|
}
|
|
703
765
|
|
|
704
766
|
return { metadata, body };
|
package/src/database.class.js
CHANGED
|
@@ -6,7 +6,7 @@ import jsonStableStringify from "json-stable-stringify";
|
|
|
6
6
|
import Client from "./client.class.js";
|
|
7
7
|
import tryFn from "./concerns/try-fn.js";
|
|
8
8
|
import Resource from "./resource.class.js";
|
|
9
|
-
import { ResourceNotFound } from "./errors.js";
|
|
9
|
+
import { ResourceNotFound, DatabaseError } from "./errors.js";
|
|
10
10
|
import { idGenerator } from "./concerns/id.js";
|
|
11
11
|
import { streamToString } from "./stream/index.js";
|
|
12
12
|
|
|
@@ -35,6 +35,7 @@ export class Database extends EventEmitter {
|
|
|
35
35
|
this.passphrase = options.passphrase || "secret";
|
|
36
36
|
this.versioningEnabled = options.versioningEnabled || false;
|
|
37
37
|
this.persistHooks = options.persistHooks || false; // New configuration for hook persistence
|
|
38
|
+
this.strictValidation = options.strictValidation !== false; // Enable strict validation by default
|
|
38
39
|
|
|
39
40
|
// Initialize hooks system
|
|
40
41
|
this._initHooks();
|
|
@@ -199,6 +200,7 @@ export class Database extends EventEmitter {
|
|
|
199
200
|
asyncEvents: versionData.asyncEvents !== undefined ? versionData.asyncEvents : true,
|
|
200
201
|
hooks: this.persistHooks ? this._deserializeHooks(versionData.hooks || {}) : (versionData.hooks || {}),
|
|
201
202
|
versioningEnabled: this.versioningEnabled,
|
|
203
|
+
strictValidation: this.strictValidation,
|
|
202
204
|
map: versionData.map,
|
|
203
205
|
idGenerator: restoredIdGenerator,
|
|
204
206
|
idSize: restoredIdSize
|
|
@@ -450,7 +452,12 @@ export class Database extends EventEmitter {
|
|
|
450
452
|
const plugin = this.plugins[pluginName] || this.pluginRegistry[pluginName];
|
|
451
453
|
|
|
452
454
|
if (!plugin) {
|
|
453
|
-
throw new
|
|
455
|
+
throw new DatabaseError(`Plugin '${name}' not found`, {
|
|
456
|
+
operation: 'uninstallPlugin',
|
|
457
|
+
pluginName: name,
|
|
458
|
+
availablePlugins: Object.keys(this.pluginRegistry),
|
|
459
|
+
suggestion: 'Check plugin name or list available plugins using Object.keys(db.pluginRegistry)'
|
|
460
|
+
});
|
|
454
461
|
}
|
|
455
462
|
|
|
456
463
|
// Stop the plugin first
|
|
@@ -999,6 +1006,7 @@ export class Database extends EventEmitter {
|
|
|
999
1006
|
autoDecrypt: config.autoDecrypt !== undefined ? config.autoDecrypt : true,
|
|
1000
1007
|
hooks: hooks || {},
|
|
1001
1008
|
versioningEnabled: this.versioningEnabled,
|
|
1009
|
+
strictValidation: this.strictValidation,
|
|
1002
1010
|
map: config.map,
|
|
1003
1011
|
idGenerator: config.idGenerator,
|
|
1004
1012
|
idSize: config.idSize,
|
|
@@ -1215,10 +1223,20 @@ export class Database extends EventEmitter {
|
|
|
1215
1223
|
addHook(event, fn) {
|
|
1216
1224
|
if (!this._hooks) this._initHooks();
|
|
1217
1225
|
if (!this._hooks.has(event)) {
|
|
1218
|
-
throw new
|
|
1226
|
+
throw new DatabaseError(`Unknown hook event: ${event}`, {
|
|
1227
|
+
operation: 'addHook',
|
|
1228
|
+
invalidEvent: event,
|
|
1229
|
+
availableEvents: this._hookEvents,
|
|
1230
|
+
suggestion: `Use one of the available hook events: ${this._hookEvents.join(', ')}`
|
|
1231
|
+
});
|
|
1219
1232
|
}
|
|
1220
1233
|
if (typeof fn !== 'function') {
|
|
1221
|
-
throw new
|
|
1234
|
+
throw new DatabaseError('Hook function must be a function', {
|
|
1235
|
+
operation: 'addHook',
|
|
1236
|
+
event,
|
|
1237
|
+
receivedType: typeof fn,
|
|
1238
|
+
suggestion: 'Provide a function that will be called when the hook event occurs'
|
|
1239
|
+
});
|
|
1222
1240
|
}
|
|
1223
1241
|
this._hooks.get(event).push(fn);
|
|
1224
1242
|
}
|