@aurios/jason
Version:
A simple, lightweight, and embeddable JSON document database built on Bun.
476 lines (468 loc) • 17.5 kB
TypeScript
import { Schema, Effect, Layer, Context } from 'effect';
import { BadArgument, SystemError, PlatformError } from '@effect/platform/Error';
import { ParseError } from 'effect/ParseResult';
declare const DatabaseError_base: Schema.TaggedErrorClass<DatabaseError, "DatabaseError", {
readonly _tag: Schema.tag<"DatabaseError">;
} & {
message: typeof Schema.String;
cause: typeof Schema.Unknown;
}>;
declare class DatabaseError extends DatabaseError_base {
}
declare const JsonError_base: Schema.TaggedErrorClass<JsonError, "JsonError", {
readonly _tag: Schema.tag<"JsonError">;
} & {
message: typeof Schema.String;
cause: typeof Schema.Unknown;
}>;
declare class JsonError extends JsonError_base {
}
/** Trim left space from a string */
type TrimLeft<T extends string> = T extends ` ${infer R}` ? TrimLeft<R> : T;
/** Trim right space from a string */
type TrimRight<T extends string> = T extends `${infer R} ` ? TrimRight<R> : T;
/** Trim left and right space from a string */
type Trim<T extends string> = TrimLeft<TrimRight<T>>;
type TypeMap = {
string: string;
number: number;
boolean: boolean;
date: Date;
any: any;
unknown: unknown;
null: null;
bigint: bigint;
};
/**
* Parse a type string into a TypeScript type.
*/
type ParseType<T extends string> = Trim<T> extends keyof TypeMap ? TypeMap[Trim<T>] : Trim<T> extends `array<${infer Inner}>` ? ParseType<Inner>[] : Trim<T> extends `record<${infer K},${infer V}>` ? Record<ParseType<K> & (string | number | symbol), ParseType<V>> : ParseTypeName<T>;
type CleanKey<T extends string> = T extends `++${infer K}` | `&${infer K}` | `@${infer K}` | `[${infer K}]` | `*${infer K}` ? K : T;
/**
* Maps a type name string to its corresponding TypeScript type.
* If the type name exists in TypeMap, returns the mapped type; otherwise returns 'any'.
*/
type ParseTypeName<T extends string> = T extends keyof TypeMap ? TypeMap[T] : any;
/**
* Verify if the key of a field has the `*` prefix
*/
type IsMultiEntry<T extends string> = T extends `*${string}` ? true : false;
type ParseField<T extends string> = T extends `${infer Key}:${infer TypeName}` ? {
[K in CleanKey<Trim<Key>>]: IsMultiEntry<Trim<Key>> extends true ? ParseType<TypeName>[] : ParseType<TypeName>;
} : {
[K in CleanKey<Trim<T>>]: IsMultiEntry<Trim<T>> extends true ? string[] : string;
};
type Split<S extends string, D extends string> = string extends S ? string[] : S extends "" ? [] : S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S];
/**
* Convert a union type to an intersection type.
* This is a utility type that is useful for combining multiple types into one.
*/
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
/**
* Parse a JSON Schema string into a JSON Schema object.
* This is a utility type that is useful for converting
* a JSON Schema string into a JSON Schema object.
*/
type ParseSchemaString<T extends string> = UnionToIntersection<ParseField<Split<T, ";">[number]>>;
/**
* Represents a standard schema compliant with @standard-schema/spec.
*/
interface StandardSchemaV1<Input = any, Output = any> {
readonly "~standard": {
readonly version: 1;
readonly vendor: string;
readonly validate: (value: unknown) => StandardSchemaV1.Result<Output> | Promise<StandardSchemaV1.Result<Output>>;
readonly types?: {
readonly input: Input;
readonly output: Output;
};
};
}
declare namespace StandardSchemaV1 {
type Result<Output> = SuccessResult<Output> | FailureResult;
interface SuccessResult<Output> {
readonly value: Output;
readonly issues?: undefined;
}
interface FailureResult {
readonly issues: ReadonlyArray<Issue>;
}
interface Issue {
readonly message: string;
readonly path?: ReadonlyArray<PropertyKey>;
}
}
/**
* Represents a schema definition, which can be either
* an Effect Schema.Struct, a standard-schema compliant validator, or a string.
*/
type SchemaOrString = string | Schema.Schema<any, any> | StandardSchemaV1<any, any>;
type ComparisonOperator<T> = {
_tag: "eq";
value: T;
} | {
_tag: "ne";
value: T;
} | {
_tag: "gt";
value: T;
} | {
_tag: "gte";
value: T;
} | {
_tag: "lt";
value: T;
} | {
_tag: "lte";
value: T;
} | {
_tag: "in";
values: T[];
} | {
_tag: "nin";
values: T[];
} | {
_tag: "startsWith";
value: string;
} | {
_tag: "regex";
pattern: string;
flags?: string | undefined;
};
type LogicalOperator<Doc> = {
_tag: "and";
filters: FilterExpression<Doc>[];
} | {
_tag: "or";
filters: FilterExpression<Doc>[];
} | {
_tag: "not";
filter: FilterExpression<Doc>;
};
type FilterExpression<Doc> = {
[K in keyof Doc]?: Doc[K] | ComparisonOperator<Doc[K]>;
} | LogicalOperator<Doc>;
/**
* A filter object used to specify criteria for querying documents in a collection.
*/
type Filter<Doc> = FilterExpression<Doc>;
interface OrderBy<Doc> {
/**
* The field to order by.
* This should be a key of the document type `Doc`.
*/
field: keyof Doc;
/**
* The order direction, either ascending (`asc`) or descending (`desc`).
*/
order: "asc" | "desc";
}
interface QueryOptions<Doc> {
/**
* The filter criteria to apply to the query.
*
* @example
* db.collections.user.find({
* where: {age: 20}
* })
*/
where: Filter<Doc>;
/**
* The ordering criteria to apply to the query.
*
* @example
* db.collections.user.find({
* where: { age: 20 }
* order_by: { field: 'age', order: 'asc' }
* })
*/
order_by?: OrderBy<Doc>;
/**
* The number of documents to skip in the query results.
*/
skip?: number;
/**
* The maximum number of documents to return in the query results.
*/
limit?: number;
}
/**
* The result of a batch operation.
*/
interface BatchResult {
/** The number of successful operations. */
success: number;
/** A list of failed operations with their index or ID and error message. */
failures: Array<{
index?: number;
id?: string;
error: string;
}>;
}
/**
* Represents batch operations on a collection, returning `Effect` computations.
*/
interface BatchOperationsEffect<Doc> {
/**
* Inserts multiple documents into the collection.
* @param docs The array of documents to insert.
* @returns An `Effect` that resolves to a `BatchResult`.
*/
insert: (docs: Doc[]) => Effect.Effect<BatchResult, DatabaseError>;
/**
* Deletes multiple documents matching the filter.
* @param filter The filter to match documents for deletion.
* @returns An `Effect` that resolves to a `BatchResult`.
*/
delete: (filter: Filter<Doc>) => Effect.Effect<BatchResult, DatabaseError>;
/**
* Updates multiple documents matching the filter.
* @param filter The filter to match documents for update.
* @param data The partial data to update in matching documents.
* @returns An `Effect` that resolves to a `BatchResult`.
*/
update: (filter: Filter<Doc>, data: Partial<Omit<Doc, "id">>) => Effect.Effect<BatchResult, DatabaseError>;
}
/**
* Represents batch operations on a collection.
*/
interface BatchOperations<Doc> {
/**
* Inserts multiple documents into the collection.
* @param docs The array of documents to insert.
* @returns A promise that resolves to a `BatchResult`.
*/
insert: (docs: Doc[]) => Promise<BatchResult>;
/**
* Deletes multiple documents matching the filter.
* @param filter The filter to match documents for deletion.
* @returns A promise that resolves to a `BatchResult`.
*/
delete: (filter: Filter<Doc>) => Promise<BatchResult>;
/**
* Updates multiple documents matching the filter.
* @param filter The filter to match documents for update.
* @param data The partial data to update in matching documents.
* @returns A promise that resolves to a `BatchResult`.
*/
update: (filter: Filter<Doc>, data: Partial<Omit<Doc, "id">>) => Promise<BatchResult>;
}
/**
* Represents a collection of documents, with methods that return `Effect` computations.
* This interface is for users who prefer to work within the `Effect` ecosystem for handling asynchronous operations and errors.
*/
interface CollectionEffect<Doc> {
/**
* Batch operations for the collection.
*/
batch: BatchOperationsEffect<Doc>;
/**
* Creates a new document in the collection.
* @param data The data for the new document, excluding the 'id'.
* @returns An `Effect` that resolves to the created document or fails with an `Error`.
*/
create: (data: Doc) => Effect.Effect<Doc, DatabaseError>;
/**
* Retrieves a document by its ID.
* @param id The ID of the document to retrieve.
* @returns An `Effect` that resolves to the document or `undefined` if not found, and can fail with an `Error`.
*/
findById: (id: string) => Effect.Effect<Doc | undefined, BadArgument | SystemError | JsonError | ParseError>;
/**
* Updates a document by its ID with partial data.
* @param id The ID of the document to update.
* @param data The partial data to update in the document.
* @returns An `Effect` that resolves to the updated document or `undefined` if not found, and can fail with a `DatabaseError`.
*/
update: (id: string, data: Partial<Doc>) => Effect.Effect<Doc | undefined, DatabaseError>;
/**
* Deletes a document by its ID.
* @param id The ID of the document to delete.
* @returns An `Effect` that resolves to `true` if the document was deleted, `false` otherwise, and can fail with a `DatabaseError`.
*/
delete: (id: string) => Effect.Effect<boolean, DatabaseError>;
/**
* Finds documents based on query options.
* @param options Query options for filtering, ordering, and pagination.
* @returns An `Effect` that resolves to an array of documents and can fail with an `Error`.
*/
find: (options: QueryOptions<Doc>) => Effect.Effect<Doc[], Error>;
/**
* Checks if a document with the given ID exists.
* @param id The ID of the document to check.
* @returns An `Effect` that resolves to `true` if the document exists, `false` otherwise, and can fail with a `PlatformError`.
*/
has: (id: string) => Effect.Effect<boolean, PlatformError>;
/**
* Finds a single document based on query options.
* @param options Query options for filtering and ordering.
* @returns An `Effect` that resolves to a single document or `undefined` if not found, and can fail with a `PlatformError`.
*/
findOne: (options: QueryOptions<Doc>) => Effect.Effect<Doc | undefined, PlatformError>;
}
/**
* Represents a collection of documents in the database.
*/
interface Collection<Doc> {
/**
* Batch operations for the collection.
*/
batch: BatchOperations<Doc>;
/**
* Creates a new document in the collection.
* @param data - The data to be stored in the document.
* @returns The created document.
*/
create: (data: Doc) => Promise<Doc>;
/**
* Retrieves a document by its id.
* @param id - The id of the document to retrieve.
* @returns The retrieved document, or `undefined` if not found.
*/
findById: (id: string) => Promise<Doc | undefined>;
/**
* Updates a document by its id.
* @param id - The id of the document to update.
* @param data - The data to update in the document.
* @returns The updated document, or `undefined` if the document with the given id was not found.
*/
update: (id: string, data: Partial<Omit<Doc, "id">>) => Promise<Doc | undefined>;
/**
* Deletes a document by its id.
* @param id - The id of the document to delete.
* @returns A promise that resolves to `true` if the document was deleted, `false` otherwise.
*/
delete: (id: string) => Promise<boolean>;
/**
* Finds documents based on the provided query options.
* @param options - Query options including filtering, ordering, skipping, and limiting.
* @returns A promise that resolves to an array of documents.
*/
find: (options: QueryOptions<Doc>) => Promise<Doc[]>;
/**
* Checks if a document with the given id exists in the collection.
* @param id The id of the document to check.
* @returns A promise that resolves to `true` if the document exists, `false` otherwise.
*/
has: (id: string) => Promise<boolean>;
/**
* Finds a single document based on the provided query options.
* @param options - Query options including filtering and ordering.
* @returns A promise that resolves to a single document or undefined if not found.
*/
findOne: (options: QueryOptions<Doc>) => Promise<Doc | undefined>;
}
/**
* A utility type that infers the document types for all collections defined in the database configuration.
* It maps over the `collections` object and resolves each schema (whether a `Schema` object or a schema string)
* to its corresponding TypeScript type.
*
* @template T - The type of the `collections` configuration object.
*/
type InferCollections<T extends Record<string, SchemaOrString>> = {
[K in keyof T]: T[K] extends Schema.Schema<infer A, any> ? A : T[K] extends StandardSchemaV1<any, infer A> ? A : T[K] extends string ? ParseSchemaString<T[K]> : any;
};
/**
* Configuration options for creating a JasonDB instance.
*/
interface JasonDBConfig<T extends Record<string, SchemaOrString>> {
/**
* The base path where the database files will be stored.
*
* @example
* const db = await createJasonDB({
* base_path: "db_dir",
* });
*/
base_path: string;
/**
* A record defining the schemas for the database collections.
*
* You can define schemas using `Effect.Schema` objects or a convenient string-based syntax.
* The types for your collections are automatically inferred from these definitions.
*
* Example of defining collections with schema strings
* ```ts
* const db = await createJasonDB({
* base_path: "db_dir",
* collections: {
* user: "@id;name;age:number;email;isManager:boolean",
* post: "@id;title;author;*tags"
* }
* });
* ```
*
* Accessing a collection
* ```ts
* const { user } = db.collections;
* ```
*
* ### Schema String Syntax
*
* The string syntax is a shorthand for defining fields and indexes.
* Fields are separated by semicolons `;`.
*
* #### Field Types
*
* Specify a type by appending a colon `:` followed by the type name.
* If no type is specified, it defaults to `string`.
*
* - `fieldName:string` = `string`
* - `fieldName:number` = `number`
* - `fieldName:boolean` = `boolean`
* - `fieldName:date` = `Date`
* - `fieldName:array<type>` = `type[]` (e.g., `items:array<string>`)
* - `fieldName:record<key, value>` = `Record<key, value>` (e.g., `props:record<string, number>`)
*
* #### Index Modifiers
*
* You can prefix a field name with a symbol to create an index.
*
* - `@id`: Primary key (UUID).
* - `++id`: Primary key (auto-incrementing number).
* - `&name`: A unique index on the `name` field.
* - `*tags`: A multi-entry index, ideal for array fields. The field type will be inferred as an array (e.g., `*tags:string` becomes `tags: string[]`).
* - `[name+email]`: A compound index on `name` and `email`.
*
* All defined schemas are validated at runtime using `Effect.Schema`.
*/
collections: T;
/**
* Optional configuration for caching.
*/
cache?: {
/**
* Maximum number of documents to cache per collection.
* @default 1000
*/
document_capacity?: number;
/**
* Maximum number of B-Tree nodes to cache per index.
* @default 1000
*/
index_capacity?: number;
};
}
interface DatabaseEffect<Collections extends Record<string, any>> {
readonly collections: {
[K in keyof Collections]: CollectionEffect<Collections[K]>;
};
}
interface Database<Collections extends Record<string, any>> {
readonly collections: {
[K in keyof Collections]: Collection<Collections[K]>;
};
readonly [Symbol.asyncDispose]: () => Promise<void>;
}
declare const JasonDB_base: Context.TagClass<JasonDB, "DatabaseService", DatabaseEffect<any>>;
declare class JasonDB extends JasonDB_base {
}
declare const createJasonDBLayer: <const T extends Record<string, SchemaOrString>>(config: JasonDBConfig<T>) => Layer.Layer<JasonDB, unknown, unknown>;
/**
* Creates a JasonDB instance based on the provided configuration.
*
* @param config - The configuration object for the JasonDB instance.
* @returns A Promise that resolves to a Database instance with collections defined in the config.
*/
declare const createJasonDB: <const T extends Record<string, SchemaOrString>>(config: JasonDBConfig<T>) => Promise<Database<InferCollections<T>>>;
export { JasonDB, createJasonDB, createJasonDBLayer };