@uttori/storage-provider-json-memory
Version:
Uttori storage provider using JavaScript objects in memory.
284 lines (262 loc) • 10.4 kB
JavaScript
/* eslint-disable @typescript-eslint/require-await */
import processQuery from './query-tools.js';
let debug = (..._) => {};
/* c8 ignore next */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
try { const { default: d } = await import('debug'); debug = d('Uttori.Plugin.StorageProvider.JSON'); } catch {}
/**
* @typedef UttoriDocument The document object we store, with only the minimum methods we access listed.
* @property {string} slug The unique identifier for the document.
* @property {number} [createDate] The creation date of the document.
* @property {number} [updateDate] The last date the document was updated.
*/
/**
* @typedef StorageProviderConfig The configuration object for the StorageProvider.
* @property {boolean} [updateTimestamps] Should update times be marked at the time of edit.
* @property {boolean} [useHistory] Should history entries be created.
* @property {Record<string, string[]>} [events] The events to listen for.
*/
/**
* Storage for Uttori documents using JSON objects in memory.
* @property {UttoriDocument[]} documents - The collection of documents.
* @property {object} history - The collection of document histories indexes.
* @property {object} histories - The collection of document revisions by index.
* @example <caption>Init StorageProvider</caption>
* const storageProvider = new StorageProvider();
* @class
*/
class StorageProvider {
/**
* Creates an instance of StorageProvider.
* @param {StorageProviderConfig} [config] - A configuration object.
* @class
*/
constructor(config = {}) {
debug('constructor');
this.config = {
updateTimestamps: true,
useHistory: true,
...config,
};
/** @type {Record<string, UttoriDocument>} The collection of documents where the slug is the key and the value is the document. */
this.documents = {};
/** @type {Record<string, string[]>} The collection of document histories indexes. */
this.history = {};
/** @type {Record<string, UttoriDocument>} The collection of document revisions by timestamp. */
this.histories = {};
}
/**
* Returns all documents.
* @returns {Promise<Record<string, UttoriDocument>>} All documents.
* @example
* ```js
* storageProvider.all();
* ➜ { 'first-document': { slug: 'first-document', ... }, ... }
* ```
*/
all = async () => {
debug('all');
return this.documents;
};
/**
* Returns all documents matching a given query.
* @param {string} query - The conditions on which documents should be returned.
* @returns {Promise<number | UttoriDocument[]>} The items matching the supplied query.
*/
getQuery = async (query) => {
debug('getQuery:', query);
return processQuery(query, Object.values(await this.all()));
};
/**
* Returns a document for a given slug.
* @param {string} slug - The slug of the document to be returned.
* @returns {Promise<UttoriDocument|undefined>} The returned UttoriDocument.
*/
get = async (slug) => {
debug('get', slug);
if (!slug) {
debug('get: Cannot get document without slug.');
return;
}
const document = this.documents[slug];
if (!document) {
debug('No document found!');
return;
}
// eslint-disable-next-line consistent-return
return { ...document };
};
/**
* Returns the history of edits for a given slug.
* @param {string} slug - The slug of the document to get history for.
* @returns {Promise<string[]>} The returned history object.
*/
getHistory = async (slug) => {
debug('getHistory', slug);
if (!slug) {
debug('Cannot get document history without slug.', slug);
return [];
}
return this.history[slug] || [];
};
/**
* Returns a specifc revision from the history of edits for a given slug and revision timestamp.
* @param {object} params - The params object.
* @param {string} params.slug - The slug of the document to be returned.
* @param {string|number} params.revision - The unix timestamp of the history to be returned.
* @returns {Promise<UttoriDocument|undefined>} The returned revision of the document.
*/
getRevision = async ({ slug, revision }) => {
debug('getRevision', slug, revision);
if (!slug) {
debug('Cannot get document history without slug.', slug);
return;
}
if (!revision) {
debug('Cannot get document history without revision.', revision);
return;
}
const document = this.history[slug] && this.histories[`${slug}-${revision}`] ? this.histories[`${slug}-${revision}`] : undefined;
if (!document) {
debug(`Document history not found for "${slug}", with revision "${revision}"`);
}
// eslint-disable-next-line consistent-return
return document;
};
/**
* Saves a document to internal array.
* @param {UttoriDocument} document - The document to be added to the collection.
*/
add = async (document) => {
if (!document || !document.slug) {
debug('add: Cannot add, missing slug.');
return;
}
document = { ...document };
const existing = await this.get(document.slug);
if (!existing) {
debug('add: New Document:', document.slug);
const date = document.createDate || Date.now();
document.createDate = date;
document.updateDate = document.createDate;
if (this.config.useHistory) {
await this.updateHistory({ slug: document.slug, content: document });
}
this.documents[document.slug] = document;
const random = Math.random().toString(36).slice(8);
this.history[document.slug] = [`${date}-${random}`];
this.histories[`${document.slug}-${date}-${random}`] = document;
} else {
debug('add: Cannot add, existing document!');
}
};
/**
* Updates a document and saves to memory.
* @private
* @param {object} params - The params object.
* @param {UttoriDocument} params.document - The document to be updated in the collection.
* @param {string} params.originalSlug - The original slug identifying the document, or the slug if it has not changed.
*/
updateValid = async ({ document, originalSlug }) => {
debug('updateValid');
document = { ...document };
if (this.config.updateTimestamps) {
document.updateDate = Date.now();
}
if (this.config.useHistory) {
await this.updateHistory({ slug: document.slug, content: document, originalSlug });
}
this.documents[originalSlug] = document;
};
/**
* Updates a document and figures out how to save to memory.
* Calling with a new document will add that document.
* @param {object} params - The params object.
* @param {UttoriDocument} params.document - The document to be updated in the collection.
* @param {string} params.originalSlug - The original slug identifying the document, or the slug if it has not changed.
*/
update = async ({ document, originalSlug }) => {
debug('update');
if (!document || !document.slug) {
debug('Cannot update, missing slug.');
return;
}
debug('update:', document.slug, originalSlug);
const existing = await this.get(document.slug);
const original = originalSlug ? await this.get(originalSlug) : undefined;
if (existing && original && original.slug !== existing.slug) {
debug(`Cannot update, existing document with slug "${originalSlug}"!`);
} else if (existing && original && original.slug === existing.slug) {
debug(`Updating document with slug "${document.slug}"`);
await this.updateValid({ document, originalSlug });
} else if (existing && !original) {
debug(`Updating document with slug "${document.slug}" but no originalSlug`);
await this.updateValid({ document, originalSlug: document.slug });
} else if (!existing && original) {
debug(`Updating document with slug from "${originalSlug}" to "${document.slug}"`);
await this.updateValid({ document, originalSlug });
} else {
debug(`No document found to update with slug "${originalSlug || ''}", adding document with slug "${document.slug}"`);
await this.add(document);
}
};
/**
* Removes a document from memory.
* @param {string} slug - The slug identifying the document.
*/
delete = async (slug) => {
debug('delete:', slug);
const existing = await this.get(slug);
if (existing) {
debug('Document found, deleting document:', slug);
if (this.config.useHistory) {
await this.updateHistory({ slug, content: existing });
}
delete this.documents[slug];
} else {
debug('Document not found:', slug);
}
};
// Format Specific Methods
/**
* Resets to the initial state.
*/
reset() {
debug('reset');
this.documents = {};
this.history = {};
this.histories = {};
}
/**
* Updates History for a given slug, renaming the key and history key as needed.
* @param {object} params - The params object.
* @param {string} params.slug - The slug of the document to update history for.
* @param {UttoriDocument} params.content - The revision of the document to be saved.
* @param {string} [params.originalSlug] - The original slug identifying the document, or the slug if it has not changed.
*/
updateHistory = async ({ slug, content, originalSlug }) => {
// Rename old history folder if one existed
if (slug && originalSlug && originalSlug !== slug && this.history[originalSlug]) {
debug(`updateHistory: Updating history from "${originalSlug}" to "${slug}"`);
this.history[slug] = [...this.history[originalSlug]];
delete this.history[originalSlug];
// Rename old histories
for (const key of Object.keys(this.histories)) {
if (key.startsWith(`${originalSlug}-`)) {
const new_key = key.replace(originalSlug, slug);
this.histories[new_key] = { ...this.histories[key] };
delete this.histories[key];
}
}
}
if (!Array.isArray(this.history[slug])) {
debug('updateHistory: Creating history...');
this.history[slug] = [];
}
const random = Math.random().toString(36).slice(8);
this.history[slug].push(`${Date.now()}-${random}`);
this.histories[`${slug}-${Date.now()}-${random}`] = content;
debug('updateHistory:', random);
};
}
export default StorageProvider;