s3db.js
Version:
Use AWS S3, the world's most reliable document storage, as a database with this ORM.
152 lines (137 loc) • 5.81 kB
JavaScript
import { calculateTotalSize, calculateAttributeSizes, calculateUTF8Bytes } from '../concerns/calculator.js';
import { calculateEffectiveLimit } from '../concerns/calculator.js';
import { S3_METADATA_LIMIT_BYTES } from './enforce-limits.js';
import { tryFn, tryFnSync } from '../concerns/try-fn.js';
const OVERFLOW_FLAG = '$overflow';
const OVERFLOW_FLAG_VALUE = 'true';
const OVERFLOW_FLAG_BYTES = calculateUTF8Bytes(OVERFLOW_FLAG) + calculateUTF8Bytes(OVERFLOW_FLAG_VALUE);
/**
* Body Overflow Behavior Configuration Documentation
*
* The `body-overflow` behavior optimizes metadata usage by sorting attributes by size
* in ascending order and placing as many small attributes as possible in metadata,
* while moving larger attributes to the S3 object body. This maximizes metadata
* utilization while keeping frequently accessed small fields in metadata for fast access.
*
* ## Purpose & Use Cases
* - For objects with mixed field sizes (some small, some large)
* - When you want to optimize for both metadata efficiency and read performance
* - For objects that exceed metadata limits but have important small fields
* - When you need fast access to frequently used small fields
*
* ## How It Works
* 1. Calculates the size of each attribute
* 2. Sorts attributes by size in ascending order (smallest first)
* 3. Fills metadata with small attributes until limit is reached
* 4. Places remaining (larger) attributes in the object body as JSON
* 5. Adds a `$overflow` flag to metadata to indicate body usage
*
* ## Performance Characteristics
* - Fast access to small fields (in metadata)
* - Slower access to large fields (requires body read)
* - Optimized metadata utilization
* - Balanced approach between performance and size efficiency
*
* @example
* // Create a resource with body-overflow behavior
* const resource = await db.createResource({
* name: 'mixed_content',
* attributes: { ... },
* behavior: 'body-overflow'
* });
*
* // Small fields go to metadata, large fields go to body
* const doc = await resource.insert({
* id: 'doc123', // Small -> metadata
* title: 'Short Title', // Small -> metadata
* content: 'Very long...', // Large -> body
* metadata: { ... } // Large -> body
* });
*
* ## Comparison to Other Behaviors
* | Behavior | Metadata Usage | Body Usage | Size Limits | Performance |
* |------------------|----------------|------------|-------------|-------------|
* | body-overflow | Optimized | Overflow | 2KB metadata | Balanced |
* | body-only | Minimal (_v) | All data | 5TB | Slower reads |
* | truncate-data | All (truncated)| None | 2KB metadata | Fast reads |
* | enforce-limits | All (limited) | None | 2KB metadata | Fast reads |
* | user-managed | All (unlimited)| None | S3 limit | Fast reads |
*
* @typedef {Object} BodyOverflowBehaviorConfig
* @property {boolean} [enabled=true] - Whether the behavior is active
* @property {number} [metadataReserve=50] - Reserve bytes for system fields
* @property {string[]} [priorityFields] - Fields that should be prioritized in metadata
* @property {boolean} [preserveOrder=false] - Whether to preserve original field order
*/
export async function handleInsert({ resource, data, mappedData, originalData }) {
const effectiveLimit = calculateEffectiveLimit({
s3Limit: S3_METADATA_LIMIT_BYTES,
systemConfig: {
version: resource.version,
timestamps: resource.config.timestamps,
id: data.id
}
});
const attributeSizes = calculateAttributeSizes(mappedData);
const sortedFields = Object.entries(attributeSizes)
.sort(([, a], [, b]) => a - b);
const metadataFields = {};
const bodyFields = {};
let currentSize = 0;
let willOverflow = false;
// Always include version field first
if (mappedData._v) {
metadataFields._v = mappedData._v;
currentSize += attributeSizes._v;
}
// Reserve space for $overflow if overflow is possible
let reservedLimit = effectiveLimit;
for (const [fieldName, size] of sortedFields) {
if (fieldName === '_v') continue;
if (!willOverflow && (currentSize + size > effectiveLimit)) {
reservedLimit -= OVERFLOW_FLAG_BYTES;
willOverflow = true;
}
if (!willOverflow && (currentSize + size <= reservedLimit)) {
metadataFields[fieldName] = mappedData[fieldName];
currentSize += size;
} else {
bodyFields[fieldName] = mappedData[fieldName];
willOverflow = true;
}
}
if (willOverflow) {
metadataFields[OVERFLOW_FLAG] = OVERFLOW_FLAG_VALUE;
}
const hasOverflow = Object.keys(bodyFields).length > 0;
let body = hasOverflow ? JSON.stringify(bodyFields) : "";
// FIX: Only return metadataFields as mappedData, not full mappedData
return { mappedData: metadataFields, body };
}
export async function handleUpdate({ resource, id, data, mappedData, originalData }) {
// For updates, use the same logic as insert (split fields by size)
return handleInsert({ resource, data, mappedData, originalData });
}
export async function handleUpsert({ resource, id, data, mappedData }) {
return handleInsert({ resource, data, mappedData });
}
export async function handleGet({ resource, metadata, body }) {
// Parse body content if it exists
let bodyData = {};
if (body && body.trim() !== '') {
const [ok, err, parsed] = tryFnSync(() => JSON.parse(body));
if (ok) {
bodyData = parsed;
} else {
bodyData = {};
}
}
// Merge metadata and body data, with metadata taking precedence
const mergedData = {
...bodyData,
...metadata
};
// Remove internal flags from the merged result
delete mergedData.$overflow;
return { metadata: mergedData, body };
}