UNPKG

@aurios/jason

Version:

A simple, lightweight, and embeddable JSON document database built on Bun.

476 lines (468 loc) 17.5 kB
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 };