319 lines
8.1 KiB
JavaScript
319 lines
8.1 KiB
JavaScript
import { deserializeFromBinary, serializeToBinary } from '@og/binary-struct';
|
|
import { VirtualMethodNotImplementedError } from '@og/error-utils';
|
|
import { pluralize } from 'inflection';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
/**
|
|
* @abstract
|
|
* @class
|
|
* @classdesc Represents a key-value storage interface.
|
|
*/
|
|
export class BaseKeyValueStorage {
|
|
/**
|
|
* Sets a value for the given class and primary key.
|
|
* @param {string} cls - The class identifier.
|
|
* @param {string} pk - The primary key.
|
|
* @param {ArrayBuffer} val - The value to set.
|
|
* @abstruct
|
|
* @async
|
|
*/
|
|
async set(cls, pk, val) {
|
|
throw new VirtualMethodNotImplementedError();
|
|
}
|
|
|
|
/**
|
|
* Retrieves the value associated with the given class and primary key.
|
|
* @param {string} cls - The class identifier.
|
|
* @param {string} pk - The primary key.
|
|
* @abstruct
|
|
* @async
|
|
* @returns {Promise<ArrayBuffer|null>} - The value associated with the class and primary key, or null if not found.
|
|
*/
|
|
async get(cls, pk) {
|
|
throw new VirtualMethodNotImplementedError();
|
|
}
|
|
|
|
/**
|
|
* Checks whether a value exists for the given class and primary key.
|
|
* @param {string} cls - The class identifier.
|
|
* @param {string} pk - The primary key.
|
|
* @abstruct
|
|
* @async
|
|
* @returns {Promise<boolean>} - True if the value exists, otherwise false.
|
|
*/
|
|
async has(cls, pk) {
|
|
throw new VirtualMethodNotImplementedError();
|
|
}
|
|
|
|
/**
|
|
* Deletes the value associated with the given class and primary key.
|
|
* @param {string} cls - The class identifier.
|
|
* @param {string} pk - The primary key.
|
|
* @abstruct
|
|
* @async
|
|
*/
|
|
async destroy(cls, pk) {
|
|
throw new VirtualMethodNotImplementedError();
|
|
}
|
|
|
|
/**
|
|
* Initializes or updates the class.
|
|
* @param {string} cls - The class identifier.
|
|
* @abstruct
|
|
* @async
|
|
*/
|
|
async touchClass(cls) { }
|
|
}
|
|
|
|
/**
|
|
* Represents a key-value storage implementation using the file system.
|
|
* @class
|
|
* @extends {BaseKeyValueStorage}
|
|
*/
|
|
export class FileSystemKeyValueStorage extends BaseKeyValueStorage {
|
|
/**
|
|
* Creates an instance of FileSystemKeyValueStorage.
|
|
* @param {string} root - The root directory where key-value data will be stored.
|
|
*/
|
|
constructor(root) {
|
|
super();
|
|
/** @private @type {string} */
|
|
this.root = root;
|
|
}
|
|
|
|
/**
|
|
* Sets a value for the given class and primary key.
|
|
* @param {string} cls - The class identifier.
|
|
* @param {string} pk - The primary key.
|
|
* @param {ArrayBuffer} val - The value to set.
|
|
* @abstruct
|
|
*/
|
|
async set(cls, pk, val) {
|
|
const filePath = this._getFilePath(cls, pk);
|
|
await fs.writeFile(filePath, Buffer.from(val));
|
|
}
|
|
|
|
/**
|
|
* Retrieves the value associated with the given class and primary key.
|
|
* @param {string} cls - The class identifier.
|
|
* @param {string} pk - The primary key.
|
|
* @abstruct
|
|
* @returns {Promise<ArrayBuffer|null>} - The value associated with the class and primary key, or null if not found.
|
|
*/
|
|
async get(cls, pk) {
|
|
const filepath = this._getFilePath(cls, pk);
|
|
try {
|
|
const data = await fs.readFile(filepath);
|
|
return data.buffer;
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') { // File not found
|
|
return null;
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks whether a value exists for the given class and primary key.
|
|
* @param {string} cls - The class identifier.
|
|
* @param {string} pk - The primary key.
|
|
* @abstruct
|
|
* @returns {Promise<boolean>} - True if the value exists, otherwise false.
|
|
*/
|
|
async has(cls, pk) {
|
|
const filepath = this._getFilePath(cls, pk);
|
|
try {
|
|
await fs.access(filepath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes the value associated with the given class and primary key.
|
|
* @param {string} cls - The class identifier.
|
|
* @param {string} pk - The primary key.
|
|
* @abstruct
|
|
*/
|
|
async destroy(cls, pk) {
|
|
const filepath = this._getFilePath(cls, pk);
|
|
await fs.unlink(filepath);
|
|
}
|
|
|
|
/**
|
|
* Initializes or updates the class by creating a folder for it if it doesn't exist.
|
|
* @param {string} cls - The class identifier.
|
|
* @abstruct
|
|
*/
|
|
async touchClass(cls) {
|
|
const cls_dir = path.join(this.root, cls);
|
|
await fs.mkdir(cls_dir, { recursive: true });
|
|
}
|
|
|
|
/**
|
|
* Constructs the file path for a given class and primary key.
|
|
* @private
|
|
* @param {string} cls - The class identifier.
|
|
* @param {string} pk - The primary key.
|
|
* @returns {string} - The file path.
|
|
*/
|
|
_getFilePath(cls, pk) {
|
|
const cls_dir = path.join(this.root, cls);
|
|
return path.join(cls_dir, pk);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Abstract base class for all models.
|
|
* @abstract
|
|
* @class
|
|
*/
|
|
export class BaseModel {
|
|
/**
|
|
* Gets the primary key of the model.
|
|
* @abstract
|
|
* @returns {string|null} The primary key value or null if not applicable.
|
|
*/
|
|
get pk() {
|
|
throw new VirtualMethodNotImplementedError();
|
|
}
|
|
|
|
/**
|
|
* Asynchronously saves the current model instance to the key-value storage.
|
|
* @throws {Error} - Throws an error if the primary key is not set.
|
|
* @returns {Promise<void>}
|
|
* @abstract
|
|
*/
|
|
async save() {
|
|
throw new VirtualMethodNotImplementedError();
|
|
}
|
|
|
|
/**
|
|
* Asynchronously destroys the current model instance in the key-value storage.
|
|
* @throws {Error} - Throws an error if the primary key is not set.
|
|
* @returns {Promise<void>}
|
|
* @abstract
|
|
*/
|
|
async destroy() {
|
|
throw new VirtualMethodNotImplementedError();
|
|
}
|
|
|
|
/**
|
|
* Asynchronously checks if the current model instance can be created in the key-value storage.
|
|
* @returns {Promise<boolean>} - True if the model can be created, false otherwise.
|
|
* @abstract
|
|
*/
|
|
async canCreate() {
|
|
throw new VirtualMethodNotImplementedError();
|
|
}
|
|
|
|
/**
|
|
* Asynchronously loads a model instance from the key-value storage based on its primary key.
|
|
* @param {string} pk - The primary key of the model to load.
|
|
* @returns {Promise<*>} The loaded model instance or null if not found.
|
|
* @abstract
|
|
*/
|
|
static async load(pk) {
|
|
throw new VirtualMethodNotImplementedError();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A mixin function that creates a model class with basic CRUD operations and storage methods.
|
|
*
|
|
* @param {string} name - The name of the model.
|
|
* @param {BaseKeyValueStorage} kv - The key-value storage for data persistence.
|
|
* @returns {Function} - The generated model class.
|
|
*/
|
|
export function Model(name, kv) {
|
|
const cls = pluralize(name);
|
|
|
|
/**
|
|
* @class
|
|
* @extends BaseModel
|
|
*/
|
|
return class extends BaseModel {
|
|
/**
|
|
* Asynchronously saves the current model instance to the key-value storage.
|
|
* @throws {Error} - Throws an error if the primary key is not set.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async save() {
|
|
const pk = this.pk;
|
|
if (pk == null) {
|
|
throw new PrimaryKeyNotSetError();
|
|
}
|
|
|
|
const buf = serializeToBinary(this, this.constructor);
|
|
await kv.set(cls, pk, buf);
|
|
}
|
|
|
|
/**
|
|
* Asynchronously destroys the current model instance in the key-value storage.
|
|
* @throws {Error} - Throws an error if the primary key is not set.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async destroy() {
|
|
const pk = this.pk;
|
|
if (pk == null) {
|
|
throw new PrimaryKeyNotSetError();
|
|
}
|
|
|
|
await kv.destroy(cls, pk);
|
|
}
|
|
|
|
/**
|
|
* Asynchronously checks if the current model instance can be created in the key-value storage.
|
|
* @returns {Promise<boolean>} - True if the model can be created, false otherwise.
|
|
*/
|
|
async canCreate() {
|
|
const pk = this.pk;
|
|
if (pk == null) {
|
|
return false;
|
|
}
|
|
|
|
return !(await kv.has(cls, pk));
|
|
}
|
|
|
|
/**
|
|
* Asynchronously loads a model instance from the key-value storage based on its primary key.
|
|
* @param {string} pk - The primary key of the model to load.
|
|
* @returns {Promise<*>} - The loaded model instance or null if not found.
|
|
*/
|
|
static async load(pk) {
|
|
const buf = await kv.get(cls, pk);
|
|
if (buf == null) {
|
|
return null;
|
|
}
|
|
|
|
return deserializeFromBinary(new DataView(buf), this);
|
|
}
|
|
|
|
/**
|
|
* Initializes the key-value storage for the current class.
|
|
*/
|
|
static {
|
|
kv.touchClass(cls);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Represents an error that is thrown when the primary key of an object is not set.
|
|
* @class
|
|
* @extends {Error}
|
|
*/
|
|
class PrimaryKeyNotSetError extends Error {
|
|
/**
|
|
* Constructs a new PrimaryKeyNotSetError instance with a default error message.
|
|
* @constructor
|
|
*/
|
|
constructor() {
|
|
super('pk() returns null');
|
|
}
|
|
};
|