Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@cboulanger/zotero-sync-bookends

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cboulanger/zotero-sync-bookends - npm Package Compare versions

Comparing version 1.0.1 to 2.0.1

2

package.json
{
"name": "@cboulanger/zotero-sync-bookends",
"version": "1.0.1",
"version": "2.0.1",
"description": "Bookends store for @retorquere/zotero-sync",

@@ -5,0 +5,0 @@ "main": "src/index.ts",

@@ -16,7 +16,37 @@ # Bookends Store for @retorquere/zotero-sync

> Note: This is currently a proof of concept not suitable for production use. Please
> let me know if it works for you and open issues / pull requests if it doesn't.
See [the test script](test.ts) for an example on how to integrate the library in your project.
## Issues
- Access to Bookends via OSA/JXA is slow. It can take a couple of hours to synchronize large libraries.
- The current implementation works well enough for my use case but of course isn't perfect. Please
let me know if it works for you and open issues / pull requests if it doesn't.
- Bookends crashes quite a bit - see below how to recover from the crash without having to restart the
synchronization from scratch.
- Make sure not to modify a library on zotero.org during the initial synchronization, since otherwise
synchronization will abort with a "last-modified-version changed ... retry later" error.
- The implementation stores sync metadata such as the Zotero library version in the Bookends
group names. This isn't a very robust solution but since there is no other place in Bookends
for this kind of data, the best one I could come up with.
## Recovering from a Bookends crash
Unfortunately, especially when syncing large libraries, Bookends sometimes crashes and messes up all the groups.
You can restore the state before the crash like so:
1) Rebuild the library
2) If the groups have disappeared, execute "Flatten Groups List Hierarchy" from the menu in the bottom left corner of
the application window. Now the groups should re-appear.
3) The group names (and the metadata stored in them) often need to be manually repaired. Double-click on them.
For every library that had been fully synchronized before the crash, set the `lastIndex` value to 0 and the
`version` value to the value of `library.version` displayed at `https://api.zotero.org/<prefix>/items/top?key=<your key>`
(the `prefix` is also stored in the group name). You might also need to repair the group name, which is available
from the same JSON data as `library.name`
Then you can restart the synchronization. It will skip and update the previously synchronized libraries. For the others,
it will "fast-forward" (sort of) to the not-yet-synchronized items if the "lastIndex" value has been preserved.
## Testing
The test will actually work out-of-the-box to sync your data if you provide the needed environment variables.
```bash

@@ -30,2 +60,7 @@ git clone https://github.com/cboulanger/zotero-sync-bookends.git

See [the test script](test.ts) for an example on how to integrate the library in your project.
## Resources
- [JXA Release notes](https://developer.apple.com/library/archive/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/Articles/OSX10-10.html#//apple_ref/doc/uid/TP40014508-CH109-SW1)
- [JXA debugging](https://developer.apple.com/library/archive/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/Articles/OSX10-11.html#//apple_ref/doc/uid/TP40014508-CH110-SW1)
- [AppleEvents Error Codes](https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_error_codes.html)

@@ -110,3 +110,3 @@ /**

},
accessDate: 'user20',
accessDate: false,
abstractNote: 'abstract',

@@ -128,3 +128,3 @@ authors: {

bookTitle: 'volume',
collections: 'groups',
collections: "user20",
conferenceName: 'journal',

@@ -406,3 +406,3 @@ callNumber: 'user5',

user16: false,
user19: false,
user19: "collections",
volume: {

@@ -409,0 +409,0 @@ translateName: function (data) {

@@ -134,3 +134,7 @@ /**

bookTitle: 'bookTitle',
collections: false,
collections: {
default: () => [],
translateName: () => 'collections',
translateContent: data => data.split(",")
},
conferenceName: 'conferenceName',

@@ -328,4 +332,4 @@ callNumber: 'callNumber',

*/
const fields_toGlobal =
{ key: {
const fields_toGlobal ={
key: {
translateName : function(data) {

@@ -355,2 +359,6 @@ return false;

abstractNote: 'abstractNote',
collections: {
translateName: () => 'collections',
translateContent: data => data.collections.join(",")
},
creators: {

@@ -357,0 +365,0 @@ translateName: function() {return false;}, // field name depends on content

@@ -5,2 +5,3 @@ import type { Zotero } from "@retorquere/zotero-sync/typings/zotero";

fileName: string;
static verbose: boolean;
constructor(fileName: string);

@@ -27,7 +28,19 @@ /**

version: number;
maxTries: number;
private readonly prefix;
private readonly store;
private groupName?;
private fastForwardTo;
private lastIndex;
private isNew;
private synchronizingMessage;
constructor(store: Store, user_or_group_prefix: string);
/**
* Output additional information on the console if the environment variable
* ZOTERO_SYNC_DEBUG is set
* @param {string} msg
* @private
*/
private debug;
/**
* Runs a JXA script in the context of the current Bookends library. The following

@@ -52,6 +65,20 @@ * constants are predefined when running the passed script fragement:

/**
* Deletes the Bookends group containing the library items
* Adds a Bookends group
* @param groupName
*/
delete(): Promise<void>;
protected addGroup(groupName: string): Promise<void>;
/**
* Renames a Bookends group
* @param oldGroupName
* @param newGroupName
* @protected
*/
protected renameGroup(oldGroupName: string, newGroupName: string): Promise<void>;
/**
* Deletes a Bookends group
* @param groupName
* @protected
*/
protected removeGroup(groupName: string): Promise<void>;
/**
* Parse the metadata stored in a group name

@@ -65,2 +92,3 @@ * @protected

prefix: string;
lastIndex: number;
};

@@ -70,6 +98,6 @@ };

* Store metadata in the group name
* @param name
* @param {string?} name
* @protected
*/
protected generateGroupName(name: string): string;
protected generateGroupName(name?: string): string;
/**

@@ -83,2 +111,6 @@ * Finds a group name by the Zotero library prefix in the contained metadata

/**
* Removes the Bookends group containing the library items
*/
delete(): Promise<void>;
/**
* Adds a Zotero collection object

@@ -100,3 +132,5 @@ * @param {Zotero.Collection} collection

*/
protected generateCitekey(item: Zotero.Item.Any): string;
protected generateCitekey(item: Zotero.Item.Any | {
key: string;
}): string;
/**

@@ -108,10 +142,38 @@ * Translates a zotero item to data that can be imported into bookends

*/
protected zoteroToBookends(item: Zotero.Item.Any | any, citekey: string): object;
protected zoteroToBookends(item: Zotero.Item.Any | any, citekey: string): {
[key: string]: string;
};
/**
* Returns the id of the item with the given citeky, or undefined if none such item exists
* Returns the data of the publication item with the given citeky, or undefined if none such item exists
* @param citekey
* @protected
*/
getIdByCitekey(citekey: string): Promise<string | undefined>;
protected getPublicationByCitekey(citekey: string): Promise<{
[key: string]: string;
} | undefined>;
/**
* Adds a publication item and links it to the current group
* @param {object} data
* @param {string?} groupName Optional name of the group, by default the group that contains the library items
* @protected
*/
protected addPublication(data: {
[key: string]: string;
}, groupName?: string): Promise<void>;
/**
* Updates a publication item
* @param {number} id
* @param {object} data
* @protected
*/
protected updatePublication(id: number, data: {
[key: string]: string;
}): Promise<void>;
/**
* Deletes a publication item
* @param id
* @protected
*/
protected removePublication(id: number): Promise<void>;
/**
* Adds or updates a Zotero item object

@@ -122,3 +184,3 @@ * @param {Zotero.Item.Any} item

/**
* Removes an Zotero item object
* Removes Zotero item objects
* @param {string[]} keys

@@ -128,4 +190,9 @@ */

/**
* Saves the Library
* @param {String?} name Descriptive Name of the library
* Saves the library metadata in the group name
* @protected
*/
protected saveMetadata(): Promise<void>;
/**
* Saves the Library metadata at the end of the sync process
* @param {String} name Descriptive Name of the library
* @param {Number} version

@@ -132,0 +199,0 @@ */

@@ -9,2 +9,3 @@ "use strict";

const { Translator, zoteroDictionary, bookendsDictionary } = require('./dictionaries/translator');
const process = require('process');
class Store {

@@ -39,2 +40,3 @@ constructor(fileName) {

exports.Store = Store;
Store.verbose = false; // set this to true if you want to have verbose output
/**

@@ -48,2 +50,8 @@ * Implementation of a Zotero library object

this.version = 0;
// public properties
this.maxTries = 3; // how often to retry an OSA command if it times out
this.fastForwardTo = 0;
this.lastIndex = 0;
this.isNew = false;
this.synchronizingMessage = "Synchronizing...";
this.store = store;

@@ -53,2 +61,15 @@ this.prefix = user_or_group_prefix;

/**
* Output additional information on the console if the environment variable
* ZOTERO_SYNC_DEBUG is set
* @param {string} msg
* @private
*/
debug(msg) {
if (Store.verbose) {
process.stdout.clearLine();
process.stdout.cursorTo(0);
console.log(msg);
}
}
/**
* Runs a JXA script in the context of the current Bookends library. The following

@@ -84,2 +105,3 @@ * constants are predefined when running the passed script fragement:

this.groupName = await this.findGroupNameByPrefix();
this.lastIndex = 0;
if (this.groupName) {

@@ -89,6 +111,13 @@ const { name, data } = this.parseGroupName(this.groupName);

this.version = data.version;
if (this.version === 0) {
// a case of an aborted initial sync
this.isNew = true;
}
this.fastForwardTo = data.lastIndex || 0;
}
else {
this.groupName = this.generateGroupName("Synchronizing...");
await this.run(`libraryWindow.groupItems.push(bookends.GroupItem({name:\`${this.groupName}\`}));`);
this.isNew = true;
this.name = this.synchronizingMessage;
this.groupName = this.generateGroupName();
await this.addGroup(this.groupName);
}

@@ -98,10 +127,31 @@ return this;

/**
* Deletes the Bookends group containing the library items
* Adds a Bookends group
* @param groupName
*/
async delete() {
async addGroup(groupName) {
await this.run(`libraryWindow.groupItems.push(bookends.GroupItem({name:\`${groupName}\`}));`);
}
/**
* Renames a Bookends group
* @param oldGroupName
* @param newGroupName
* @protected
*/
async renameGroup(oldGroupName, newGroupName) {
await this.run(`libraryWindow.groupItems.byName(\`${oldGroupName}\`).setProperty("name",\`${newGroupName}\`);`);
}
/**
* Deletes a Bookends group
* @param groupName
* @protected
*/
async removeGroup(groupName) {
try {
await this.run(`bookends.delete(libraryWindows.groupItems.byName(\`${this.groupName}\`))`);
await this.run(`bookends.delete(libraryWindow.groupItems.byName(\`${groupName}\`))`);
}
catch (e) {
console.log(e.stderr);
if (e.message.includes("-1728")) {
throw new Error(`Cannot delete non-existing group item with name ${groupName}`);
}
throw e;
}

@@ -124,3 +174,3 @@ }

* Store metadata in the group name
* @param name
* @param {string?} name
* @protected

@@ -131,5 +181,7 @@ */

prefix: this.prefix,
version: this.version
version: this.version,
lastIndex: this.lastIndex
};
return `${name.padEnd(20, " ")} ${JSON.stringify(data)}`;
name = name || this.name;
return `${name.padEnd(50, " ")} ${JSON.stringify(data)}`;
}

@@ -156,2 +208,13 @@ /**

/**
* Removes the Bookends group containing the library items
*/
async delete() {
if (this.groupName) {
await this.removeGroup(this.groupName);
}
else {
throw new Error("Cannot delete Library - bookends group name has not been determined yet.");
}
}
/**
* Adds a Zotero collection object

@@ -192,11 +255,11 @@ * @param {Zotero.Collection} collection

/**
* Returns the id of the item with the given citeky, or undefined if none such item exists
* Returns the data of the publication item with the given citeky, or undefined if none such item exists
* @param citekey
* @protected
*/
async getIdByCitekey(citekey) {
async getPublicationByCitekey(citekey) {
return await this.run(`
const items = bookends.sqlSearch("user1 REGEX '${citekey}'", {in: libraryWindow});
if (items.length) {
return items[0].id()
return items[0].properties();
}

@@ -206,2 +269,55 @@ return undefined;`);

/**
* Adds a publication item and links it to the current group
* @param {object} data
* @param {string?} groupName Optional name of the group, by default the group that contains the library items
* @protected
*/
async addPublication(data, groupName) {
groupName = groupName || this.groupName;
await this.run(`
const item = bookends.PublicationItem(${JSON.stringify(data)});
libraryWindow.publicationItems.push(item);
const group = libraryWindow.groupItems.byName(\`${groupName}\`);
bookends.add(item, {to:group});
`);
}
/**
* Updates a publication item
* @param {number} id
* @param {object} data
* @protected
*/
async updatePublication(id, data) {
try {
await this.run(`
const item = libraryWindow.publicationItems.byId("${id}");
for (let [key, value] of Object.entries(args[0])) {
item.setProperty(key, value);
}
`, [data]);
}
catch (e) {
if (e.message.includes("-1728")) {
throw new Error(`Cannot update non-existing publication item with id ${id}`);
}
throw e;
}
}
/**
* Deletes a publication item
* @param id
* @protected
*/
async removePublication(id) {
try {
await this.run(`bookends.delete(libraryWindow.publicationItems.byId(${id})`);
}
catch (e) {
if (e.message.includes("-1728")) {
throw new Error(`Cannot delete non-existing publication item with id ${id}`);
}
throw e;
}
}
/**
* Adds or updates a Zotero item object

@@ -211,2 +327,10 @@ * @param {Zotero.Item.Any} item

async add(item) {
// fast-forward in the case of a previous aborted sync
if (this.fastForwardTo > 0 && this.lastIndex < this.fastForwardTo) {
if (this.lastIndex === 0) {
this.debug(`Fast-forwarding, skipping previously synchronized items...`);
}
this.lastIndex++;
return;
}
const citekey = this.generateCitekey(item);

@@ -218,20 +342,48 @@ switch (item.itemType) {

default: {
const id = await this.getIdByCitekey(citekey);
const data = this.zoteroToBookends(item, citekey);
if (id) {
await this.run(`
const item = libraryWindow.publicationItems.byId("${id}");
for (let [key, value] of Object.entries(args[0])) {
item.setProperty(key, value);
}
`, [data]);
let tries = 0;
let error = null;
while (tries++ < this.maxTries) {
try {
const storedData = await this.getPublicationByCitekey(citekey);
const data = this.zoteroToBookends(item, citekey);
if (storedData) {
const changedProperties = Object.keys(data).filter(key => data[key] !== storedData[key]);
if (changedProperties.length) {
// update if item has changed
const changedData = {};
changedProperties.forEach(key => changedData[key] = data[key]);
this.debug(`Updating item '${data.title}', properties ${Object.keys(changedData).join(",")}`);
await this.updatePublication(Number(storedData.id), changedData);
}
}
else {
this.debug(`Adding item '${data.title}' ...`);
await this.addPublication(data);
}
// success!
error = null;
break; // break while-loop
}
catch (e) {
if (e.message.includes("-1712")) {
// timeout, try again
error = e;
continue;
}
// try to save metadata, including last index, ignoring any errors
try {
await this.saveMetadata();
}
catch (e) { }
throw e;
}
}
else {
await this.run(`
const item = bookends.PublicationItem(${JSON.stringify(data)});
const group = libraryWindow.groupItems.byName(\`${this.groupName}\`);
libraryWindow.publicationItems.push(item);
bookends.add(item, {to:group});
`);
if (error) {
throw error;
}
this.lastIndex++;
// save metadata every 10 items so that this doesn't slow things down too much
if (this.lastIndex % 10 === 0) {
await this.saveMetadata();
}
}

@@ -241,23 +393,42 @@ }

/**
* Removes an Zotero item object
* Removes Zotero item objects
* @param {string[]} keys
*/
async remove(keys) {
// to do
if (this.isNew) {
// nothing to delete
return;
}
for (let key of keys) {
const item = await this.getPublicationByCitekey(this.generateCitekey({ key }));
if (item) {
this.debug(`Deleting '${item.title}' ...`);
await this.removePublication(Number(item.id));
}
}
}
/**
* Saves the Library
* @param {String?} name Descriptive Name of the library
* Saves the library metadata in the group name
* @protected
*/
async saveMetadata() {
const oldGroupName = await this.findGroupNameByPrefix();
if (!oldGroupName) {
throw new Error("Cannot find group for prefix " + this.prefix);
}
this.groupName = this.generateGroupName();
await this.renameGroup(oldGroupName, this.groupName);
}
/**
* Saves the Library metadata at the end of the sync process
* @param {String} name Descriptive Name of the library
* @param {Number} version
*/
async save(name, version) {
this.name = name;
this.name = name || "User Library";
this.version = version;
const oldGroupName = await this.findGroupNameByPrefix();
this.groupName = this.generateGroupName(name);
await this.run(`
libraryWindow.groupItems.byName(\`${oldGroupName}\`)
.setProperty("name",\`${this.groupName}\`);`);
this.lastIndex = 0;
await this.saveMetadata();
}
}
exports.Library = Library;

@@ -5,7 +5,12 @@ import runJxa from 'run-jxa';

const { Translator, zoteroDictionary, bookendsDictionary } = require('./dictionaries/translator');
const process = require('process');
export class Store implements Zotero.Store {
// interface properties
public libraries : string[];
// public properties
public fileName: string;
public static verbose: boolean = false; // set this to true if you want to have verbose output

@@ -42,4 +47,2 @@ constructor(fileName: string) {

/**

@@ -54,2 +57,5 @@ * Implementation of a Zotero library object

// public properties
public maxTries = 3; // how often to retry an OSA command if it times out
// internal config

@@ -59,2 +65,6 @@ private readonly prefix: string;

private groupName?: string;
private fastForwardTo : number = 0;
private lastIndex : number = 0;
private isNew: boolean = false;
private synchronizingMessage = "Synchronizing...";

@@ -67,2 +77,16 @@ constructor(store: Store, user_or_group_prefix: string) {

/**
* Output additional information on the console if the environment variable
* ZOTERO_SYNC_DEBUG is set
* @param {string} msg
* @private
*/
private debug(msg:string) {
if (Store.verbose) {
process.stdout.clearLine();
process.stdout.cursorTo(0);
console.log(msg);
}
}
/**
* Runs a JXA script in the context of the current Bookends library. The following

@@ -98,2 +122,3 @@ * constants are predefined when running the passed script fragement:

this.groupName = await this.findGroupNameByPrefix();
this.lastIndex = 0;
if (this.groupName) {

@@ -103,5 +128,12 @@ const {name, data} = this.parseGroupName(this.groupName);

this.version = data.version;
if (this.version === 0) {
// a case of an aborted initial sync
this.isNew = true;
}
this.fastForwardTo = data.lastIndex || 0;
} else {
this.groupName = this.generateGroupName("Synchronizing...");
await this.run(`libraryWindow.groupItems.push(bookends.GroupItem({name:\`${this.groupName}\`}));`);
this.isNew = true;
this.name = this.synchronizingMessage;
this.groupName = this.generateGroupName();
await this.addGroup(this.groupName);
}

@@ -112,9 +144,32 @@ return this;

/**
* Deletes the Bookends group containing the library items
* Adds a Bookends group
* @param groupName
*/
public async delete() {
protected async addGroup(groupName:string) {
await this.run(`libraryWindow.groupItems.push(bookends.GroupItem({name:\`${groupName}\`}));`);
}
/**
* Renames a Bookends group
* @param oldGroupName
* @param newGroupName
* @protected
*/
protected async renameGroup(oldGroupName: string, newGroupName: string) {
await this.run(`libraryWindow.groupItems.byName(\`${oldGroupName}\`).setProperty("name",\`${newGroupName}\`);`);
}
/**
* Deletes a Bookends group
* @param groupName
* @protected
*/
protected async removeGroup(groupName:string) {
try {
await this.run(`bookends.delete(libraryWindows.groupItems.byName(\`${this.groupName}\`))`);
} catch (e) {
console.log(e.stderr);
await this.run(`bookends.delete(libraryWindow.groupItems.byName(\`${groupName}\`))`);
} catch(e) {
if (e.message.includes("-1728")) {
throw new Error(`Cannot delete non-existing group item with name ${groupName}`);
}
throw e;
}

@@ -127,3 +182,3 @@ }

*/
protected parseGroupName(groupName: string): {name: string, data: {version:number, prefix:string}} {
protected parseGroupName(groupName: string): {name: string, data: {version:number, prefix:string, lastIndex:number }} {
if (!groupName) {

@@ -140,11 +195,13 @@ throw new Error("Missing group name");

* Store metadata in the group name
* @param name
* @param {string?} name
* @protected
*/
protected generateGroupName(name: string): string {
protected generateGroupName(name?: string): string {
const data = {
prefix: this.prefix,
version: this.version
version: this.version,
lastIndex: this.lastIndex
};
return `${name.padEnd(20, " ")} ${JSON.stringify(data)}`;
name = name || this.name;
return `${name.padEnd(50, " ")} ${JSON.stringify(data)}`;
}

@@ -173,2 +230,13 @@

/**
* Removes the Bookends group containing the library items
*/
public async delete() {
if (this.groupName) {
await this.removeGroup(this.groupName);
} else {
throw new Error("Cannot delete Library - bookends group name has not been determined yet.");
}
}
/**
* Adds a Zotero collection object

@@ -196,3 +264,3 @@ * @param {Zotero.Collection} collection

*/
protected generateCitekey(item: Zotero.Item.Any) : string {
protected generateCitekey(item: Zotero.Item.Any|{key:string}) : string {
return `https://api.zotero.org${this.prefix}/items/${item.key}`;

@@ -207,3 +275,3 @@ }

*/
protected zoteroToBookends(item: Zotero.Item.Any | any, citekey: string): object {
protected zoteroToBookends(item: Zotero.Item.Any | any, citekey: string): { [key: string] : string} {
const data = Translator.translate(item, zoteroDictionary, bookendsDictionary);

@@ -215,11 +283,11 @@ data.citekey = citekey;

/**
* Returns the id of the item with the given citeky, or undefined if none such item exists
* Returns the data of the publication item with the given citeky, or undefined if none such item exists
* @param citekey
* @protected
*/
public async getIdByCitekey(citekey: string) : Promise<string|undefined> {
protected async getPublicationByCitekey(citekey: string) : Promise<{[key:string]:string}|undefined> {
return await this.run(`
const items = bookends.sqlSearch("user1 REGEX '${citekey}'", {in: libraryWindow});
if (items.length) {
return items[0].id()
return items[0].properties();
}

@@ -230,2 +298,56 @@ return undefined;`);

/**
* Adds a publication item and links it to the current group
* @param {object} data
* @param {string?} groupName Optional name of the group, by default the group that contains the library items
* @protected
*/
protected async addPublication(data: {[key:string]:string}, groupName?:string) : Promise<void> {
groupName = groupName || this.groupName;
await this.run(`
const item = bookends.PublicationItem(${JSON.stringify(data)});
libraryWindow.publicationItems.push(item);
const group = libraryWindow.groupItems.byName(\`${groupName}\`);
bookends.add(item, {to:group});
`);
}
/**
* Updates a publication item
* @param {number} id
* @param {object} data
* @protected
*/
protected async updatePublication(id: number, data: {[key:string]:string}) : Promise<void> {
try {
await this.run(`
const item = libraryWindow.publicationItems.byId("${id}");
for (let [key, value] of Object.entries(args[0])) {
item.setProperty(key, value);
}
`, [data]);
} catch(e) {
if (e.message.includes("-1728")) {
throw new Error(`Cannot update non-existing publication item with id ${id}`);
}
throw e;
}
}
/**
* Deletes a publication item
* @param id
* @protected
*/
protected async removePublication(id: number) : Promise<void> {
try {
await this.run(`bookends.delete(libraryWindow.publicationItems.byId(${id})`);
} catch(e) {
if (e.message.includes("-1728")) {
throw new Error(`Cannot delete non-existing publication item with id ${id}`);
}
throw e;
}
}
/**
* Adds or updates a Zotero item object

@@ -235,2 +357,10 @@ * @param {Zotero.Item.Any} item

public async add(item: Zotero.Item.Any | any): Promise<void> {
// fast-forward in the case of a previous aborted sync
if (this.fastForwardTo > 0 && this.lastIndex < this.fastForwardTo) {
if (this.lastIndex === 0) {
this.debug(`Fast-forwarding, skipping previously synchronized items...`);
}
this.lastIndex++;
return;
}
const citekey = this.generateCitekey(item);

@@ -242,19 +372,45 @@ switch (item.itemType) {

default: {
const id = await this.getIdByCitekey(citekey);
const data = this.zoteroToBookends(item, citekey);
if (id) {
await this.run(`
const item = libraryWindow.publicationItems.byId("${id}");
for (let [key, value] of Object.entries(args[0])) {
item.setProperty(key, value);
let tries = 0;
let error: null | string = null;
while(tries++ < this.maxTries) {
try {
const storedData = await this.getPublicationByCitekey(citekey)
const data = this.zoteroToBookends(item, citekey);
if (storedData) {
const changedProperties = Object.keys(data).filter( key => data[key] !== storedData[key]);
if (changedProperties.length) {
// update if item has changed
const changedData: {[key: string]: string} = {};
changedProperties.forEach(key => changedData[key] = data[key]);
this.debug(`Updating item '${data.title}', properties ${Object.keys(changedData).join(",")}`);
await this.updatePublication(Number(storedData.id), changedData);
}
} else {
this.debug(`Adding item '${data.title}' ...`);
await this.addPublication(data);
}
// success!
error = null;
break; // break while-loop
} catch (e) {
if (e.message.includes("-1712")) {
// timeout, try again
error = e;
continue;
}
// try to save metadata, including last index, ignoring any errors
try {
await this.saveMetadata();
} catch (e) {}
throw e;
}
`, [data]);
} else {
await this.run(`
const item = bookends.PublicationItem(${JSON.stringify(data)});
const group = libraryWindow.groupItems.byName(\`${this.groupName}\`);
libraryWindow.publicationItems.push(item);
bookends.add(item, {to:group});
`);
}
if (error) {
throw error;
}
this.lastIndex++;
// save metadata every 10 items so that this doesn't slow things down too much
if (this.lastIndex % 10 === 0) {
await this.saveMetadata();
}
}

@@ -265,23 +421,43 @@ }

/**
* Removes an Zotero item object
* Removes Zotero item objects
* @param {string[]} keys
*/
public async remove(keys: string[]): Promise<void> {
// to do
if (this.isNew) {
// nothing to delete
return;
}
for (let key of keys) {
const item = await this.getPublicationByCitekey(this.generateCitekey({key}));
if (item) {
this.debug(`Deleting '${item.title}' ...`);
await this.removePublication(Number(item.id));
}
}
}
/**
* Saves the Library
* @param {String?} name Descriptive Name of the library
* Saves the library metadata in the group name
* @protected
*/
protected async saveMetadata() {
const oldGroupName = await this.findGroupNameByPrefix();
if (!oldGroupName) {
throw new Error("Cannot find group for prefix " + this.prefix);
}
this.groupName = this.generateGroupName();
await this.renameGroup(oldGroupName, this.groupName);
}
/**
* Saves the Library metadata at the end of the sync process
* @param {String} name Descriptive Name of the library
* @param {Number} version
*/
public async save(name: string, version: number): Promise<void> {
this.name = name;
this.name = name || "User Library";
this.version = version;
const oldGroupName = await this.findGroupNameByPrefix();
this.groupName = this.generateGroupName(name);
await this.run(`
libraryWindow.groupItems.byName(\`${oldGroupName}\`)
.setProperty("name",\`${this.groupName}\`);`);
this.lastIndex = 0;
await this.saveMetadata();
}
}

@@ -20,16 +20,20 @@ import { Sync } from '@retorquere/zotero-sync/index'

Store.verbose = true;
// configure visual feedback
const gauge = new Gauge;
let libraryName:string="";
syncEngine.on(Sync.event.library, (library, index: number, total: number) => {
let name = library.type === "group" ? library.name : "User library";
libraryName = name;
gauge.show(`Saving library "${name}" (${index}/${total})`, index/total);
});
syncEngine.on(Sync.event.remove, (type: string, objects: string[]) => {
gauge.show(`Removing ${objects.length} ${type}`);
gauge.show(`"${libraryName.slice(0,20)}": Removing ${objects.length} ${type}`);
});
syncEngine.on(Sync.event.collection, (collection: Zotero.Collection, index: number, total: number) => {
gauge.show(`Saving collection ${index}/${total}`, index/total);
gauge.show(`"${libraryName.slice(0,20)}": Saving collection ${index}/${total}`, index/total);
});
syncEngine.on(Sync.event.item, (item: Zotero.Item.Any, index: number, total: number) => {
gauge.show(`Saving item ${index}/${total}`, index/total);
gauge.show(`"${libraryName.slice(0,20)}": Saving item ${index}/${total}`, index/total);
});

@@ -36,0 +40,0 @@

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc