UNPKG

durable-objects-nosql

Version:

MongoDB-style NoSQL interface for Cloudflare Worker Durable Objects

393 lines (392 loc) 11.3 kB
// src/index.ts var CursorImpl = class { documents = []; _limit; _skip; _sort; constructor(documents) { this.documents = documents; } async toArray() { let result = [...this.documents]; if (this._sort) { result.sort((a, b) => { for (const [key, direction] of Object.entries(this._sort)) { const valueA = getNestedProperty(a, key); const valueB = getNestedProperty(b, key); if (valueA < valueB) return -1 * direction; if (valueA > valueB) return 1 * direction; } return 0; }); } if (this._skip) { result = result.slice(this._skip); } if (this._limit) { result = result.slice(0, this._limit); } return result; } async first() { const results = await this.toArray(); return results.length > 0 ? results[0] : null; } async count() { return this.documents.length; } limit(n) { this._limit = n; return this; } skip(n) { this._skip = n; return this; } sort(sortSpec) { this._sort = sortSpec; return this; } }; var CollectionImpl = class { storage; collectionName; constructor(storage, collectionName) { this.storage = storage; this.collectionName = collectionName; } /** * Find documents matching a query */ find(query = {}, options = {}) { const cursor = new CursorImpl([]); cursor._execute = async () => { const documents = await this.getAllDocuments(); return documents.filter((doc) => this.matchesQuery(doc, query)); }; const originalToArray = cursor.toArray; cursor.toArray = async () => { if (!cursor._documents || cursor._documents.length === 0) { ; cursor.documents = await cursor._execute(); } return originalToArray.call(cursor); }; return cursor; } /** * Find a single document matching a query */ async findOne(query = {}) { const cursor = await this.find(query); return cursor.first(); } /** * Insert a single document */ async insertOne(document) { const id = crypto.randomUUID(); const docWithId = { ...document, _id: id }; await this.storage.sql.exec(`INSERT INTO documents (id, collection, data) VALUES (?, ?, ?)`, id, this.collectionName, JSON.stringify(docWithId)); return { id }; } /** * Insert multiple documents */ async insertMany(documents) { const ids = []; await this.storage.transaction(async (txn) => { for (const document of documents) { const id = crypto.randomUUID(); const docWithId = { ...document, _id: id }; await this.storage.sql.exec( `INSERT INTO documents (id, collection, data) VALUES (?, ?, ?)`, id, this.collectionName, JSON.stringify(docWithId) ); ids.push(id); } }); return { ids }; } /** * Update a single document matching a query */ async updateOne(query, update) { const document = await this.findOne(query); if (!document) { return { matchedCount: 0, modifiedCount: 0 }; } const id = document._id; const updatedDocument = this.applyUpdate(document, update); await this.storage.sql.exec(`UPDATE documents SET data = ? WHERE id = ? AND collection = ?`, JSON.stringify(updatedDocument), id, this.collectionName); return { matchedCount: 1, modifiedCount: 1 }; } /** * Update multiple documents matching a query */ async updateMany(query, update) { const cursor = this.find(query); const documents = await cursor.toArray(); if (documents.length === 0) { return { matchedCount: 0, modifiedCount: 0 }; } let modifiedCount = 0; await this.storage.transaction(async (txn) => { for (const document of documents) { const id = document._id; const updatedDocument = this.applyUpdate(document, update); await this.storage.sql.exec( `UPDATE documents SET data = ? WHERE id = ? AND collection = ?`, JSON.stringify(updatedDocument), id, this.collectionName ); modifiedCount++; } }); return { matchedCount: documents.length, modifiedCount }; } /** * Delete a single document matching a query */ async deleteOne(query) { const document = await this.findOne(query); if (!document) { return { deletedCount: 0 }; } const id = document._id; await this.storage.sql.exec(`DELETE FROM documents WHERE id = ? AND collection = ?`, id, this.collectionName); return { deletedCount: 1 }; } /** * Delete multiple documents matching a query */ async deleteMany(query) { const cursor = this.find(query); const documents = await cursor.toArray(); if (documents.length === 0) { return { deletedCount: 0 }; } let deletedCount = 0; await this.storage.transaction(async (txn) => { for (const document of documents) { const id = document._id; await this.storage.sql.exec( `DELETE FROM documents WHERE id = ? AND collection = ?`, id, this.collectionName ); deletedCount++; } }); return { deletedCount }; } /** * Get all documents in the collection */ async getAllDocuments() { const cursor = this.storage.sql.exec(`SELECT * FROM documents WHERE collection = ?`, this.collectionName); const documents = []; let result = cursor.next(); while (!result.done && result.value) { documents.push(JSON.parse(result.value.data)); result = cursor.next(); } return documents; } /** * Check if a document matches a query */ matchesQuery(document, query) { for (const [key, value] of Object.entries(query)) { if (key.startsWith("$")) { switch (key) { case "$and": if (!Array.isArray(value)) return false; if (!value.every((subQuery) => this.matchesQuery(document, subQuery))) return false; break; case "$or": if (!Array.isArray(value)) return false; if (!value.some((subQuery) => this.matchesQuery(document, subQuery))) return false; break; default: return false; } } else { const docValue = getNestedProperty(document, key); if (value !== null && typeof value === "object") { for (const [op, opValue] of Object.entries(value)) { if (!this.matchesOperator(docValue, op, opValue)) return false; } } else if (!this.matchesOperator(docValue, "$eq", value)) { return false; } } } return true; } /** * Check if a value matches an operator condition */ matchesOperator(value, operator, operatorValue) { switch (operator) { case "$eq": return value === operatorValue; case "$gt": return value > operatorValue; case "$gte": return value >= operatorValue; case "$lt": return value < operatorValue; case "$lte": return value <= operatorValue; case "$ne": return value !== operatorValue; case "$in": return Array.isArray(operatorValue) && operatorValue.includes(value); case "$nin": return Array.isArray(operatorValue) && !operatorValue.includes(value); case "$not": return !this.matchesQuery({ value }, { value: operatorValue }); default: return false; } } /** * Apply an update to a document */ applyUpdate(document, update) { const result = { ...document }; for (const [operator, fields] of Object.entries(update)) { switch (operator) { case "$set": for (const [field, value] of Object.entries(fields)) { setNestedProperty(result, field, value); } break; case "$unset": for (const field of Object.keys(fields)) { deleteNestedProperty(result, field); } break; case "$inc": for (const [field, value] of Object.entries(fields)) { const currentValue = getNestedProperty(result, field) || 0; setNestedProperty(result, field, currentValue + value); } break; case "$push": for (const [field, value] of Object.entries(fields)) { const currentValue = getNestedProperty(result, field) || []; if (!Array.isArray(currentValue)) { throw new Error(`Cannot apply $push to non-array field: ${field}`); } setNestedProperty(result, field, [...currentValue, value]); } break; case "$pull": for (const [field, value] of Object.entries(fields)) { const currentValue = getNestedProperty(result, field); if (!Array.isArray(currentValue)) { throw new Error(`Cannot apply $pull to non-array field: ${field}`); } setNestedProperty( result, field, currentValue.filter((item) => !this.matchesQuery({ item }, { item: value })) ); } break; } } return result; } }; var DurableObjectsNoSQL = class { storage; collections = /* @__PURE__ */ new Map(); constructor(storage) { this.storage = storage; this.initializeDatabase(); } /** * Get a collection by name */ collection(name) { if (!this.collections.has(name)) { this.collections.set(name, new CollectionImpl(this.storage, name)); } return this.collections.get(name); } /** * Initialize the database schema */ async initializeDatabase() { this.storage.sql.exec(` CREATE TABLE IF NOT EXISTS documents ( id TEXT PRIMARY KEY, collection TEXT NOT NULL, data TEXT NOT NULL ) `); this.storage.sql.exec(` CREATE INDEX IF NOT EXISTS idx_collection ON documents (collection) `); } }; var handler = { get(target, prop) { if (prop in target) { return target[prop]; } return target.collection(prop); } }; function createNoSQLClient(storage) { const client = new DurableObjectsNoSQL(storage); return new Proxy(client, handler); } function getNestedProperty(obj, path) { const parts = path.split("."); let current = obj; for (const part of parts) { if (current === null || current === void 0) { return void 0; } current = current[part]; } return current; } function setNestedProperty(obj, path, value) { const parts = path.split("."); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (!(part in current)) { current[part] = {}; } current = current[part]; } current[parts[parts.length - 1]] = value; } function deleteNestedProperty(obj, path) { const parts = path.split("."); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (!(part in current)) { return; } current = current[part]; } delete current[parts[parts.length - 1]]; } var index_default = createNoSQLClient; export { DurableObjectsNoSQL, createNoSQLClient, index_default as default, handler };