s3db.js 11.3.2 → 12.0.1

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.
Files changed (83) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36945 -15510
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +66 -1
  5. package/dist/s3db.es.js +36914 -15534
  6. package/dist/s3db.es.js.map +1 -1
  7. package/mcp/entrypoint.js +58 -0
  8. package/mcp/tools/documentation.js +434 -0
  9. package/mcp/tools/index.js +4 -0
  10. package/package.json +35 -15
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +79 -49
  13. package/src/concerns/base62.js +85 -0
  14. package/src/concerns/dictionary-encoding.js +294 -0
  15. package/src/concerns/geo-encoding.js +256 -0
  16. package/src/concerns/high-performance-inserter.js +34 -30
  17. package/src/concerns/ip.js +325 -0
  18. package/src/concerns/metadata-encoding.js +345 -66
  19. package/src/concerns/money.js +193 -0
  20. package/src/concerns/partition-queue.js +7 -4
  21. package/src/concerns/plugin-storage.js +97 -47
  22. package/src/database.class.js +76 -74
  23. package/src/errors.js +0 -4
  24. package/src/plugins/api/auth/api-key-auth.js +88 -0
  25. package/src/plugins/api/auth/basic-auth.js +154 -0
  26. package/src/plugins/api/auth/index.js +112 -0
  27. package/src/plugins/api/auth/jwt-auth.js +169 -0
  28. package/src/plugins/api/index.js +544 -0
  29. package/src/plugins/api/middlewares/index.js +15 -0
  30. package/src/plugins/api/middlewares/validator.js +185 -0
  31. package/src/plugins/api/routes/auth-routes.js +241 -0
  32. package/src/plugins/api/routes/resource-routes.js +304 -0
  33. package/src/plugins/api/server.js +354 -0
  34. package/src/plugins/api/utils/error-handler.js +147 -0
  35. package/src/plugins/api/utils/openapi-generator.js +1240 -0
  36. package/src/plugins/api/utils/response-formatter.js +218 -0
  37. package/src/plugins/backup/streaming-exporter.js +132 -0
  38. package/src/plugins/backup.plugin.js +103 -50
  39. package/src/plugins/cache/s3-cache.class.js +95 -47
  40. package/src/plugins/cache.plugin.js +107 -9
  41. package/src/plugins/concerns/plugin-dependencies.js +313 -0
  42. package/src/plugins/concerns/prometheus-formatter.js +255 -0
  43. package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
  44. package/src/plugins/consumers/sqs-consumer.js +4 -0
  45. package/src/plugins/costs.plugin.js +255 -39
  46. package/src/plugins/eventual-consistency/helpers.js +15 -1
  47. package/src/plugins/geo.plugin.js +873 -0
  48. package/src/plugins/importer/index.js +1020 -0
  49. package/src/plugins/index.js +11 -0
  50. package/src/plugins/metrics.plugin.js +163 -4
  51. package/src/plugins/queue-consumer.plugin.js +6 -27
  52. package/src/plugins/relation.errors.js +139 -0
  53. package/src/plugins/relation.plugin.js +1242 -0
  54. package/src/plugins/replicator.plugin.js +2 -1
  55. package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
  56. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  57. package/src/plugins/replicators/index.js +28 -3
  58. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  59. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  60. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  61. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  62. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  63. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  64. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  65. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  66. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  67. package/src/plugins/state-machine.plugin.js +122 -68
  68. package/src/plugins/tfstate/README.md +745 -0
  69. package/src/plugins/tfstate/base-driver.js +80 -0
  70. package/src/plugins/tfstate/errors.js +112 -0
  71. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  72. package/src/plugins/tfstate/index.js +2660 -0
  73. package/src/plugins/tfstate/s3-driver.js +192 -0
  74. package/src/plugins/ttl.plugin.js +536 -0
  75. package/src/resource.class.js +315 -36
  76. package/src/s3db.d.ts +66 -1
  77. package/src/schema.class.js +366 -32
  78. package/SECURITY.md +0 -76
  79. package/src/partition-drivers/base-partition-driver.js +0 -106
  80. package/src/partition-drivers/index.js +0 -66
  81. package/src/partition-drivers/memory-partition-driver.js +0 -289
  82. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  83. package/src/partition-drivers/sync-partition-driver.js +0 -38
@@ -13,7 +13,7 @@ import { ResourceReader, ResourceWriter } from "./stream/index.js"
13
13
  import { getBehavior, DEFAULT_BEHAVIOR } from "./behaviors/index.js";
14
14
  import { idGenerator as defaultIdGenerator } from "./concerns/id.js";
15
15
  import { calculateTotalSize, calculateEffectiveLimit } from "./concerns/calculator.js";
16
- import { mapAwsError, InvalidResourceItem, ResourceError, PartitionError } from "./errors.js";
16
+ import { mapAwsError, InvalidResourceItem, ResourceError, PartitionError, ValidationError } from "./errors.js";
17
17
 
18
18
 
19
19
  export class Resource extends AsyncEventEmitter {
@@ -22,7 +22,7 @@ export class Resource extends AsyncEventEmitter {
22
22
  * @param {Object} config - Resource configuration
23
23
  * @param {string} config.name - Resource name
24
24
  * @param {Object} config.client - S3 client instance
25
- * @param {string} [config.version='v0'] - Resource version
25
+ * @param {string} [config.version='v1'] - Resource version
26
26
  * @param {Object} [config.attributes={}] - Resource attributes schema
27
27
  * @param {string} [config.behavior='user-managed'] - Resource behavior strategy
28
28
  * @param {string} [config.passphrase='secret'] - Encryption passphrase
@@ -392,7 +392,8 @@ export class Resource extends AsyncEventEmitter {
392
392
  this.attributes = newAttributes;
393
393
 
394
394
  // Apply configuration to ensure timestamps and hooks are set up
395
- this.applyConfiguration({ map: this.schema?.map });
395
+ // Don't pass old map - let it regenerate with new attributes
396
+ this.applyConfiguration();
396
397
 
397
398
  return { oldAttributes, newAttributes };
398
399
  }
@@ -1250,6 +1251,314 @@ export class Resource extends AsyncEventEmitter {
1250
1251
  }
1251
1252
  }
1252
1253
 
1254
+ /**
1255
+ * Patch resource (partial update optimized for metadata-only behaviors)
1256
+ *
1257
+ * This method provides an optimized update path for resources using metadata-only behaviors
1258
+ * (enforce-limits, truncate-data). It uses HeadObject + CopyObject for atomic updates without
1259
+ * body transfer, eliminating race conditions and reducing latency by ~50%.
1260
+ *
1261
+ * For behaviors that store data in body (body-overflow, body-only), it automatically falls
1262
+ * back to the standard update() method.
1263
+ *
1264
+ * @param {string} id - Resource ID
1265
+ * @param {Object} fields - Fields to update (partial data)
1266
+ * @param {Object} options - Update options
1267
+ * @param {string} options.partition - Partition name (if using partitions)
1268
+ * @param {Object} options.partitionValues - Partition values (if using partitions)
1269
+ * @returns {Promise<Object>} Updated resource data
1270
+ *
1271
+ * @example
1272
+ * // Fast atomic update (enforce-limits behavior)
1273
+ * await resource.patch('user-123', { status: 'active', loginCount: 42 });
1274
+ *
1275
+ * @example
1276
+ * // With partitions
1277
+ * await resource.patch('order-456', { status: 'shipped' }, {
1278
+ * partition: 'byRegion',
1279
+ * partitionValues: { region: 'US' }
1280
+ * });
1281
+ */
1282
+ async patch(id, fields, options = {}) {
1283
+ if (isEmpty(id)) {
1284
+ throw new Error('id cannot be empty');
1285
+ }
1286
+
1287
+ if (!fields || typeof fields !== 'object') {
1288
+ throw new Error('fields must be a non-empty object');
1289
+ }
1290
+
1291
+ const behavior = this.behavior;
1292
+
1293
+ // Check if fields contain dot notation (nested fields)
1294
+ const hasNestedFields = Object.keys(fields).some(key => key.includes('.'));
1295
+
1296
+ // ✅ Optimization: HEAD + COPY for metadata-only behaviors WITHOUT nested fields
1297
+ if ((behavior === 'enforce-limits' || behavior === 'truncate-data') && !hasNestedFields) {
1298
+ return await this._patchViaCopyObject(id, fields, options);
1299
+ }
1300
+
1301
+ // ⚠️ Fallback: GET + merge + PUT for:
1302
+ // - Behaviors with body storage
1303
+ // - Nested field updates (need full object merge)
1304
+ return await this.update(id, fields, options);
1305
+ }
1306
+
1307
+ /**
1308
+ * Internal helper: Optimized patch using HeadObject + CopyObject
1309
+ * Only works for metadata-only behaviors (enforce-limits, truncate-data)
1310
+ * Only for simple field updates (no nested fields with dot notation)
1311
+ * @private
1312
+ */
1313
+ async _patchViaCopyObject(id, fields, options = {}) {
1314
+ const { partition, partitionValues } = options;
1315
+
1316
+ // Build S3 key
1317
+ const key = this.getResourceKey(id);
1318
+
1319
+ // Step 1: HEAD to get current metadata (optimization: no body transfer)
1320
+ const headResponse = await this.client.headObject(key);
1321
+ const currentMetadata = headResponse.Metadata || {};
1322
+
1323
+ // Step 2: Decode metadata to user format
1324
+ let currentData = await this.schema.unmapper(currentMetadata);
1325
+
1326
+ // Ensure ID is present
1327
+ if (!currentData.id) {
1328
+ currentData.id = id;
1329
+ }
1330
+
1331
+ // Step 3: Merge with new fields (simple merge, no nested fields)
1332
+ const fieldsClone = cloneDeep(fields);
1333
+ let mergedData = cloneDeep(currentData);
1334
+
1335
+ for (const [key, value] of Object.entries(fieldsClone)) {
1336
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1337
+ // Merge objects
1338
+ mergedData[key] = merge({}, mergedData[key], value);
1339
+ } else {
1340
+ mergedData[key] = cloneDeep(value);
1341
+ }
1342
+ }
1343
+
1344
+ // Step 4: Update timestamps
1345
+ if (this.config.timestamps) {
1346
+ mergedData.updatedAt = new Date().toISOString();
1347
+ }
1348
+
1349
+ // Step 5: Validate merged data
1350
+ const validationResult = await this.schema.validate(mergedData);
1351
+ if (validationResult !== true) {
1352
+ throw new ValidationError('Validation failed during patch', validationResult);
1353
+ }
1354
+
1355
+ // Step 6: Map/encode data to storage format
1356
+ const newMetadata = await this.schema.mapper(mergedData);
1357
+
1358
+ // Add version metadata
1359
+ newMetadata._v = String(this.version);
1360
+
1361
+ // Step 8: CopyObject with new metadata (atomic operation)
1362
+ await this.client.copyObject({
1363
+ from: key,
1364
+ to: key,
1365
+ metadataDirective: 'REPLACE',
1366
+ metadata: newMetadata
1367
+ });
1368
+
1369
+ // Step 9: Update partitions if needed
1370
+ if (this.config.partitions && Object.keys(this.config.partitions).length > 0) {
1371
+ const oldData = { ...currentData, id };
1372
+ const newData = { ...mergedData, id };
1373
+
1374
+ if (this.config.asyncPartitions) {
1375
+ // Async mode: update in background
1376
+ setImmediate(() => {
1377
+ this.handlePartitionReferenceUpdates(oldData, newData).catch(err => {
1378
+ this.emit('partitionIndexError', {
1379
+ operation: 'patch',
1380
+ id,
1381
+ error: err
1382
+ });
1383
+ });
1384
+ });
1385
+ } else {
1386
+ // Sync mode: wait for completion
1387
+ await this.handlePartitionReferenceUpdates(oldData, newData);
1388
+ }
1389
+ }
1390
+
1391
+ return mergedData;
1392
+ }
1393
+
1394
+ /**
1395
+ * Replace resource (full object replacement without GET)
1396
+ *
1397
+ * This method performs a direct PUT operation without fetching the current object.
1398
+ * Use this when you already have the complete object and want to replace it entirely,
1399
+ * saving 1 S3 request (GET).
1400
+ *
1401
+ * ⚠️ Warning: You must provide ALL required fields. Missing fields will NOT be preserved
1402
+ * from the current object. This method does not merge with existing data.
1403
+ *
1404
+ * @param {string} id - Resource ID
1405
+ * @param {Object} fullData - Complete object data (all required fields)
1406
+ * @param {Object} options - Update options
1407
+ * @param {string} options.partition - Partition name (if using partitions)
1408
+ * @param {Object} options.partitionValues - Partition values (if using partitions)
1409
+ * @returns {Promise<Object>} Replaced resource data
1410
+ *
1411
+ * @example
1412
+ * // Replace entire object (must include ALL required fields)
1413
+ * await resource.replace('user-123', {
1414
+ * name: 'John Doe',
1415
+ * email: 'john@example.com',
1416
+ * status: 'active',
1417
+ * loginCount: 42
1418
+ * });
1419
+ *
1420
+ * @example
1421
+ * // With partitions
1422
+ * await resource.replace('order-456', fullOrderData, {
1423
+ * partition: 'byRegion',
1424
+ * partitionValues: { region: 'US' }
1425
+ * });
1426
+ */
1427
+ async replace(id, fullData, options = {}) {
1428
+ if (isEmpty(id)) {
1429
+ throw new Error('id cannot be empty');
1430
+ }
1431
+
1432
+ if (!fullData || typeof fullData !== 'object') {
1433
+ throw new Error('fullData must be a non-empty object');
1434
+ }
1435
+
1436
+ const { partition, partitionValues } = options;
1437
+
1438
+ // Clone data to avoid mutations
1439
+ const dataClone = cloneDeep(fullData);
1440
+
1441
+ // Apply defaults before timestamps
1442
+ const attributesWithDefaults = this.applyDefaults(dataClone);
1443
+
1444
+ // Add timestamps
1445
+ if (this.config.timestamps) {
1446
+ // Preserve createdAt if provided, otherwise set to now
1447
+ if (!attributesWithDefaults.createdAt) {
1448
+ attributesWithDefaults.createdAt = new Date().toISOString();
1449
+ }
1450
+ attributesWithDefaults.updatedAt = new Date().toISOString();
1451
+ }
1452
+
1453
+ // Ensure ID is set
1454
+ const completeData = { id, ...attributesWithDefaults };
1455
+
1456
+ // Validate data
1457
+ const {
1458
+ errors,
1459
+ isValid,
1460
+ data: validated,
1461
+ } = await this.validate(completeData);
1462
+
1463
+ if (!isValid) {
1464
+ const errorMsg = (errors && errors.length && errors[0].message) ? errors[0].message : 'Replace failed';
1465
+ throw new InvalidResourceItem({
1466
+ bucket: this.client.config.bucket,
1467
+ resourceName: this.name,
1468
+ attributes: completeData,
1469
+ validation: errors,
1470
+ message: errorMsg
1471
+ });
1472
+ }
1473
+
1474
+ // Extract id and attributes from validated data
1475
+ const { id: validatedId, ...validatedAttributes } = validated;
1476
+
1477
+ // Map/encode data to storage format
1478
+ const mappedMetadata = await this.schema.mapper(validatedAttributes);
1479
+
1480
+ // Add version metadata
1481
+ mappedMetadata._v = String(this.version);
1482
+
1483
+ // Use behavior to store data (like insert, not update)
1484
+ const behaviorImpl = getBehavior(this.behavior);
1485
+ const { mappedData: finalMetadata, body } = await behaviorImpl.handleInsert({
1486
+ resource: this,
1487
+ data: validatedAttributes,
1488
+ mappedData: mappedMetadata,
1489
+ originalData: completeData
1490
+ });
1491
+
1492
+ // Build S3 key
1493
+ const key = this.getResourceKey(id);
1494
+
1495
+ // Determine content type based on body content
1496
+ let contentType = undefined;
1497
+ if (body && body !== "") {
1498
+ const [okParse] = await tryFn(() => Promise.resolve(JSON.parse(body)));
1499
+ if (okParse) contentType = 'application/json';
1500
+ }
1501
+
1502
+ // Only throw if behavior is 'body-only' and body is empty
1503
+ if (this.behavior === 'body-only' && (!body || body === "")) {
1504
+ throw new Error(`[Resource.replace] Attempt to save object without body! Data: id=${id}, resource=${this.name}`);
1505
+ }
1506
+
1507
+ // Store to S3 (overwrites if exists, creates if not - true replace/upsert)
1508
+ const [okPut, errPut] = await tryFn(() => this.client.putObject({
1509
+ key,
1510
+ body,
1511
+ contentType,
1512
+ metadata: finalMetadata,
1513
+ }));
1514
+
1515
+ if (!okPut) {
1516
+ const msg = errPut && errPut.message ? errPut.message : '';
1517
+ if (msg.includes('metadata headers exceed') || msg.includes('Replace failed')) {
1518
+ const totalSize = calculateTotalSize(finalMetadata);
1519
+ const effectiveLimit = calculateEffectiveLimit({
1520
+ s3Limit: 2047,
1521
+ systemConfig: {
1522
+ version: this.version,
1523
+ timestamps: this.config.timestamps,
1524
+ id
1525
+ }
1526
+ });
1527
+ const excess = totalSize - effectiveLimit;
1528
+ errPut.totalSize = totalSize;
1529
+ errPut.limit = 2047;
1530
+ errPut.effectiveLimit = effectiveLimit;
1531
+ errPut.excess = excess;
1532
+ throw new ResourceError('metadata headers exceed', { resourceName: this.name, operation: 'replace', id, totalSize, effectiveLimit, excess, suggestion: 'Reduce metadata size or number of fields.' });
1533
+ }
1534
+ throw errPut;
1535
+ }
1536
+
1537
+ // Build the final object to return
1538
+ const replacedObject = { id, ...validatedAttributes };
1539
+
1540
+ // Update partitions if needed
1541
+ if (this.config.partitions && Object.keys(this.config.partitions).length > 0) {
1542
+ if (this.config.asyncPartitions) {
1543
+ // Async mode: update partition indexes in background
1544
+ setImmediate(() => {
1545
+ this.handlePartitionReferenceUpdates({}, replacedObject).catch(err => {
1546
+ this.emit('partitionIndexError', {
1547
+ operation: 'replace',
1548
+ id,
1549
+ error: err
1550
+ });
1551
+ });
1552
+ });
1553
+ } else {
1554
+ // Sync mode: update partition indexes immediately
1555
+ await this.handlePartitionReferenceUpdates({}, replacedObject);
1556
+ }
1557
+ }
1558
+
1559
+ return replacedObject;
1560
+ }
1561
+
1253
1562
  /**
1254
1563
  * Update with conditional check (If-Match ETag)
1255
1564
  * @param {string} id - Resource ID
@@ -2291,7 +2600,7 @@ export class Resource extends AsyncEventEmitter {
2291
2600
 
2292
2601
  /**
2293
2602
  * Get schema for a specific version
2294
- * @param {string} version - Version string (e.g., 'v0', 'v1')
2603
+ * @param {string} version - Version string (e.g., 'v1', 'v2')
2295
2604
  * @returns {Object} Schema object for the version
2296
2605
  */
2297
2606
  async getSchemaForVersion(version) {
@@ -2785,11 +3094,12 @@ export class Resource extends AsyncEventEmitter {
2785
3094
  const [ok, err, unmapped] = await tryFn(() => this.schema.unmapper(metadata));
2786
3095
  unmappedMetadata = ok ? unmapped : metadata;
2787
3096
  // Helper function to filter out internal S3DB fields
3097
+ // Preserve geo-related fields (_geohash, _geohash_zoom*) for GeoPlugin
2788
3098
  const filterInternalFields = (obj) => {
2789
3099
  if (!obj || typeof obj !== 'object') return obj;
2790
3100
  const filtered = {};
2791
3101
  for (const [key, value] of Object.entries(obj)) {
2792
- if (!key.startsWith('_')) {
3102
+ if (!key.startsWith('_') || key === '_geohash' || key.startsWith('_geohash_zoom')) {
2793
3103
  filtered[key] = value;
2794
3104
  }
2795
3105
  }
@@ -2865,37 +3175,6 @@ export class Resource extends AsyncEventEmitter {
2865
3175
  return filtered;
2866
3176
  }
2867
3177
 
2868
-
2869
- async replace(id, attributes) {
2870
- await this.delete(id);
2871
- await new Promise(r => setTimeout(r, 100));
2872
- // Polling para garantir que a key foi removida do S3
2873
- const maxWait = 5000;
2874
- const interval = 50;
2875
- const start = Date.now();
2876
- let waited = 0;
2877
- while (Date.now() - start < maxWait) {
2878
- const exists = await this.exists(id);
2879
- if (!exists) {
2880
- break;
2881
- }
2882
- await new Promise(r => setTimeout(r, interval));
2883
- waited = Date.now() - start;
2884
- }
2885
- if (waited >= maxWait) {
2886
- }
2887
- try {
2888
- const result = await this.insert({ ...attributes, id });
2889
- return result;
2890
- } catch (err) {
2891
- if (err && err.message && err.message.includes('already exists')) {
2892
- const result = await this.update(id, attributes);
2893
- return result;
2894
- }
2895
- throw err;
2896
- }
2897
- }
2898
-
2899
3178
  // --- MIDDLEWARE SYSTEM ---
2900
3179
  _initMiddleware() {
2901
3180
  // Map of methodName -> array of middleware functions
package/src/s3db.d.ts CHANGED
@@ -319,6 +319,20 @@ declare module 's3db.js' {
319
319
  maxResults?: number;
320
320
  }
321
321
 
322
+ /** Geo Plugin resource config */
323
+ export interface GeoResourceConfig {
324
+ latField: string;
325
+ lonField: string;
326
+ precision?: number;
327
+ addGeohash?: boolean;
328
+ }
329
+
330
+ /** Geo Plugin config */
331
+ export interface GeoPluginConfig extends PluginConfig {
332
+ resources?: Record<string, GeoResourceConfig>;
333
+ verbose?: boolean;
334
+ }
335
+
322
336
  /** Metrics Plugin config */
323
337
  export interface MetricsPluginConfig extends PluginConfig {
324
338
  trackLatency?: boolean;
@@ -709,6 +723,8 @@ declare module 's3db.js' {
709
723
  get(id: string): Promise<any>;
710
724
  exists(id: string): Promise<boolean>;
711
725
  update(id: string, attributes: any): Promise<any>;
726
+ patch(id: string, fields: any, options?: { partition?: string; partitionValues?: Record<string, any> }): Promise<any>;
727
+ replace(id: string, fullData: any, options?: { partition?: string; partitionValues?: Record<string, any> }): Promise<any>;
712
728
  upsert(data: any): Promise<any>;
713
729
  delete(id: string): Promise<void>;
714
730
  deleteMany(ids: string[]): Promise<void>;
@@ -827,7 +843,13 @@ declare module 's3db.js' {
827
843
  }): Promise<any>;
828
844
  getObject(key: string): Promise<any>;
829
845
  headObject(key: string): Promise<any>;
830
- copyObject(options: { from: string; to: string }): Promise<any>;
846
+ copyObject(options: {
847
+ from: string;
848
+ to: string;
849
+ metadata?: Record<string, any>;
850
+ metadataDirective?: 'COPY' | 'REPLACE';
851
+ contentType?: string;
852
+ }): Promise<any>;
831
853
  exists(key: string): Promise<boolean>;
832
854
  deleteObject(key: string): Promise<any>;
833
855
  deleteObjects(keys: string[]): Promise<{ deleted: any[]; notFound: any[] }>;
@@ -1016,6 +1038,16 @@ declare module 's3db.js' {
1016
1038
  getIndexStats(): any;
1017
1039
  }
1018
1040
 
1041
+ /** Geo Plugin */
1042
+ export class GeoPlugin extends Plugin {
1043
+ constructor(config?: GeoPluginConfig);
1044
+ encodeGeohash(latitude: number, longitude: number, precision?: number): string;
1045
+ decodeGeohash(geohash: string): { latitude: number; longitude: number; error: { latitude: number; longitude: number } };
1046
+ calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number;
1047
+ getNeighbors(geohash: string): string[];
1048
+ getStats(): any;
1049
+ }
1050
+
1019
1051
  /** Metrics Plugin */
1020
1052
  export class MetricsPlugin extends Plugin {
1021
1053
  constructor(config?: MetricsPluginConfig);
@@ -1254,6 +1286,33 @@ declare module 's3db.js' {
1254
1286
  }
1255
1287
 
1256
1288
  /** Resource extensions added by EventualConsistencyPlugin */
1289
+ export interface GeoResourceExtensions {
1290
+ /** Find locations within radius of a point */
1291
+ findNearby(options: {
1292
+ lat: number;
1293
+ lon: number;
1294
+ radius?: number;
1295
+ limit?: number;
1296
+ }): Promise<Array<any & { _distance: number }>>;
1297
+
1298
+ /** Find locations within bounding box */
1299
+ findInBounds(options: {
1300
+ north: number;
1301
+ south: number;
1302
+ east: number;
1303
+ west: number;
1304
+ limit?: number;
1305
+ }): Promise<any[]>;
1306
+
1307
+ /** Get distance between two records */
1308
+ getDistance(id1: string, id2: string): Promise<{
1309
+ distance: number;
1310
+ unit: string;
1311
+ from: string;
1312
+ to: string;
1313
+ }>;
1314
+ }
1315
+
1257
1316
  export interface EventualConsistencyResourceExtensions {
1258
1317
  /** Set field value (replaces current value) */
1259
1318
  set(id: string, field: string, value: number): Promise<number>;
@@ -1264,6 +1323,12 @@ declare module 's3db.js' {
1264
1323
  /** Decrement field value */
1265
1324
  sub(id: string, field: string, amount: number): Promise<number>;
1266
1325
 
1326
+ /** Increment field value by 1 (shorthand for add(id, field, 1)) */
1327
+ increment(id: string, field: string): Promise<number>;
1328
+
1329
+ /** Decrement field value by 1 (shorthand for sub(id, field, 1)) */
1330
+ decrement(id: string, field: string): Promise<number>;
1331
+
1267
1332
  /** Manually trigger consolidation */
1268
1333
  consolidate(id: string, field: string): Promise<number>;
1269
1334