diff --git a/data.db b/data.db new file mode 100644 index 0000000..bc2c9a8 Binary files /dev/null and b/data.db differ diff --git a/package.json b/package.json index 1d1482d..6dc4f27 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "plugins/session", "plugins/rate-limit", "plugins/edge", - "plugins/jwt" + "plugins/jwt", + "plugins/orm" ], "author": "angga7togk", "license": "MIT", diff --git a/plugins/orm/CHANGELOG.md b/plugins/orm/CHANGELOG.md new file mode 100644 index 0000000..6cbe53b --- /dev/null +++ b/plugins/orm/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +## 1.1.0 + +### Changed +- Improved relation method implementation in BaseModel for better handling of asynchronous operations and parameter passing +- Refactored type definitions and generated declaration files for enhanced TypeScript support +- Updated tests to reflect changes in relation methods and overall functionality + +## 1.0.0 + +### Added +- Initial release of the Gaman ORM plugin +- Core ORM class (`GamanORM`) for database operations via providers +- Base model class (`BaseModel`) with support for CRUD operations +- Automatic data type casting (int, float, string, boolean, json, datetime) +- Model relations: hasMany, belongsTo, hasOne +- SQLite provider implementation for database connectivity +- Sample models demonstrating usage and relations diff --git a/plugins/orm/README.md b/plugins/orm/README.md new file mode 100644 index 0000000..dc4c225 --- /dev/null +++ b/plugins/orm/README.md @@ -0,0 +1,203 @@ +# Gaman ORM Plugin + +The Gaman ORM Plugin provides a lightweight Object-Relational Mapping (ORM) system for Gaman applications. It supports basic CRUD operations, automatic data type casting, and model relations through a provider-based architecture. + +## Features + +- Database-agnostic via providers (e.g., SQLite) +- Automatic data type casting (int, float, string, boolean, json, datetime) +- Model-based relations (hasMany, belongsTo, hasOne) +- Simple query interface +- Lightweight and easy to integrate + +## Installation + +The ORM plugin is part of the Gaman ecosystem. Ensure you have Gaman installed and include the ORM plugin in your project dependencies. + +```bash +npm install @gaman/orm +``` + +## Setup + +1. Import the necessary classes in your application: + +```typescript +import { GamanORM, BaseModel, SQLiteProvider } from '@gaman/orm'; +``` + +2. Initialize the ORM with a provider: + +```typescript +const orm = new GamanORM(new SQLiteProvider()); +await orm.connect(); +``` + +3. Define your models by extending `BaseModel`: + +```typescript +interface User { + id: number; + name: string; + email: string; + created_at: Date; + settings: Record; +} + +class UserModel extends BaseModel { + static options = { + table: 'users', + casts: { + id: 'int', + created_at: 'datetime', + settings: 'json', + }, + }; + + constructor(orm: GamanORM) { + super(orm, UserModel.options); + } +} +``` + +## Usage + +### Basic CRUD Operations + +#### Create +```typescript +const userModel = new UserModel(orm); +await userModel.create({ + name: 'John Doe', + email: 'john@example.com', + settings: { theme: 'dark' } +}); +``` + +#### Read +```typescript +// Find all users +const users = await userModel.find(); + +// Find users with specific criteria +const activeUsers = await userModel.find({ active: true }); + +// Find one user +const user = await userModel.findOne({ id: 1 }); +``` + +#### Update +```typescript +await userModel.update({ id: 1 }, { name: 'Jane Doe' }); +``` + +#### Delete +```typescript +await userModel.delete({ id: 1 }); +``` + +### Data Casting + +The ORM automatically casts data types based on the `casts` option in your model: + +- `int` or `integer`: Converts to number +- `float` or `double`: Converts to number +- `string`: Converts to string +- `bool` or `boolean`: Converts to boolean +- `json`: Parses JSON string or keeps as object +- `datetime`: Converts to Date object + +### Relations + +#### hasMany +```typescript +// Assuming a Post model related to User +class PostModel extends BaseModel { + // ... options +} + +const posts = await userModel.hasManyPosts(1); // Get posts for user ID 1 +``` + +#### belongsTo +```typescript +const user = await postModel.belongsToUser(1); // Get user for post ID 1 +``` + +#### hasOne +```typescript +const profile = await userModel.hasOneProfile(1); // Get profile for user ID 1 +``` + +## Providers + +### SQLite Provider + +The SQLite provider uses `sqlite3` and `sqlite` packages. It connects to a `data.db` file in the current directory. + +```typescript +import { SQLiteProvider } from '@gaman/orm'; + +const provider = new SQLiteProvider(); +await provider.connect(); +// Perform operations +await provider.disconnect(); +``` + +## Example Application + +Here's a complete example: + +```typescript +import { GamanORM, BaseModel, SQLiteProvider } from '@gaman/orm'; + +interface User { + id: number; + name: string; + email: string; + created_at: Date; +} + +class UserModel extends BaseModel { + static options = { + table: 'users', + casts: { + id: 'int', + created_at: 'datetime', + }, + }; + + constructor(orm: GamanORM) { + super(orm, UserModel.options); + } +} + +async function main() { + const orm = new GamanORM(new SQLiteProvider()); + await orm.connect(); + + const userModel = new UserModel(orm); + + // Create a user + await userModel.create({ + name: 'Alice', + email: 'alice@example.com', + }); + + // Find users + const users = await userModel.find(); + console.log(users); + + await orm.disconnect(); +} + +main().catch(console.error); +``` + +## Notes + +- Ensure your database tables exist before performing operations. +- The ORM does not handle migrations; use your database tools for schema management. +- For production, consider using more robust providers or databases. + +For more advanced usage, refer to the source code in `index.ts`, `orm.ts`, and `model/base.ts`. \ No newline at end of file diff --git a/plugins/orm/data.db b/plugins/orm/data.db new file mode 100644 index 0000000..4c941af Binary files /dev/null and b/plugins/orm/data.db differ diff --git a/plugins/orm/index.d.ts b/plugins/orm/index.d.ts index 9ba4c4d..ae8bf37 100644 --- a/plugins/orm/index.d.ts +++ b/plugins/orm/index.d.ts @@ -1,29 +1,38 @@ /** - * @module - * CORS Middleware for Gaman. - * Implements Cross-Origin Resource Sharing (CORS) with customizable options. + * @fileoverview Gaman ORM Plugin + * + * This module provides a lightweight Object-Relational Mapping (ORM) system for Gaman applications. + * It supports basic CRUD operations, data casting, and model relations through a provider-based architecture. + * + * Key features: + * - Database-agnostic via providers (e.g., SQLite) + * - Automatic data type casting + * - Model-based relations (hasMany, belongsTo, hasOne) + * - Simple query interface + * + * @example + * ```typescript + * import { GamanORM, BaseModel, SQLiteProvider } from '@gaman/orm'; + * + * const orm = new GamanORM(new SQLiteProvider()); + * class User extends BaseModel { + * // Define model options + * } + * ``` */ -import { AppConfig, Handler } from "@gaman/core/types"; /** - * CORS middleware options. + * The main ORM class that handles database connections and operations. */ -export type CorsOptions = { - /** Allowed origin(s) for the request. */ - origin?: string | string[] | null; - /** HTTP methods allowed for the request. Default: `["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"]` */ - allowMethods?: string[]; - /** Headers allowed in the request. Default: `["Content-Type", "Authorization"]` */ - allowHeaders?: string[]; - /** Maximum cache age for preflight requests (in seconds). */ - maxAge?: number; - /** Whether to include credentials (cookies, HTTP auth, etc.) in the request. */ - credentials?: boolean; - /** Headers exposed to the client in the response. */ - exposeHeaders?: string[]; -}; +export { GamanORM } from './orm.js'; /** - * Middleware for handling Cross-Origin Resource Sharing (CORS). - * @param options - The options for configuring CORS behavior. - * @returns Middleware function for handling CORS. + * Base model class for defining database models with casting and relations. */ -export declare const cors: (options: CorsOptions) => Handler; +export { BaseModel, BaseModelOptions } from './model/base.js'; +/** + * Interface for database providers that implement the actual database interactions. + */ +export { GamanProvider } from './provider/base.js'; +/** + * SQLite implementation of the GamanProvider interface. + */ +export { SQLiteProvider } from './provider/sqlite.js'; diff --git a/plugins/orm/index.js b/plugins/orm/index.js index 61b8368..d4da410 100644 --- a/plugins/orm/index.js +++ b/plugins/orm/index.js @@ -1,69 +1,34 @@ /** - * @module - * CORS Middleware for Gaman. - * Implements Cross-Origin Resource Sharing (CORS) with customizable options. + * @fileoverview Gaman ORM Plugin + * + * This module provides a lightweight Object-Relational Mapping (ORM) system for Gaman applications. + * It supports basic CRUD operations, data casting, and model relations through a provider-based architecture. + * + * Key features: + * - Database-agnostic via providers (e.g., SQLite) + * - Automatic data type casting + * - Model-based relations (hasMany, belongsTo, hasOne) + * - Simple query interface + * + * @example + * ```typescript + * import { GamanORM, BaseModel, SQLiteProvider } from '@gaman/orm'; + * + * const orm = new GamanORM(new SQLiteProvider()); + * class User extends BaseModel { + * // Define model options + * } + * ``` */ -import { next } from "@gaman/core/next"; /** - * Middleware for handling Cross-Origin Resource Sharing (CORS). - * @param options - The options for configuring CORS behavior. - * @returns Middleware function for handling CORS. + * The main ORM class that handles database connections and operations. */ -export const cors = (options) => { - const { origin = "*", allowMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"], allowHeaders = [], maxAge, credentials, exposeHeaders, } = options; - return async (ctx) => { - const requestOrigin = ctx.header("Origin"); - // Determine allowed origin - let allowedOrigin = "*"; - if (typeof origin === "string") { - allowedOrigin = origin; - } - else if (Array.isArray(origin) && origin.includes(requestOrigin || '')) { - allowedOrigin = requestOrigin; - } - else { - allowedOrigin = undefined; - } - // Set CORS headers - const headers = {}; - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin - if (allowedOrigin !== "*") { - const existingVary = ctx.header("Vary"); - if (existingVary) { - ctx.headers.set("Vary", existingVary); - } - else { - ctx.headers.set("Vary", "Origin"); - } - } - if (allowedOrigin) { - headers["Access-Control-Allow-Origin"] = allowedOrigin; - } - if (allowMethods.length) { - headers["Access-Control-Allow-Methods"] = allowMethods.join(", "); - } - if (allowHeaders.length) { - headers["Access-Control-Allow-Headers"] = allowHeaders.join(", "); - } - if (maxAge) { - headers["Access-Control-Max-Age"] = maxAge.toString(); - } - if (credentials) { - headers["Access-Control-Allow-Credentials"] = "true"; - } - if (exposeHeaders?.length) { - headers["Access-Control-Expose-Headers"] = exposeHeaders.join(", "); - } - // Handle preflight request - if (ctx.request.method === "OPTIONS" && requestOrigin) { - return new Response(null, { status: 204, headers }); - } - // Add headers to the response and proceed to the next middleware - Object.entries(headers).forEach(([key, value]) => { - if (!ctx.headers.has(key)) { - ctx.headers.set(key, value); - } - }); - return await next(); - }; -}; +export { GamanORM } from './orm.js'; +/** + * Base model class for defining database models with casting and relations. + */ +export { BaseModel } from './model/base.js'; +/** + * SQLite implementation of the GamanProvider interface. + */ +export { SQLiteProvider } from './provider/sqlite.js'; diff --git a/plugins/orm/index.ts b/plugins/orm/index.ts index f172e42..b25040e 100644 --- a/plugins/orm/index.ts +++ b/plugins/orm/index.ts @@ -1,107 +1,42 @@ /** - * @module - * CORS Middleware for Gaman. - * Implements Cross-Origin Resource Sharing (CORS) with customizable options. + * @fileoverview Gaman ORM Plugin + * + * This module provides a lightweight Object-Relational Mapping (ORM) system for Gaman applications. + * It supports basic CRUD operations, data casting, and model relations through a provider-based architecture. + * + * Key features: + * - Database-agnostic via providers (e.g., SQLite) + * - Automatic data type casting + * - Model-based relations (hasMany, belongsTo, hasOne) + * - Simple query interface + * + * @example + * ```typescript + * import { GamanORM, BaseModel, SQLiteProvider } from '@gaman/orm'; + * + * const orm = new GamanORM(new SQLiteProvider()); + * class User extends BaseModel { + * // Define model options + * } + * ``` */ -import { next } from "@gaman/core/next"; -import { AppConfig, Context, Handler } from "@gaman/core/types"; - /** - * CORS middleware options. + * The main ORM class that handles database connections and operations. */ -export type CorsOptions = { - /** Allowed origin(s) for the request. */ - origin?: string | string[] | null; - /** HTTP methods allowed for the request. Default: `["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"]` */ - allowMethods?: string[]; - /** Headers allowed in the request. Default: `["Content-Type", "Authorization"]` */ - allowHeaders?: string[]; - /** Maximum cache age for preflight requests (in seconds). */ - maxAge?: number; - /** Whether to include credentials (cookies, HTTP auth, etc.) in the request. */ - credentials?: boolean; - /** Headers exposed to the client in the response. */ - exposeHeaders?: string[]; -}; +export { GamanORM } from './orm.js'; /** - * Middleware for handling Cross-Origin Resource Sharing (CORS). - * @param options - The options for configuring CORS behavior. - * @returns Middleware function for handling CORS. + * Base model class for defining database models with casting and relations. */ +export { BaseModel, BaseModelOptions } from './model/base.js'; +/** + * Interface for database providers that implement the actual database interactions. + */ +export { GamanProvider } from './provider/base.js'; - -export const cors = (options: CorsOptions): Handler => { - const { - origin = "*", - allowMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"], - allowHeaders = [], - maxAge, - credentials, - exposeHeaders, - } = options; - - return async (ctx: Context) => { - const requestOrigin = ctx.header("Origin"); - // Determine allowed origin - let allowedOrigin: string | undefined = "*"; - - if (typeof origin === "string") { - allowedOrigin = origin; - } else if (Array.isArray(origin) && origin.includes(requestOrigin || '')) { - allowedOrigin = requestOrigin; - } else { - allowedOrigin = undefined; - } - - // Set CORS headers - const headers: Record = {}; - - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin - if (allowedOrigin !== "*") { - const existingVary = ctx.header("Vary"); - if (existingVary) { - ctx.headers.set("Vary", existingVary); - } else { - ctx.headers.set("Vary", "Origin"); - } - } - - if (allowedOrigin) { - headers["Access-Control-Allow-Origin"] = allowedOrigin; - } - - if (allowMethods.length) { - headers["Access-Control-Allow-Methods"] = allowMethods.join(", "); - } - - if (allowHeaders.length) { - headers["Access-Control-Allow-Headers"] = allowHeaders.join(", "); - } - if (maxAge) { - headers["Access-Control-Max-Age"] = maxAge.toString(); - } - if (credentials) { - headers["Access-Control-Allow-Credentials"] = "true"; - } - if (exposeHeaders?.length) { - headers["Access-Control-Expose-Headers"] = exposeHeaders.join(", "); - } - - // Handle preflight request - if (ctx.request.method === "OPTIONS" && requestOrigin) { - return new Response(null, { status: 204, headers }); - } - - // Add headers to the response and proceed to the next middleware - Object.entries(headers).forEach(([key, value]) => { - if (!ctx.headers.has(key)) { - ctx.headers.set(key, value); - } - }); - - return await next(); - }; -}; +/** + * SQLite implementation of the GamanProvider interface. + */ +export { SQLiteProvider } from './provider/sqlite.js'; diff --git a/plugins/orm/model/base.d.ts b/plugins/orm/model/base.d.ts index 1a57434..5ac0825 100644 --- a/plugins/orm/model/base.d.ts +++ b/plugins/orm/model/base.d.ts @@ -1,13 +1,59 @@ +/** + * @fileoverview Base model class for ORM operations with casting and relations. + */ +import { GamanORM } from '../orm.js'; +/** + * Options for configuring a BaseModel instance. + * @template T The type of the model data. + */ export interface BaseModelOptions { table: string; validate?: (data: any) => T; + casts?: Record; } -export interface BaseModel { - table: string; - validate?: (data: any) => T; +/** + * Interface for a model constructor that includes static options. + * @template T The type of the model data. + */ +export interface ModelConstructor { + new (orm: GamanORM, options: BaseModelOptions): BaseModel; + options: BaseModelOptions; +} +/** + * Interface for a model constructor that includes a static getOptions method. + * @template T The type of the model data. + */ +export interface ModelConstructorWithGetOptions { + new (orm: GamanORM, options: BaseModelOptions): BaseModel; + getOptions(): BaseModelOptions; +} +/** + * Abstract base class for database models in the Gaman ORM. + * Provides CRUD operations, data casting, and relation methods. + * + * @template T The type of the object this model represents. + */ +export declare abstract class BaseModel { + protected orm: GamanORM; + protected options: BaseModelOptions; + constructor(orm: GamanORM, options: BaseModelOptions); + protected castAttribute(key: string, value: any): any; + protected castAttributes(data: any): T; create(data: any): Promise; find(query?: Partial): Promise; findOne(query: Partial): Promise; update(query: Partial, data: Partial): Promise; delete(query: Partial): Promise; + /** + * Defines a hasMany relation. + */ + hasMany(relatedOptions: BaseModelOptions, relatedModel: new (orm: GamanORM, options: BaseModelOptions) => BaseModel, foreignKey: string, localKey?: string, localKeyValue?: any): Promise; + /** + * Defines a belongsTo relation. + */ + belongsTo(relatedOptions: BaseModelOptions, relatedModel: new (orm: GamanORM, options: BaseModelOptions) => BaseModel, foreignKey: string, ownerKey?: string, foreignKeyValue?: any): Promise; + /** + * Defines a hasOne relation. + */ + hasOne(relatedOptions: BaseModelOptions, relatedModel: new (orm: GamanORM, options: BaseModelOptions) => BaseModel, foreignKey: string, localKey?: string, localKeyValue?: any): Promise; } diff --git a/plugins/orm/model/base.js b/plugins/orm/model/base.js index cb0ff5c..fe51423 100644 --- a/plugins/orm/model/base.js +++ b/plugins/orm/model/base.js @@ -1 +1,112 @@ -export {}; +/** + * @fileoverview Base model class for ORM operations with casting and relations. + */ +/** + * Abstract base class for database models in the Gaman ORM. + * Provides CRUD operations, data casting, and relation methods. + * + * @template T The type of the object this model represents. + */ +export class BaseModel { + constructor(orm, options) { + this.orm = orm; + this.options = options; + } + castAttribute(key, value) { + const cast = this.options.casts?.[key]; + if (!cast) + return value; + switch (cast) { + case 'int': + case 'integer': + return parseInt(value, 10); + case 'float': + case 'double': + return parseFloat(value); + case 'string': + return String(value); + case 'bool': + case 'boolean': + return Boolean(value); + case 'json': + return typeof value === 'string' ? JSON.parse(value) : value; + case 'datetime': + return new Date(value); + default: + return value; + } + } + castAttributes(data) { + const casted = {}; + for (const [key, value] of Object.entries(data)) { + casted[key] = this.castAttribute(key, value); + } + return casted; + } + async create(data) { + if (this.options.validate) { + data = this.options.validate(data); + } + await this.orm.insert(this.options.table, data); + return this.castAttributes(data); + } + async find(query) { + const results = await this.orm.find(this.options.table, query); + return results.map((result) => this.castAttributes(result)); + } + async findOne(query) { + const result = await this.orm.findOne(this.options.table, query); + return result ? this.castAttributes(result) : null; + } + async update(query, data) { + if (this.options.validate) { + data = this.options.validate(data); + } + await this.orm.update(this.options.table, query, data); + } + async delete(query) { + await this.orm.delete(this.options.table, query); + } + /** + * Defines a hasMany relation. + */ + async hasMany(relatedOptions, relatedModel, foreignKey, localKey = 'id', localKeyValue) { + if (localKeyValue === undefined) { + throw new Error('localKeyValue is required for hasMany relation'); + } + const query = { [foreignKey]: localKeyValue }; + const results = await this.orm.find(relatedOptions.table, query); + const relatedInstance = new relatedModel(this.orm, relatedOptions); + return results.map((result) => relatedInstance.castAttributes(result)); + } + /** + * Defines a belongsTo relation. + */ + async belongsTo(relatedOptions, relatedModel, foreignKey, ownerKey = 'id', foreignKeyValue) { + if (foreignKeyValue === undefined) { + throw new Error('foreignKeyValue is required for belongsTo relation'); + } + const query = { [ownerKey]: foreignKeyValue }; + const result = await this.orm.findOne(relatedOptions.table, query); + if (result) { + const relatedInstance = new relatedModel(this.orm, relatedOptions); + return relatedInstance.castAttributes(result); + } + return null; + } + /** + * Defines a hasOne relation. + */ + async hasOne(relatedOptions, relatedModel, foreignKey, localKey = 'id', localKeyValue) { + if (localKeyValue === undefined) { + throw new Error('localKeyValue is required for hasOne relation'); + } + const query = { [foreignKey]: localKeyValue }; + const result = await this.orm.findOne(relatedOptions.table, query); + if (result) { + const relatedInstance = new relatedModel(this.orm, relatedOptions); + return relatedInstance.castAttributes(result); + } + return null; + } +} diff --git a/plugins/orm/model/base.ts b/plugins/orm/model/base.ts index 605da69..ce53055 100644 --- a/plugins/orm/model/base.ts +++ b/plugins/orm/model/base.ts @@ -1,15 +1,192 @@ +/** + * @fileoverview Base model class for ORM operations with casting and relations. + */ + +import { GamanORM } from '../orm.js'; + +/** + * Options for configuring a BaseModel instance. + * @template T The type of the model data. + */ export interface BaseModelOptions { - table: string; - validate?: (data: any) => T; // Optional validator buatan sendiri + table: string; + validate?: (data: any) => T; + casts?: Record; } -export interface BaseModel { - table: string; - validate?: (data: any) => T; +/** + * Interface for a model constructor that includes static options. + * @template T The type of the model data. + */ +export interface ModelConstructor { + new (orm: GamanORM, options: BaseModelOptions): BaseModel; + options: BaseModelOptions; +} + +/** + * Interface for a model constructor that includes a static getOptions method. + * @template T The type of the model data. + */ +export interface ModelConstructorWithGetOptions { + new (orm: GamanORM, options: BaseModelOptions): BaseModel; + getOptions(): BaseModelOptions; +} + +/** + * Abstract base class for database models in the Gaman ORM. + * Provides CRUD operations, data casting, and relation methods. + * + * @template T The type of the object this model represents. + */ +export abstract class BaseModel { + protected orm: GamanORM; + protected options: BaseModelOptions; + + constructor(orm: GamanORM, options: BaseModelOptions) { + this.orm = orm; + this.options = options; + } + + protected castAttribute(key: string, value: any): any { + const cast = this.options.casts?.[key]; + if (!cast) return value; + + switch (cast) { + case 'int': + case 'integer': + return parseInt(value, 10); + case 'float': + case 'double': + return parseFloat(value); + case 'string': + return String(value); + case 'bool': + case 'boolean': + return Boolean(value); + case 'json': + return typeof value === 'string' ? JSON.parse(value) : value; + case 'datetime': + return new Date(value); + default: + return value; + } + } + + protected castAttributes(data: any): T { + const casted: any = {}; + for (const [key, value] of Object.entries(data)) { + casted[key] = this.castAttribute(key, value); + } + return casted as T; + } + + async create(data: any): Promise { + if (this.options.validate) { + data = this.options.validate(data); + } + await this.orm.insert(this.options.table, data); + return this.castAttributes(data); + } + + async find(query?: Partial): Promise { + const results = await this.orm.find(this.options.table, query as T); + return results.map((result) => this.castAttributes(result)); + } + + async findOne(query: Partial): Promise { + const result = await this.orm.findOne(this.options.table, query as T); + return result ? this.castAttributes(result) : null; + } + + async update(query: Partial, data: Partial): Promise { + if (this.options.validate) { + data = this.options.validate(data); + } + await this.orm.update(this.options.table, query, data); + } + + async delete(query: Partial): Promise { + await this.orm.delete(this.options.table, query); + } + + /** + * Defines a hasMany relation. + */ + async hasMany( + relatedOptions: BaseModelOptions, + relatedModel: new ( + orm: GamanORM, + options: BaseModelOptions, + ) => BaseModel, + foreignKey: string, + localKey = 'id', + localKeyValue?: any, + ): Promise { + if (localKeyValue === undefined) { + throw new Error('localKeyValue is required for hasMany relation'); + } + const query = { [foreignKey]: localKeyValue } as any; + const results = await this.orm.find( + relatedOptions.table, + query, + ); + const relatedInstance = new relatedModel(this.orm, relatedOptions); + return results.map((result) => relatedInstance.castAttributes(result)); + } + + /** + * Defines a belongsTo relation. + */ + async belongsTo( + relatedOptions: BaseModelOptions, + relatedModel: new ( + orm: GamanORM, + options: BaseModelOptions, + ) => BaseModel, + foreignKey: string, + ownerKey = 'id', + foreignKeyValue?: any, + ): Promise { + if (foreignKeyValue === undefined) { + throw new Error('foreignKeyValue is required for belongsTo relation'); + } + const query = { [ownerKey]: foreignKeyValue } as any; + const result = await this.orm.findOne( + relatedOptions.table, + query, + ); + if (result) { + const relatedInstance = new relatedModel(this.orm, relatedOptions); + return relatedInstance.castAttributes(result); + } + return null; + } - create(data: any): Promise; - find(query?: Partial): Promise; - findOne(query: Partial): Promise; - update(query: Partial, data: Partial): Promise; - delete(query: Partial): Promise; + /** + * Defines a hasOne relation. + */ + async hasOne( + relatedOptions: BaseModelOptions, + relatedModel: new ( + orm: GamanORM, + options: BaseModelOptions, + ) => BaseModel, + foreignKey: string, + localKey = 'id', + localKeyValue?: any, + ): Promise { + if (localKeyValue === undefined) { + throw new Error('localKeyValue is required for hasOne relation'); + } + const query = { [foreignKey]: localKeyValue } as any; + const result = await this.orm.findOne( + relatedOptions.table, + query, + ); + if (result) { + const relatedInstance = new relatedModel(this.orm, relatedOptions); + return relatedInstance.castAttributes(result); + } + return null; + } } diff --git a/plugins/orm/orm.d.ts b/plugins/orm/orm.d.ts index 0cb7558..95288a3 100644 --- a/plugins/orm/orm.d.ts +++ b/plugins/orm/orm.d.ts @@ -1,12 +1,76 @@ -import { GamanProvider } from './provider/base'; +/** + * @fileoverview GamanORM class for handling database operations. + * This module provides a simple ORM interface that delegates to a provider for actual database interactions. + */ +import { GamanProvider } from './provider/base.js'; +/** + * GamanORM is a lightweight Object-Relational Mapping class that provides basic CRUD operations. + * It acts as a facade over a database provider, allowing for easy switching between different database backends. + * + * @example + * ```typescript + * const orm = new GamanORM(new SQLiteProvider()); + * await orm.connect(); + * const users = await orm.find('users', { active: true }); + * await orm.disconnect(); + * ``` + */ export declare class GamanORM { private provider; + /** + * Creates an instance of GamanORM with the specified provider. + * @param provider The database provider to use for operations. + */ constructor(provider: GamanProvider); + /** + * Establishes a connection to the database via the provider. + * @returns A promise that resolves when the connection is established. + */ connect(): Promise; + /** + * Closes the connection to the database via the provider. + * @returns A promise that resolves when the connection is closed. + */ disconnect(): Promise; + /** + * Finds multiple records in the specified table that match the query. + * @template T The type of the objects to retrieve. + * @param table The name of the table to query. + * @param query Optional query object to filter results. If omitted, returns all records. + * @returns A promise that resolves to an array of matching records. + */ find(table: string, query?: T): Promise; + /** + * Finds a single record in the specified table that matches the query. + * @template T The type of the object to retrieve. + * @param table The name of the table to query. + * @param query Query object to filter results. Must match exactly one record. + * @returns A promise that resolves to the matching record or null if not found. + */ findOne(table: string, query: T): Promise; + /** + * Inserts a new record into the specified table. + * @template T The type of the object to insert. + * @param table The name of the table to insert into. + * @param data The data object to insert. + * @returns A promise that resolves when the insertion is complete. + */ insert(table: string, data: T): Promise; + /** + * Updates records in the specified table that match the query. + * @template T The type of the objects being updated. + * @param table The name of the table to update. + * @param query Query object to identify records to update. + * @param data Partial data object with fields to update. + * @returns A promise that resolves when the update is complete. + */ update(table: string, query: T, data: Partial): Promise; + /** + * Deletes records in the specified table that match the query. + * @template T The type of the objects being deleted. + * @param table The name of the table to delete from. + * @param query Query object to identify records to delete. + * @returns A promise that resolves when the deletion is complete. + */ delete(table: string, query: T): Promise; } diff --git a/plugins/orm/orm.js b/plugins/orm/orm.js index 233a6ff..52dd98f 100644 --- a/plugins/orm/orm.js +++ b/plugins/orm/orm.js @@ -1,25 +1,89 @@ +/** + * @fileoverview GamanORM class for handling database operations. + * This module provides a simple ORM interface that delegates to a provider for actual database interactions. + */ +/** + * GamanORM is a lightweight Object-Relational Mapping class that provides basic CRUD operations. + * It acts as a facade over a database provider, allowing for easy switching between different database backends. + * + * @example + * ```typescript + * const orm = new GamanORM(new SQLiteProvider()); + * await orm.connect(); + * const users = await orm.find('users', { active: true }); + * await orm.disconnect(); + * ``` + */ export class GamanORM { + /** + * Creates an instance of GamanORM with the specified provider. + * @param provider The database provider to use for operations. + */ constructor(provider) { this.provider = provider; } + /** + * Establishes a connection to the database via the provider. + * @returns A promise that resolves when the connection is established. + */ async connect() { await this.provider.connect(); } + /** + * Closes the connection to the database via the provider. + * @returns A promise that resolves when the connection is closed. + */ async disconnect() { await this.provider.disconnect(); } + /** + * Finds multiple records in the specified table that match the query. + * @template T The type of the objects to retrieve. + * @param table The name of the table to query. + * @param query Optional query object to filter results. If omitted, returns all records. + * @returns A promise that resolves to an array of matching records. + */ async find(table, query) { return this.provider.find(table, query); } + /** + * Finds a single record in the specified table that matches the query. + * @template T The type of the object to retrieve. + * @param table The name of the table to query. + * @param query Query object to filter results. Must match exactly one record. + * @returns A promise that resolves to the matching record or null if not found. + */ async findOne(table, query) { return this.provider.findOne(table, query); } + /** + * Inserts a new record into the specified table. + * @template T The type of the object to insert. + * @param table The name of the table to insert into. + * @param data The data object to insert. + * @returns A promise that resolves when the insertion is complete. + */ async insert(table, data) { return this.provider.insert(table, data); } + /** + * Updates records in the specified table that match the query. + * @template T The type of the objects being updated. + * @param table The name of the table to update. + * @param query Query object to identify records to update. + * @param data Partial data object with fields to update. + * @returns A promise that resolves when the update is complete. + */ async update(table, query, data) { return this.provider.update(table, query, data); } + /** + * Deletes records in the specified table that match the query. + * @template T The type of the objects being deleted. + * @param table The name of the table to delete from. + * @param query Query object to identify records to delete. + * @returns A promise that resolves when the deletion is complete. + */ async delete(table, query) { return this.provider.delete(table, query); } diff --git a/plugins/orm/orm.ts b/plugins/orm/orm.ts index 9794cdd..e53faac 100644 --- a/plugins/orm/orm.ts +++ b/plugins/orm/orm.ts @@ -1,34 +1,102 @@ -// orm.ts -import { GamanProvider } from './provider/base'; +/** + * @fileoverview GamanORM class for handling database operations. + * This module provides a simple ORM interface that delegates to a provider for actual database interactions. + */ + import { GamanProvider } from './provider/base.js'; + +/** + * GamanORM is a lightweight Object-Relational Mapping class that provides basic CRUD operations. + * It acts as a facade over a database provider, allowing for easy switching between different database backends. + * + * @example + * ```typescript + * const orm = new GamanORM(new SQLiteProvider()); + * await orm.connect(); + * const users = await orm.find('users', { active: true }); + * await orm.disconnect(); + * ``` + */ export class GamanORM { + /** + * Creates an instance of GamanORM with the specified provider. + * @param provider The database provider to use for operations. + */ constructor(private provider: GamanProvider) {} - async connect() { + /** + * Establishes a connection to the database via the provider. + * @returns A promise that resolves when the connection is established. + */ + async connect(): Promise { await this.provider.connect(); } - async disconnect() { + /** + * Closes the connection to the database via the provider. + * @returns A promise that resolves when the connection is closed. + */ + async disconnect(): Promise { await this.provider.disconnect(); } - async find(table: string, query?: T) { + /** + * Finds multiple records in the specified table that match the query. + * @template T The type of the objects to retrieve. + * @param table The name of the table to query. + * @param query Optional query object to filter results. If omitted, returns all records. + * @returns A promise that resolves to an array of matching records. + */ + async find(table: string, query?: T): Promise { return this.provider.find(table, query); } - async findOne(table: string, query: T) { + /** + * Finds a single record in the specified table that matches the query. + * @template T The type of the object to retrieve. + * @param table The name of the table to query. + * @param query Query object to filter results. Must match exactly one record. + * @returns A promise that resolves to the matching record or null if not found. + */ + async findOne(table: string, query: T): Promise { return this.provider.findOne(table, query); } - async insert(table: string, data: T) { + /** + * Inserts a new record into the specified table. + * @template T The type of the object to insert. + * @param table The name of the table to insert into. + * @param data The data object to insert. + * @returns A promise that resolves when the insertion is complete. + */ + async insert(table: string, data: T): Promise { return this.provider.insert(table, data); } - async update(table: string, query: T, data: Partial) { + /** + * Updates records in the specified table that match the query. + * @template T The type of the objects being updated. + * @param table The name of the table to update. + * @param query Query object to identify records to update. + * @param data Partial data object with fields to update. + * @returns A promise that resolves when the update is complete. + */ + async update( + table: string, + query: T, + data: Partial, + ): Promise { return this.provider.update(table, query, data); } - async delete(table: string, query: T) { + /** + * Deletes records in the specified table that match the query. + * @template T The type of the objects being deleted. + * @param table The name of the table to delete from. + * @param query Query object to identify records to delete. + * @returns A promise that resolves when the deletion is complete. + */ + async delete(table: string, query: T): Promise { return this.provider.delete(table, query); } } diff --git a/plugins/orm/package.json b/plugins/orm/package.json index 38a53a0..3288d73 100644 --- a/plugins/orm/package.json +++ b/plugins/orm/package.json @@ -1,6 +1,6 @@ { "name": "@gaman/orm", - "version": "1.0.0", + "version": "1.1.0", "type": "module", "main": "index.js", "author": "angga7togk", diff --git a/plugins/orm/provider/base.d.ts b/plugins/orm/provider/base.d.ts index 5e0bb5d..b70a50b 100644 --- a/plugins/orm/provider/base.d.ts +++ b/plugins/orm/provider/base.d.ts @@ -1,9 +1,60 @@ +/** + * @fileoverview Interface for database providers in the Gaman ORM. + */ +/** + * Interface that all database providers must implement to work with GamanORM. + * Providers handle the actual database interactions, allowing for different backends like SQLite, PostgreSQL, etc. + */ export interface GamanProvider { + /** + * Establishes a connection to the database. + * @returns A promise that resolves when the connection is established. + */ connect(): Promise; + /** + * Closes the connection to the database. + * @returns A promise that resolves when the connection is closed. + */ disconnect(): Promise; + /** + * Finds multiple records in the specified table. + * @template T The type of the objects to retrieve. + * @param collection The name of the table or collection. + * @param query Optional query object to filter results. + * @returns A promise resolving to an array of matching records. + */ find(collection: string, query?: object): Promise; + /** + * Finds a single record in the specified table. + * @template T The type of the object to retrieve. + * @param collection The name of the table or collection. + * @param query Query object to match exactly one record. + * @returns A promise resolving to the matching record or null. + */ findOne(collection: string, query: object): Promise; + /** + * Inserts a new record into the specified table. + * @template T The type of the object to insert. + * @param collection The name of the table or collection. + * @param data The data object to insert. + * @returns A promise that resolves when the insertion is complete. + */ insert(collection: string, data: T): Promise; + /** + * Updates records in the specified table. + * @template T The type of the objects being updated. + * @param collection The name of the table or collection. + * @param query Query object to identify records to update. + * @param data Partial data object with fields to update. + * @returns A promise that resolves when the update is complete. + */ update(collection: string, query: object, data: Partial): Promise; + /** + * Deletes records in the specified table. + * @template T The type of the objects being deleted. + * @param collection The name of the table or collection. + * @param query Query object to identify records to delete. + * @returns A promise that resolves when the deletion is complete. + */ delete(collection: string, query: object): Promise; } diff --git a/plugins/orm/provider/base.js b/plugins/orm/provider/base.js index cb0ff5c..72696e8 100644 --- a/plugins/orm/provider/base.js +++ b/plugins/orm/provider/base.js @@ -1 +1,4 @@ +/** + * @fileoverview Interface for database providers in the Gaman ORM. + */ export {}; diff --git a/plugins/orm/provider/base.ts b/plugins/orm/provider/base.ts index 9e84023..ade90c4 100644 --- a/plugins/orm/provider/base.ts +++ b/plugins/orm/provider/base.ts @@ -1,16 +1,74 @@ +/** + * @fileoverview Interface for database providers in the Gaman ORM. + */ + +/** + * Interface that all database providers must implement to work with GamanORM. + * Providers handle the actual database interactions, allowing for different backends like SQLite, PostgreSQL, etc. + */ export interface GamanProvider { + /** + * Establishes a connection to the database. + * @returns A promise that resolves when the connection is established. + */ connect(): Promise; + + /** + * Closes the connection to the database. + * @returns A promise that resolves when the connection is closed. + */ disconnect(): Promise; + + /** + * Finds multiple records in the specified table. + * @template T The type of the objects to retrieve. + * @param collection The name of the table or collection. + * @param query Optional query object to filter results. + * @returns A promise resolving to an array of matching records. + */ find(collection: string, query?: object): Promise; + + /** + * Finds a single record in the specified table. + * @template T The type of the object to retrieve. + * @param collection The name of the table or collection. + * @param query Query object to match exactly one record. + * @returns A promise resolving to the matching record or null. + */ findOne( collection: string, query: object, ): Promise; + + /** + * Inserts a new record into the specified table. + * @template T The type of the object to insert. + * @param collection The name of the table or collection. + * @param data The data object to insert. + * @returns A promise that resolves when the insertion is complete. + */ insert(collection: string, data: T): Promise; + + /** + * Updates records in the specified table. + * @template T The type of the objects being updated. + * @param collection The name of the table or collection. + * @param query Query object to identify records to update. + * @param data Partial data object with fields to update. + * @returns A promise that resolves when the update is complete. + */ update( collection: string, query: object, data: Partial, ): Promise; + + /** + * Deletes records in the specified table. + * @template T The type of the objects being deleted. + * @param collection The name of the table or collection. + * @param query Query object to identify records to delete. + * @returns A promise that resolves when the deletion is complete. + */ delete(collection: string, query: object): Promise; } diff --git a/plugins/orm/provider/sqlite.d.ts b/plugins/orm/provider/sqlite.d.ts index 1ebd5b5..59352b4 100644 --- a/plugins/orm/provider/sqlite.d.ts +++ b/plugins/orm/provider/sqlite.d.ts @@ -1,12 +1,75 @@ +/** + * @fileoverview SQLite provider implementation for the Gaman ORM. + */ import { Database } from 'sqlite'; -import { GamanProvider } from './base'; +import { GamanProvider } from './base.js'; +/** + * SQLite implementation of the GamanProvider interface. + * Uses the 'sqlite' package with 'sqlite3' driver for database operations. + * + * @example + * ```typescript + * const provider = new SQLiteProvider(); + * await provider.connect(); + * const users = await provider.find('users'); + * await provider.disconnect(); + * ``` + */ export declare class SQLiteProvider implements GamanProvider { + /** + * The SQLite database instance. + */ db: Database; + /** + * Establishes a connection to the SQLite database. + * Opens a database file named 'data.db' in the current directory. + * @returns A promise that resolves when the connection is established. + */ connect(): Promise; + /** + * Closes the connection to the SQLite database. + * @returns A promise that resolves when the connection is closed. + */ disconnect(): Promise; + /** + * Finds multiple records in the specified table. + * @template T The type of the objects to retrieve. + * @param table The name of the table to query. + * @param query Optional query object to filter results using WHERE clauses. + * @returns A promise resolving to an array of matching records. + */ find(table: string, query?: object): Promise; + /** + * Finds a single record in the specified table. + * @template T The type of the object to retrieve. + * @param table The name of the table to query. + * @param query Query object to match exactly one record. + * @returns A promise resolving to the matching record or null. + */ findOne(table: string, query: object): Promise; + /** + * Inserts a new record into the specified table. + * @template T The type of the object to insert. + * @param table The name of the table to insert into. + * @param data The data object to insert. + * @returns A promise that resolves when the insertion is complete. + */ insert(table: string, data: T): Promise; + /** + * Updates records in the specified table. + * @template T The type of the objects being updated. + * @param table The name of the table to update. + * @param query Query object to identify records to update. + * @param data Partial data object with fields to update. + * @returns A promise that resolves when the update is complete. + */ update(table: string, query: object, data: Partial): Promise; + /** + * Deletes records in the specified table. + * @template T The type of the objects being deleted. + * @param table The name of the table to delete from. + * @param query Query object to identify records to delete. + * @returns A promise that resolves when the deletion is complete. + */ delete(table: string, query: object): Promise; } diff --git a/plugins/orm/provider/sqlite.js b/plugins/orm/provider/sqlite.js index dcfe557..4966f7b 100644 --- a/plugins/orm/provider/sqlite.js +++ b/plugins/orm/provider/sqlite.js @@ -1,12 +1,43 @@ +/** + * @fileoverview SQLite provider implementation for the Gaman ORM. + */ import sqlite3 from 'sqlite3'; import { open } from 'sqlite'; +/** + * SQLite implementation of the GamanProvider interface. + * Uses the 'sqlite' package with 'sqlite3' driver for database operations. + * + * @example + * ```typescript + * const provider = new SQLiteProvider(); + * await provider.connect(); + * const users = await provider.find('users'); + * await provider.disconnect(); + * ``` + */ export class SQLiteProvider { + /** + * Establishes a connection to the SQLite database. + * Opens a database file named 'data.db' in the current directory. + * @returns A promise that resolves when the connection is established. + */ async connect() { this.db = await open({ filename: 'data.db', driver: sqlite3.Database }); } + /** + * Closes the connection to the SQLite database. + * @returns A promise that resolves when the connection is closed. + */ async disconnect() { await this.db.close(); } + /** + * Finds multiple records in the specified table. + * @template T The type of the objects to retrieve. + * @param table The name of the table to query. + * @param query Optional query object to filter results using WHERE clauses. + * @returns A promise resolving to an array of matching records. + */ async find(table, query = {}) { const keys = Object.keys(query); const where = keys.length @@ -15,16 +46,38 @@ export class SQLiteProvider { const values = Object.values(query); return this.db.all(`SELECT * FROM ${table} ${where}`, values); } + /** + * Finds a single record in the specified table. + * @template T The type of the object to retrieve. + * @param table The name of the table to query. + * @param query Query object to match exactly one record. + * @returns A promise resolving to the matching record or null. + */ async findOne(table, query) { const rows = await this.find(table, query); return rows[0] || null; } + /** + * Inserts a new record into the specified table. + * @template T The type of the object to insert. + * @param table The name of the table to insert into. + * @param data The data object to insert. + * @returns A promise that resolves when the insertion is complete. + */ async insert(table, data) { const keys = Object.keys(data); const placeholders = keys.map(() => '?').join(', '); const values = Object.values(data); await this.db.run(`INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders})`, values); } + /** + * Updates records in the specified table. + * @template T The type of the objects being updated. + * @param table The name of the table to update. + * @param query Query object to identify records to update. + * @param data Partial data object with fields to update. + * @returns A promise that resolves when the update is complete. + */ async update(table, query, data) { const qKeys = Object.keys(query); const dKeys = Object.keys(data); @@ -33,6 +86,13 @@ export class SQLiteProvider { const values = [...Object.values(data), ...Object.values(query)]; await this.db.run(`UPDATE ${table} SET ${set} WHERE ${where}`, values); } + /** + * Deletes records in the specified table. + * @template T The type of the objects being deleted. + * @param table The name of the table to delete from. + * @param query Query object to identify records to delete. + * @returns A promise that resolves when the deletion is complete. + */ async delete(table, query) { const qKeys = Object.keys(query); const where = qKeys.map((k) => `${k} = ?`).join(' AND '); diff --git a/plugins/orm/provider/sqlite.ts b/plugins/orm/provider/sqlite.ts index acdbf18..2bdef54 100644 --- a/plugins/orm/provider/sqlite.ts +++ b/plugins/orm/provider/sqlite.ts @@ -1,59 +1,123 @@ -import sqlite3 from 'sqlite3'; -import { open, Database } from 'sqlite'; -import { GamanProvider } from './base'; - -export class SQLiteProvider implements GamanProvider { - db!: Database; - - async connect() { - this.db = await open({ filename: 'data.db', driver: sqlite3.Database }); - } - - async disconnect() { - await this.db.close(); - } - - async find(table: string, query: object = {}): Promise { - const keys = Object.keys(query); - const where = keys.length - ? `WHERE ${keys.map((k) => `${k} = ?`).join(' AND ')}` - : ''; - const values = Object.values(query); - return this.db.all(`SELECT * FROM ${table} ${where}`, values); - } - - async findOne(table: string, query: object): Promise { - const rows = await this.find(table, query); - return rows[0] || null; - } - - async insert(table: string, data: T): Promise { - const keys = Object.keys(data); - const placeholders = keys.map(() => '?').join(', '); - const values = Object.values(data); - await this.db.run( - `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders})`, - values, - ); - } - - async update( - table: string, - query: object, - data: Partial, - ): Promise { - const qKeys = Object.keys(query); - const dKeys = Object.keys(data); - const where = qKeys.map((k) => `${k} = ?`).join(' AND '); - const set = dKeys.map((k) => `${k} = ?`).join(', '); - const values = [...Object.values(data), ...Object.values(query)]; - await this.db.run(`UPDATE ${table} SET ${set} WHERE ${where}`, values); - } - - async delete(table: string, query: object): Promise { - const qKeys = Object.keys(query); - const where = qKeys.map((k) => `${k} = ?`).join(' AND '); - const values = Object.values(query); - await this.db.run(`DELETE FROM ${table} WHERE ${where}`, values); - } -} + /** + * @fileoverview SQLite provider implementation for the Gaman ORM. + */ + + import sqlite3 from 'sqlite3'; + import { open, Database } from 'sqlite'; + import { GamanProvider } from './base.js'; + + /** + * SQLite implementation of the GamanProvider interface. + * Uses the 'sqlite' package with 'sqlite3' driver for database operations. + * + * @example + * ```typescript + * const provider = new SQLiteProvider(); + * await provider.connect(); + * const users = await provider.find('users'); + * await provider.disconnect(); + * ``` + */ + export class SQLiteProvider implements GamanProvider { + /** + * The SQLite database instance. + */ + db!: Database; + + /** + * Establishes a connection to the SQLite database. + * Opens a database file named 'data.db' in the current directory. + * @returns A promise that resolves when the connection is established. + */ + async connect(): Promise { + this.db = await open({ filename: 'data.db', driver: sqlite3.Database }); + } + + /** + * Closes the connection to the SQLite database. + * @returns A promise that resolves when the connection is closed. + */ + async disconnect(): Promise { + await this.db.close(); + } + + /** + * Finds multiple records in the specified table. + * @template T The type of the objects to retrieve. + * @param table The name of the table to query. + * @param query Optional query object to filter results using WHERE clauses. + * @returns A promise resolving to an array of matching records. + */ + async find(table: string, query: object = {}): Promise { + const keys = Object.keys(query); + const where = keys.length + ? `WHERE ${keys.map((k) => `${k} = ?`).join(' AND ')}` + : ''; + const values = Object.values(query); + return this.db.all(`SELECT * FROM ${table} ${where}`, values); + } + + /** + * Finds a single record in the specified table. + * @template T The type of the object to retrieve. + * @param table The name of the table to query. + * @param query Query object to match exactly one record. + * @returns A promise resolving to the matching record or null. + */ + async findOne(table: string, query: object): Promise { + const rows = await this.find(table, query); + return rows[0] || null; + } + + /** + * Inserts a new record into the specified table. + * @template T The type of the object to insert. + * @param table The name of the table to insert into. + * @param data The data object to insert. + * @returns A promise that resolves when the insertion is complete. + */ + async insert(table: string, data: T): Promise { + const keys = Object.keys(data); + const placeholders = keys.map(() => '?').join(', '); + const values = Object.values(data); + await this.db.run( + `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders})`, + values, + ); + } + + /** + * Updates records in the specified table. + * @template T The type of the objects being updated. + * @param table The name of the table to update. + * @param query Query object to identify records to update. + * @param data Partial data object with fields to update. + * @returns A promise that resolves when the update is complete. + */ + async update( + table: string, + query: object, + data: Partial, + ): Promise { + const qKeys = Object.keys(query); + const dKeys = Object.keys(data); + const where = qKeys.map((k) => `${k} = ?`).join(' AND '); + const set = dKeys.map((k) => `${k} = ?`).join(', '); + const values = [...Object.values(data), ...Object.values(query)]; + await this.db.run(`UPDATE ${table} SET ${set} WHERE ${where}`, values); + } + + /** + * Deletes records in the specified table. + * @template T The type of the objects being deleted. + * @param table The name of the table to delete from. + * @param query Query object to identify records to delete. + * @returns A promise that resolves when the deletion is complete. + */ + async delete(table: string, query: object): Promise { + const qKeys = Object.keys(query); + const where = qKeys.map((k) => `${k} = ?`).join(' AND '); + const values = Object.values(query); + await this.db.run(`DELETE FROM ${table} WHERE ${where}`, values); + } + } diff --git a/plugins/orm/sample-model.ts b/plugins/orm/sample-model.ts new file mode 100644 index 0000000..0091991 --- /dev/null +++ b/plugins/orm/sample-model.ts @@ -0,0 +1,101 @@ +/** + * @fileoverview Sample models demonstrating the use of BaseModel for ORM operations. + * This file provides example User and Post models with relations and casting. + */ + +import { BaseModel, BaseModelOptions } from './model/base.js'; +import { GamanORM } from './orm.js'; + +/** + * Interface representing a User entity in the database. + */ +interface User { + id: number; + name: string; + email: string; + created_at: Date; + settings: Record; +} + +/** + * UserModel extends BaseModel to provide ORM functionality for the 'users' table. + * It includes data casting for specific fields and demonstrates a hasMany relation. + * + * @example + * ```typescript + * const userModel = new UserModel(orm); + * const users = await userModel.find({ active: true }); + * const posts = await userModel.hasManyPosts(1); + * ``` + */ +export class UserModel extends BaseModel { + static options: BaseModelOptions = { + table: 'users', + casts: { + id: 'int', + created_at: 'datetime', + settings: 'json', + }, + }; + + constructor(orm: GamanORM) { + super(orm, UserModel.options); + } + + /** + * Defines a hasMany relation to PostModel. + * Retrieves all posts associated with a specific user. + * @param userId The ID of the user to get posts for. + * @returns A promise resolving to an array of Post objects. + */ + async hasManyPosts(userId: number): Promise { + return this.hasMany(PostModel.options, PostModel, 'user_id', 'id', userId); + } +} + +/** + * Interface representing a Post entity in the database. + */ +interface Post { + id: number; + user_id: number; + title: string; + content: string; + published: boolean; +} + +/** + * PostModel extends BaseModel to provide ORM functionality for the 'posts' table. + * It includes data casting and demonstrates a belongsTo relation. + * + * @example + * ```typescript + * const postModel = new PostModel(orm); + * const posts = await postModel.find({ published: true }); + * const user = await postModel.belongsToUser(1); + * ``` + */ +export class PostModel extends BaseModel { + static options: BaseModelOptions = { + table: 'posts', + casts: { + id: 'int', + user_id: 'int', + published: 'boolean', + }, + }; + + constructor(orm: GamanORM) { + super(orm, PostModel.options); + } + + /** + * Defines a belongsTo relation to UserModel. + * Retrieves the user associated with a specific post. + * @param postId The ID of the post to get the user for. + * @returns A promise resolving to a User object or null. + */ + async belongsToUser(postId: number): Promise { + return this.belongsTo(UserModel.options, UserModel, 'id', 'id', postId); + } +} diff --git a/plugins/orm/test/orm.test.ts b/plugins/orm/test/orm.test.ts new file mode 100644 index 0000000..6a193a0 --- /dev/null +++ b/plugins/orm/test/orm.test.ts @@ -0,0 +1,177 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { GamanORM } from '../orm'; +import { SQLiteProvider } from '../provider/sqlite'; +import { BaseModel, BaseModelOptions } from '../model/base'; + +interface User { + id: number; + name: string; + email: string; + created_at: Date; + settings: object; +} + +class UserModel extends BaseModel { + static getOptions(): BaseModelOptions { + return { + table: 'users', + casts: { + id: 'int', + created_at: 'datetime', + settings: 'json', + }, + }; + } + + constructor(orm: GamanORM) { + super(orm, UserModel.getOptions()); + } + + async hasManyPosts(userId: number) { + return this.hasMany( + PostModel.getOptions(), + PostModel, + 'user_id', + 'id', + userId, + ); + } +} + +interface Post { + id: number; + user_id: number; + title: string; + content: string; + published: boolean; +} + +class PostModel extends BaseModel { + static getOptions(): BaseModelOptions { + return { + table: 'posts', + casts: { + id: 'int', + user_id: 'int', + published: 'boolean', + }, + }; + } + + constructor(orm: GamanORM) { + super(orm, PostModel.getOptions()); + } + + async belongsToUser(postId: number) { + return this.belongsTo( + UserModel.getOptions(), + UserModel, + 'user_id', + 'id', + postId, + ); + } +} + +describe('GamanORM', () => { + let orm: GamanORM; + let provider: SQLiteProvider; + + beforeAll(async () => { + provider = new SQLiteProvider(); + orm = new GamanORM(provider); + await orm.connect(); + + // Create tables for testing + await provider.db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + name TEXT, + email TEXT, + created_at TEXT, + settings TEXT + ) + `); + + await provider.db.exec(` + CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY, + user_id INTEGER, + title TEXT, + content TEXT, + published INTEGER + ) + `); + }); + + afterAll(async () => { + await orm.disconnect(); + }); + + describe('UserModel', () => { + it('should create a user with proper casting', async () => { + const userModel = new UserModel(orm); + const userData = { + name: 'John Doe', + email: 'john@example.com', + created_at: '2023-01-01T00:00:00Z', + settings: '{"theme": "dark"}', + }; + + const user = await userModel.create(userData); + + expect(user.name).toBe('John Doe'); + expect(user.email).toBe('john@example.com'); + expect(user.created_at).toBeInstanceOf(Date); + expect(user.settings).toEqual({ theme: 'dark' }); + }); + + it('should find users with casting applied', async () => { + const userModel = new UserModel(orm); + const users = await userModel.find(); + + expect(users.length).toBeGreaterThan(0); + const user = users[0]; + expect(typeof user.id).toBe('number'); + expect(user.created_at).toBeInstanceOf(Date); + expect(typeof user.settings).toBe('object'); + }); + + it('should find one user with casting', async () => { + const userModel = new UserModel(orm); + const user = await userModel.findOne({ name: 'John Doe' }); + + expect(user).toBeTruthy(); + expect(user?.created_at).toBeInstanceOf(Date); + }); + }); + + describe('PostModel', () => { + it('should create a post with casting', async () => { + const postModel = new PostModel(orm); + const postData = { + user_id: 1, + title: 'Test Post', + content: 'This is a test post', + published: 1, + }; + + const post = await postModel.create(postData); + + expect(post.title).toBe('Test Post'); + expect(typeof post.user_id).toBe('number'); + expect(typeof post.published).toBe('boolean'); + }); + }); + + describe('Relations', () => { + it('should have relation methods available', async () => { + const userModel = new UserModel(orm); + const postModel = new PostModel(orm); + + // These methods require key values, so pass dummy values + await expect(userModel.hasManyPosts(1)).resolves.not.toThrow(); + await expect(postModel.belongsToUser(1)).resolves.not.toThrow(); + }); + }); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json index 7a6b428..31d7b48 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -21,8 +21,7 @@ "strict": true, "strictNullChecks": true, "forceConsistentCasingInFileNames": true, - "strictPropertyInitialization": false, - "types": ["node"], + "strictPropertyInitialization": false }, "jest": {}, "tsc-alias": { @@ -40,6 +39,7 @@ "plugins/session", "plugins/rate-limit", "plugins/edge", + "plugins/orm", "plugins/jwt" ], "exclude": ["node_modules", "dist", "**/*.test.*"] diff --git a/tsconfig.json b/tsconfig.json index 6824cf9..f8518e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,7 +38,8 @@ "plugins/websocket", "plugins/session", "plugins/rate-limit", - "plugins/jwt" + "plugins/jwt", + "plugins/orm" ], "exclude": ["node_modules", "dist", "**/*.test.*"], "files": [],