docs: update README, add some more jsdoc

This commit is contained in:
Andy Burke 2025-07-15 00:15:28 -07:00
parent 22a8b4d03f
commit 55a73c3d0a
10 changed files with 155 additions and 30 deletions

View file

@ -1,7 +1,6 @@
# Disk Storage System # Filesystem Database
We use the disk instead of a database to reduce complexity. We leave the hard We just write to the disk to reduce complexity.
optimization to the filesystem layer.
## API ## API

View file

@ -1,6 +1,6 @@
{ {
"name": "@andyburke/fsdb", "name": "@andyburke/fsdb",
"version": "1.0.0", "version": "1.0.1",
"license": "MIT", "license": "MIT",
"exports": { "exports": {
".": "./fsdb.ts", ".": "./fsdb.ts",

107
fsdb.ts
View file

@ -1,3 +1,8 @@
/**
* We just write to the disk to reduce complexity.
* @module
*/
import * as fs from '@std/fs'; import * as fs from '@std/fs';
import * as path from '@std/path'; import * as path from '@std/path';
import by_lurid from './organizers/by_lurid.ts'; import by_lurid from './organizers/by_lurid.ts';
@ -29,7 +34,9 @@ export interface FSDB_INDEXER<T> {
lookup(value: string, options?: FSDB_SEARCH_OPTIONS<T>): Promise<string[]>; lookup(value: string, options?: FSDB_SEARCH_OPTIONS<T>): Promise<string[]>;
} }
/** Represents a collection of like items within the database on disk. */ /**
* Represents a collection of like items within the database on disk.
*/
export class FSDB_COLLECTION<T extends Record<string, any>> { export class FSDB_COLLECTION<T extends Record<string, any>> {
private config: FSDB_COLLECTION_CONFIG; private config: FSDB_COLLECTION_CONFIG;
public INDEX: Record<string, FSDB_INDEXER<any>>; public INDEX: Record<string, FSDB_INDEXER<any>>;
@ -93,7 +100,14 @@ export class FSDB_COLLECTION<T extends Record<string, any>> {
Deno.writeTextFileSync(collection_info_file_path, collection_info_json); Deno.writeTextFileSync(collection_info_file_path, collection_info_json);
} }
/** Get the "organized" path for the given item within the database. */ /**
* Get the "organized" path for the given item within the database.
*
* @param {any} item The item to organize.
* @param {string} [id_field] Optional override for id_field.
*
* @returns {string} An organized, resolved item path.
*/
public get_organized_item_path(item: any, id_field?: string): string { public get_organized_item_path(item: any, id_field?: string): string {
const id: string = item[id_field ?? this.config.id_field]; const id: string = item[id_field ?? this.config.id_field];
const path_elements: string[] = this.config.organize(id); const path_elements: string[] = this.config.organize(id);
@ -101,11 +115,24 @@ export class FSDB_COLLECTION<T extends Record<string, any>> {
return resolved_item_path; return resolved_item_path;
} }
/** Get the "organized" path for the given id within the database. */ /**
* Get the "organized" path for the given id within the database.
*
* @param {string} id The id to organize.
*
* @returns {string} An organized, resolved path for the id.
*/
public get_organized_id_path(id: string): string { public get_organized_id_path(id: string): string {
return this.get_organized_item_path({ id }, 'id'); return this.get_organized_item_path({ id }, 'id');
} }
/**
* Ensure the item's directory exists.
*
* @param {any} item The item to be stored.
* @param {string} [id_field] Optional override of the id_field.
* @returns {string} An organized, resolved path for the item, with the containing directory created.
*/
private async ensure_item_path(item: any, id_field?: string): Promise<string> { private async ensure_item_path(item: any, id_field?: string): Promise<string> {
const organized_item_path: string = this.get_organized_item_path(item, id_field); const organized_item_path: string = this.get_organized_item_path(item, id_field);
const organized_item_dir: string = path.dirname(organized_item_path); const organized_item_dir: string = path.dirname(organized_item_path);
@ -135,7 +162,13 @@ export class FSDB_COLLECTION<T extends Record<string, any>> {
} }
} }
/** Get an item from the collection given its id. */ /**
* Get an item from the collection.
*
* @param {string} id The item id to look up.
*
* @returns {Promise<T | null>} Returns a promise for either the item or `null` if it cannot be found.
*/
async get(id: string): Promise<T | null> { async get(id: string): Promise<T | null> {
const item_path: string = this.get_organized_id_path(id); const item_path: string = this.get_organized_id_path(id);
const item_exists: boolean = await fs.exists(item_path); const item_exists: boolean = await fs.exists(item_path);
@ -155,7 +188,13 @@ export class FSDB_COLLECTION<T extends Record<string, any>> {
return item; return item;
} }
/** Create an item in the collection. */ /**
* Create an item in the collection.
*
* @param {T} item The item to be stored in the collection.
*
* @returns {Promise<T>} Returns a promise for the item after it has been stored.
*/
async create(item: T): Promise<T> { async create(item: T): Promise<T> {
const item_path: string = this.get_organized_item_path(item); const item_path: string = this.get_organized_item_path(item);
const item_exists: boolean = await fs.exists(item_path); const item_exists: boolean = await fs.exists(item_path);
@ -176,7 +215,13 @@ export class FSDB_COLLECTION<T extends Record<string, any>> {
return item; return item;
} }
/** Update the given item in the collection, requiring the id to be stable. */ /**
* Update the given item in the collection, requiring the id to be stable.
*
* @param {T} item The item to be updated in the collection.
*
* @returns {Promise<T>} Returns a promise for the item after it has been updated.
*/
async update(item: T): Promise<T> { async update(item: T): Promise<T> {
const item_path: string = this.get_organized_item_path(item); const item_path: string = this.get_organized_item_path(item);
const id: string = item[this.config.id_field]; const id: string = item[this.config.id_field];
@ -199,7 +244,13 @@ export class FSDB_COLLECTION<T extends Record<string, any>> {
return item; return item;
} }
/** Delete the given item from the collection. */ /**
* Delete the given item from the collection.
*
* @param {T} item The item to be updated in the collection.
*
* @returns {Promise<T>} Returns a promise for the item after it has been deleted or `null` if the item wasn't in the collection.
*/
async delete(item: T): Promise<T | null> { async delete(item: T): Promise<T | null> {
const item_path = this.get_organized_item_path(item); const item_path = this.get_organized_item_path(item);
const item_exists = await fs.exists(item_path); const item_exists = await fs.exists(item_path);
@ -241,7 +292,13 @@ export class FSDB_COLLECTION<T extends Record<string, any>> {
return item; return item;
} }
/** Iterate through the items. */ /**
* Iterate through all items in the collection.
*
* @param {FSDB_SEARCH_OPTIONS} options The item to be updated in the collection.
*
* @returns {Promise<WALK_ENTRY<T>[]>} Returns an array of `WALK_ENTRY` items for items given the input options.
*/
async all({ async all({
limit = 100, limit = 100,
offset = 0, offset = 0,
@ -314,16 +371,22 @@ export class FSDB_COLLECTION<T extends Record<string, any>> {
return results; return results;
} }
/** Use indexes to search for matching items. */ /**
async find(criteria: Record<string, any>, input_options?: FSDB_SEARCH_OPTIONS<T>): Promise<WALK_ENTRY<T>[]> { * Use indexes to search for matching items.
*
* @param {Record<string, any>} criteria The criteria an item should match, usually a field like `{ email: 'someone@somewhere.com` }`
* @param {FSDB_SEARCH_OPTIONS<T>} options
* @returns {Promise<WALK_ENTRY<T>[]>} Returns an array of `WALK_ENTRY` items for items given the input options.
*/
async find(criteria: Record<string, any>, options?: FSDB_SEARCH_OPTIONS<T>): Promise<WALK_ENTRY<T>[]> {
if (Deno.env.get('FSDB_PERF')) performance.mark('fsdb_find_begin'); if (Deno.env.get('FSDB_PERF')) performance.mark('fsdb_find_begin');
const options: FSDB_SEARCH_OPTIONS<T> = { const find_options: FSDB_SEARCH_OPTIONS<T> = {
...{ ...{
limit: 100, limit: 100,
offset: 0 offset: 0
}, },
...(input_options ?? {}) ...(options ?? {})
}; };
const results: WALK_ENTRY<T>[] = []; const results: WALK_ENTRY<T>[] = [];
@ -333,12 +396,12 @@ export class FSDB_COLLECTION<T extends Record<string, any>> {
const indexer_for_search_key: FSDB_INDEXER<T> | undefined = this.INDEX[search_key]; const indexer_for_search_key: FSDB_INDEXER<T> | undefined = this.INDEX[search_key];
const value: string = criteria[search_key]; const value: string = criteria[search_key];
if (indexer_for_search_key) { if (indexer_for_search_key) {
item_paths.push(...await indexer_for_search_key.lookup(value, input_options)); item_paths.push(...await indexer_for_search_key.lookup(value, find_options));
} }
} }
const limit = options?.limit ?? 100; const limit = find_options.limit ?? 100;
const offset = options?.offset ?? 0; const offset = find_options.offset ?? 0;
let counter = 0; let counter = 0;
for await (const item_path of item_paths) { for await (const item_path of item_paths) {
@ -369,13 +432,19 @@ export class FSDB_COLLECTION<T extends Record<string, any>> {
this.emit('find', { this.emit('find', {
criteria, criteria,
options, options: find_options,
results results
}); });
return results; return results;
} }
/**
* Add an event listener.
*
* @param {string} event The event to listen for.
* @param {(event_data: any) => void} handler The handler for the event.
*/
public on(event: string, handler: (event_data: any) => void) { public on(event: string, handler: (event_data: any) => void) {
const listeners: ((event: any) => void)[] = this.event_listeners[event] = this.event_listeners[event] ?? []; const listeners: ((event: any) => void)[] = this.event_listeners[event] = this.event_listeners[event] ?? [];
if (!listeners.includes(handler)) { if (!listeners.includes(handler)) {
@ -393,6 +462,12 @@ export class FSDB_COLLECTION<T extends Record<string, any>> {
} }
} }
/**
* Remove an event listener.
*
* @param {string} event The event that was listened to.
* @param {(event_data: any) => void} handler The handler that was registered that should be removed.
*/
public off(event: string, handler: (event_data: any) => void) { public off(event: string, handler: (event_data: any) => void) {
const listeners: ((event: any) => void)[] = this.event_listeners[event] = this.event_listeners[event] ?? []; const listeners: ((event: any) => void)[] = this.event_listeners[event] = this.event_listeners[event] ?? [];
if (listeners.includes(handler)) { if (listeners.includes(handler)) {

View file

@ -1,3 +1,8 @@
/**
* FSDB Indexers
* @module
*/
import { FSDB_INDEXER_SYMLINKS } from './indexers/symlinks.ts'; import { FSDB_INDEXER_SYMLINKS } from './indexers/symlinks.ts';
export { FSDB_INDEXER_SYMLINKS }; export { FSDB_INDEXER_SYMLINKS };

View file

@ -1,3 +1,8 @@
/**
* FSDB Symlink Indexer
* @module
*/
import * as fs from '@std/fs'; import * as fs from '@std/fs';
import { FSDB_INDEXER, FSDB_SEARCH_OPTIONS } from '../fsdb.ts'; import { FSDB_INDEXER, FSDB_SEARCH_OPTIONS } from '../fsdb.ts';
import * as path from '@std/path'; import * as path from '@std/path';

View file

@ -1,3 +1,8 @@
/**
* FSDB Organizers
* @module
*/
import by_character from './organizers/by_character.ts'; import by_character from './organizers/by_character.ts';
import by_email from './organizers/by_email.ts'; import by_email from './organizers/by_email.ts';
import by_lurid from './organizers/by_lurid.ts'; import by_lurid from './organizers/by_lurid.ts';

View file

@ -1,3 +1,14 @@
/**
* FSDB Organizer: By Character
*
* Organizes by splitting a string up and using the characters
* for folders, eg:
*
* abcdefg.json => /a/ab/abc/abcdefg.json
*
* @module
*/
import sanitize from '../utils/sanitize.ts'; import sanitize from '../utils/sanitize.ts';
export default function by_character(value: string): string[] { export default function by_character(value: string): string[] {

View file

@ -1,3 +1,18 @@
/**
* FSDB Organizer: By Email
*
* Organizes by splitting an email into its components, eg:
*
* com/example.com/soandso@example.com
*
* A symlinking index based on this organizer might look like:
* fsdb root index tld domain email
* [ ][ V ][ V ][ V ][ V ]
* /path/to/db/root/.indexes/email/com/example.com/soandso@example.com
*
* @module
*/
import sanitize from '../utils/sanitize.ts'; import sanitize from '../utils/sanitize.ts';
const EMAIL_REGEX = /^(?<username>.+)@(?<domain>(?<hostname>.+)\.(?<tld>.+))$/; const EMAIL_REGEX = /^(?<username>.+)@(?<domain>(?<hostname>.+)\.(?<tld>.+))$/;
@ -16,9 +31,5 @@ export default function by_email(email: string): string[] {
return []; return [];
} }
// for example, a symlinking index based on this organizer might look like:
// fsdb root index tld domain email
// [ ][ V ][ V ][ V ][ V ]
// /path/to/db/root/.indexes/email/com/example.com/soandso@example.com
return [sanitize(tld), sanitize(domain), sanitize(email), `${sanitize(email)}.json`]; return [sanitize(tld), sanitize(domain), sanitize(email), `${sanitize(email)}.json`];
} }

View file

@ -1,12 +1,19 @@
/**
* FSDB Organizer: By Lurid
*
* Organizes by splitting a lurid strategically:
*
* able-fish-cost-them/able-fish-cost-them-post-many-form/able-fish-cost-them-post-many-form-hope-wife-born.json
*
* @module
*/
import sanitize from '../utils/sanitize.ts'; import sanitize from '../utils/sanitize.ts';
export default function by_lurid(id: string): string[] { export default function by_lurid(id: string): string[] {
// Replace invalid filename characters and leading dots // Replace invalid filename characters and leading dots
const sanitized_id = sanitize(id); const sanitized_id = sanitize(id);
// assuming a lurid, eg: able-fish-cost-them-post-many-form-hope-wife-born
// ./able-fish-cost-them/able-fish-cost-them-post-many-form/able-fish-cost-them-post-many-form-hope-wife-born.json
const result: string[] = [ const result: string[] = [
sanitized_id.slice(0, 14), sanitized_id.slice(0, 14),
sanitized_id.slice(0, 34), sanitized_id.slice(0, 34),

View file

@ -1,3 +1,13 @@
/**
* FSDB Organizer: By Phone
*
* Organizes by splitting a phone number into its components:
*
* Eg: 1-213-555-1234 => 1/213/555/213-555-1234.json
*
* @module
*/
import sanitize from '../utils/sanitize.ts'; import sanitize from '../utils/sanitize.ts';
const PHONE_REGEX = const PHONE_REGEX =
@ -22,9 +32,6 @@ export default function by_phone(phone: string): string[] {
const normalized_number = `${sanitize(area_code)}-${sanitize(central_office_code)}-${sanitize(subscriber_code)}`; const normalized_number = `${sanitize(area_code)}-${sanitize(central_office_code)}-${sanitize(subscriber_code)}`;
// for example, a symlinking index based on this organizer might look like:
// fsdb root index country_code office_code area_code phone
// /path/to/db/root/.indexes/phone/1/213/555/213-555-1234
return [ return [
sanitize(country_code ?? '1'), sanitize(country_code ?? '1'),
sanitize(area_code), sanitize(area_code),