UNPKG

@cboulanger/zotero-sync-couchbase

Version:

Couchbase store for @retorquere/zotero-sync

293 lines (292 loc) 10 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Library = exports.Store = void 0; const couchbase_1 = __importDefault(require("couchbase")); /** * Helper function to create a couchbase collection. Resolves with the collection object once it has * been created * @param {Bucket} bucket * @param {String} scopeName * @param {String} collectionName * @param {Number} timeout * @return {Promise<Collection>} */ async function createCbCollection(bucket, scopeName, collectionName, timeout) { // create scope try { await bucket.collections().createScope(scopeName); //console.log(`Created scope ${bucketName}.${scopeName}`); } catch (e) { if (!e.toString().includes("exists")) { throw e; } } // create collection let collSpec = { name: collectionName, scopeName: scopeName, maxExpiry: 0 }; try { await bucket.collections().createCollection(collSpec); //console.log(`Created collection ${bucketName}.${scopeName}.${storeName}`); } catch (e) { if (!e.toString().includes("exists")) { throw e; } } const now = new Date().getTime(); let isTimedOut = false; let collection; while (!collection && !isTimedOut) { try { collection = await bucket.scope(scopeName).collection(collectionName); } catch (e) { console.log(e.message); } await new Promise(resolve => setTimeout(resolve, 100)); isTimedOut = new Date().getTime() > now + timeout; } if (isTimedOut) { throw new Error(`Timeout of ${timeout} ms reached when creating collection ${bucket.name}.${scopeName}.${collectionName}`); } // how can I tell Typescript that at this point, collection will always be of type Collection? // @ts-ignore return collection; } /** * Helper function to create a primary index on a collection. Returns a promise that resolves * when the index is online. * @param {Cluster} cluster * @param {String} bucketName * @param {String} scopeName * @param {String} collectionName * @param {Number} timeout */ async function createPrimaryIndex(cluster, bucketName, scopeName, collectionName, timeout) { let query = `create primary index on default:\`${bucketName}\`.\`${scopeName}\`.\`${collectionName}\``; try { await cluster.query(query); //console.log(`Created primary index on default:${this.bucketName}.${scopeName}.${collectionName}`); } catch (e) { let err = e.context ? e.context : e; if (err.first_error_message && err.first_error_message.includes("already exists")) { return; } throw e; } const now = new Date().getTime(); let isTimedOut = false; while (!isTimedOut) { let query = `SELECT RAW state FROM system:indexes WHERE name = "#primary" AND bucket_id = "${bucketName}" AND scope_id ="${scopeName}" AND keyspace_id ="${collectionName}"`; let result = (await cluster.query(query)).rows; if (result.length && result[0].toString() === "online") { break; } await new Promise(resolve => setTimeout(resolve, 100)); isTimedOut = new Date().getTime() > now + timeout; } if (isTimedOut) { throw new Error(`Timeout of ${timeout} ms reached when creating primary index on ${bucketName}.${scopeName}.${collectionName}`); } } class Store { constructor(url, username, password, options = {}) { this.url = url; this.username = username; this.password = password; this.options = options; this.bucketName = options.bucketName || "zotero"; this.timeout = options.timeout || 5000; this.libraries = []; } /** * Returns the couchbase Cluster instance * @return Promise<Cluster> */ async getCluster() { if (!this.cluster) { this.cluster = await couchbase_1.default.connect(this.url, { username: this.username, password: this.password, }); } return this.cluster; } /** * Returns the couchbase bucket in which the zotero data is stored * @returns {Promise<Bucket>} */ async getBucket() { return (await this.getCluster()).bucket(this.bucketName); } /** * Given a zotero REST API user-or-group prefix (e.g. users/12345 or groups/12345), return * a library id that can be stored as a scope name in couchbase. * @param {String} user_or_group_prefix * @protected */ createScopeNameFromPrefix(user_or_group_prefix) { const impl = this.options.createScopeNameFromPrefixFunc || this.createScopeNameFromPrefixImpl; return impl(user_or_group_prefix); } /** * The default implementation shortens the prefix to "u12345" or "g12345", * respectively. * @param {String} user_or_group_prefix * @protected */ createScopeNameFromPrefixImpl(user_or_group_prefix) { return user_or_group_prefix .replace(/roups\/|sers\//, "") .replace("/", ""); } /** * Removes a library from the store * @implements Zotero.Store.remove * @param user_or_group_prefix */ async remove(user_or_group_prefix) { const scopeName = this.createScopeNameFromPrefix(user_or_group_prefix); const bucket = await this.getBucket(); try { await bucket.collections().dropScope(scopeName); } catch (e) { console.error(e); } this.libraries = this.libraries.filter(prefix => prefix !== user_or_group_prefix); } /** * Gets a library, creating it if it doesn't exist. * @implements Zotero.Store.get * @param user_or_group_prefix * @return {Promise<Library>} */ async get(user_or_group_prefix) { const library = new Library(this, user_or_group_prefix); if (!this.libraries.includes(user_or_group_prefix)) { this.libraries.push(user_or_group_prefix); } return await library.init(); } } exports.Store = Store; /** * Implementation of a Zotero library object */ class Library { constructor(store, user_or_group_prefix) { // interface properties this.name = ""; this.version = 0; this.synchronizedObjectTypes = ["items", "collections"]; this.store = store; this.user_or_group_prefix = user_or_group_prefix; this.cbCollections = new Map; } /** * Returns "user" or "group" */ getType() { return this.user_or_group_prefix.startsWith("/users") ? "user" : "group"; } /** * Initialize the library instance. This creates the necessary couchbase * collections. Resolves with the library instance when done. */ async init() { const cluster = await this.store.getCluster(); const bucket = await this.store.getBucket(); const scopeName = this.store.createScopeNameFromPrefix(this.user_or_group_prefix); // create collections for synchronized zotero types plus one for sync metadata const collectionNames = this.synchronizedObjectTypes.concat("meta"); for (const collectionName of collectionNames) { const cbColl = await createCbCollection(bucket, scopeName, collectionName, this.store.timeout); await createPrimaryIndex(cluster, bucket.name, scopeName, collectionName, this.store.timeout); this.cbCollections.set(collectionName, cbColl); } const metaCollection = this.cbCollections.get("meta"); try { this.name = (await metaCollection.get("name")).content; this.version = (await metaCollection.get("version")).content; } catch (e) { if (!e.message.includes("document not found")) { throw e; } } return this; } /** * Adds a Zotero collection object * @param {Zotero.Collection} collection */ async add_collection(collection) { const cbCollection = this.cbCollections.get("collections"); await cbCollection.upsert(collection.key, collection); } /** * Removes a Zotero collection object * @param {string[]} keys */ async remove_collections(keys) { const cbCollection = this.cbCollections.get("collections"); for (const key of keys) { try { await cbCollection.remove(key); } catch (e) { if (!e.message.includes("document not found")) { throw e; } } } } /** * Adds a Zotero item object * @param {Zotero.Item.Any} item */ async add(item) { const cbCollection = this.cbCollections.get("items"); await cbCollection.upsert(item.key, item); } /** * Removes an Zotero item object * @param {string[]} keys */ async remove(keys) { const cbCollection = this.cbCollections.get("items"); for (const key of keys) { try { await cbCollection.remove(key); } catch (e) { if (!e.message.includes("document not found")) { throw e; } } } } /** * Saves the Library * @param {String?} name Descriptive Name of the library * @param {Number} version */ async save(name, version) { const metaCollection = this.cbCollections.get("meta"); await metaCollection.upsert("version", version); await metaCollection.upsert("name", name); } } exports.Library = Library;