diff --git a/README.md b/README.md index 89f5642..171d34b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -# Disk Storage System +# Filesystem Database -We use the disk instead of a database to reduce complexity. We leave the hard -optimization to the filesystem layer. +We just write to the disk to reduce complexity. ## API diff --git a/deno.json b/deno.json index a731f65..395a0cf 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@andyburke/fsdb", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "exports": { ".": "./fsdb.ts", diff --git a/fsdb.ts b/fsdb.ts index 147c7f4..a3716fc 100644 --- a/fsdb.ts +++ b/fsdb.ts @@ -1,3 +1,8 @@ +/** + * We just write to the disk to reduce complexity. + * @module + */ + import * as fs from '@std/fs'; import * as path from '@std/path'; import by_lurid from './organizers/by_lurid.ts'; @@ -29,7 +34,9 @@ export interface FSDB_INDEXER { lookup(value: string, options?: FSDB_SEARCH_OPTIONS): Promise; } -/** 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> { private config: FSDB_COLLECTION_CONFIG; public INDEX: Record>; @@ -93,7 +100,14 @@ export class FSDB_COLLECTION> { 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 { const id: string = item[id_field ?? this.config.id_field]; const path_elements: string[] = this.config.organize(id); @@ -101,11 +115,24 @@ export class FSDB_COLLECTION> { 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 { 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 { const organized_item_path: string = this.get_organized_item_path(item, id_field); const organized_item_dir: string = path.dirname(organized_item_path); @@ -135,7 +162,13 @@ export class FSDB_COLLECTION> { } } - /** 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} Returns a promise for either the item or `null` if it cannot be found. + */ async get(id: string): Promise { const item_path: string = this.get_organized_id_path(id); const item_exists: boolean = await fs.exists(item_path); @@ -155,7 +188,13 @@ export class FSDB_COLLECTION> { 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} Returns a promise for the item after it has been stored. + */ async create(item: T): Promise { const item_path: string = this.get_organized_item_path(item); const item_exists: boolean = await fs.exists(item_path); @@ -176,7 +215,13 @@ export class FSDB_COLLECTION> { 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} Returns a promise for the item after it has been updated. + */ async update(item: T): Promise { const item_path: string = this.get_organized_item_path(item); const id: string = item[this.config.id_field]; @@ -199,7 +244,13 @@ export class FSDB_COLLECTION> { 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} 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 { const item_path = this.get_organized_item_path(item); const item_exists = await fs.exists(item_path); @@ -241,7 +292,13 @@ export class FSDB_COLLECTION> { 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[]>} Returns an array of `WALK_ENTRY` items for items given the input options. + */ async all({ limit = 100, offset = 0, @@ -314,16 +371,22 @@ export class FSDB_COLLECTION> { return results; } - /** Use indexes to search for matching items. */ - async find(criteria: Record, input_options?: FSDB_SEARCH_OPTIONS): Promise[]> { + /** + * Use indexes to search for matching items. + * + * @param {Record} criteria The criteria an item should match, usually a field like `{ email: 'someone@somewhere.com` }` + * @param {FSDB_SEARCH_OPTIONS} options + * @returns {Promise[]>} Returns an array of `WALK_ENTRY` items for items given the input options. + */ + async find(criteria: Record, options?: FSDB_SEARCH_OPTIONS): Promise[]> { if (Deno.env.get('FSDB_PERF')) performance.mark('fsdb_find_begin'); - const options: FSDB_SEARCH_OPTIONS = { + const find_options: FSDB_SEARCH_OPTIONS = { ...{ limit: 100, offset: 0 }, - ...(input_options ?? {}) + ...(options ?? {}) }; const results: WALK_ENTRY[] = []; @@ -333,12 +396,12 @@ export class FSDB_COLLECTION> { const indexer_for_search_key: FSDB_INDEXER | undefined = this.INDEX[search_key]; const value: string = criteria[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 offset = options?.offset ?? 0; + const limit = find_options.limit ?? 100; + const offset = find_options.offset ?? 0; let counter = 0; for await (const item_path of item_paths) { @@ -369,13 +432,19 @@ export class FSDB_COLLECTION> { this.emit('find', { criteria, - options, + options: find_options, 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) { const listeners: ((event: any) => void)[] = this.event_listeners[event] = this.event_listeners[event] ?? []; if (!listeners.includes(handler)) { @@ -393,6 +462,12 @@ export class FSDB_COLLECTION> { } } + /** + * 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) { const listeners: ((event: any) => void)[] = this.event_listeners[event] = this.event_listeners[event] ?? []; if (listeners.includes(handler)) { diff --git a/indexers.ts b/indexers.ts index 4c2e8ce..d995049 100644 --- a/indexers.ts +++ b/indexers.ts @@ -1,3 +1,8 @@ +/** + * FSDB Indexers + * @module + */ + import { FSDB_INDEXER_SYMLINKS } from './indexers/symlinks.ts'; export { FSDB_INDEXER_SYMLINKS }; diff --git a/indexers/symlinks.ts b/indexers/symlinks.ts index 3418140..813cdf3 100644 --- a/indexers/symlinks.ts +++ b/indexers/symlinks.ts @@ -1,3 +1,8 @@ +/** + * FSDB Symlink Indexer + * @module + */ + import * as fs from '@std/fs'; import { FSDB_INDEXER, FSDB_SEARCH_OPTIONS } from '../fsdb.ts'; import * as path from '@std/path'; diff --git a/organizers.ts b/organizers.ts index ea743eb..c616d62 100644 --- a/organizers.ts +++ b/organizers.ts @@ -1,3 +1,8 @@ +/** + * FSDB Organizers + * @module + */ + import by_character from './organizers/by_character.ts'; import by_email from './organizers/by_email.ts'; import by_lurid from './organizers/by_lurid.ts'; diff --git a/organizers/by_character.ts b/organizers/by_character.ts index 52457f0..887a8a8 100644 --- a/organizers/by_character.ts +++ b/organizers/by_character.ts @@ -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'; export default function by_character(value: string): string[] { diff --git a/organizers/by_email.ts b/organizers/by_email.ts index 886be49..9ae17e1 100644 --- a/organizers/by_email.ts +++ b/organizers/by_email.ts @@ -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'; const EMAIL_REGEX = /^(?.+)@(?(?.+)\.(?.+))$/; @@ -16,9 +31,5 @@ export default function by_email(email: string): string[] { 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`]; } diff --git a/organizers/by_lurid.ts b/organizers/by_lurid.ts index f3fb6ff..08202f0 100644 --- a/organizers/by_lurid.ts +++ b/organizers/by_lurid.ts @@ -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'; export default function by_lurid(id: string): string[] { // Replace invalid filename characters and leading dots 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[] = [ sanitized_id.slice(0, 14), sanitized_id.slice(0, 34), diff --git a/organizers/by_phone.ts b/organizers/by_phone.ts index f85f38b..c3627cf 100644 --- a/organizers/by_phone.ts +++ b/organizers/by_phone.ts @@ -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'; 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)}`; - // 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 [ sanitize(country_code ?? '1'), sanitize(area_code),