From 1ba174ccf977552ef89205692197e0c35bb94d7c Mon Sep 17 00:00:00 2001 From: fiandev Date: Sun, 19 Oct 2025 09:32:44 +0700 Subject: [PATCH 1/3] feat(orm): add ORM plugin Adds a new ORM plugin with support for SQLite, casting, and relations. Includes BaseModel, GamanORM, and provider implementations. --- data.db | Bin 0 -> 12288 bytes package.json | 3 +- plugins/orm/data.db | Bin 0 -> 12288 bytes plugins/orm/index.d.ts | 32 ++----- plugins/orm/index.js | 71 +------------- plugins/orm/index.ts | 109 ++------------------- plugins/orm/model/base.d.ts | 14 ++- plugins/orm/model/base.js | 78 ++++++++++++++- plugins/orm/model/base.ts | 115 ++++++++++++++++++++-- plugins/orm/orm.d.ts | 2 +- plugins/orm/orm.ts | 2 +- plugins/orm/package.json | 21 ---- plugins/orm/provider/sqlite.d.ts | 2 +- plugins/orm/provider/sqlite.ts | 2 +- plugins/orm/sample-model.ts | 56 +++++++++++ plugins/orm/test/orm.test.ts | 159 +++++++++++++++++++++++++++++++ plugins/orm/test/simple.test.ts | 13 +++ tsconfig.base.json | 4 +- tsconfig.json | 3 +- 19 files changed, 448 insertions(+), 238 deletions(-) create mode 100644 data.db create mode 100644 plugins/orm/data.db delete mode 100644 plugins/orm/package.json create mode 100644 plugins/orm/sample-model.ts create mode 100644 plugins/orm/test/orm.test.ts create mode 100644 plugins/orm/test/simple.test.ts diff --git a/data.db b/data.db new file mode 100644 index 0000000000000000000000000000000000000000..9f929413bd957858741b7c5d7b49ef88183b3f35 GIT binary patch literal 12288 zcmeI&O-jQ+6bJB0T9g(v>ZZ65hpZI*s1`S_tTjlriqv8_3BQ!e#Ez(qdEQW zQi|l{Ye6@td=&`pI)`reyAel9M5-s!xjYSg*%F_b(`2)E6GpeTjIJ%sE%ji2t4)V~ zQCsANy;X?N^Mjrm8j0|fXr9on{;O0el=7oe;VrZb4#bSdWwsrVjUPjFkleh5#On7Z1k#C)?TWY_gsFZkt|RHa$W7Hi`pR@G{$An#QV}GsZHLM@}kqnHMJO zbkgS5-z_t&7{9H_U48w7=ks~(by2t- zpN3H)5|uJ_+p0MhcWQ@Ci>ngfUdd*&UXM4<9qZV#`Gws$tJy7nYPELLtx-a8d$zA? z!V^Am$GUdZYc~o7g<@71jyk@V^l5skJZNXUs;REVkE*Nm+(ae)OIND&V^^UY%r!uP z>-n?KcVlt|_1f}YKkYFgA-wP|QI)iv^lq-@-kBT-2tWV=5P$##AOHafKmY;|fB*#k zoyir#}J$5P$##AOHafKmY;|fB*y_0D*rj@Cg--Ze0KX literal 0 HcmV?d00001 diff --git a/plugins/orm/index.d.ts b/plugins/orm/index.d.ts index 9ba4c4d..10009a3 100644 --- a/plugins/orm/index.d.ts +++ b/plugins/orm/index.d.ts @@ -1,29 +1,9 @@ /** * @module - * CORS Middleware for Gaman. - * Implements Cross-Origin Resource Sharing (CORS) with customizable options. + * Gaman ORM Plugin. + * Provides Object-Relational Mapping with support for casting and relations. */ -import { AppConfig, Handler } from "@gaman/core/types"; -/** - * CORS middleware options. - */ -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[]; -}; -/** - * Middleware for handling Cross-Origin Resource Sharing (CORS). - * @param options - The options for configuring CORS behavior. - * @returns Middleware function for handling CORS. - */ -export declare const cors: (options: CorsOptions) => Handler; +export { GamanORM } from './orm.js'; +export { BaseModel, BaseModelOptions } from './model/base.js'; +export { GamanProvider } from './provider/base.js'; +export { SQLiteProvider } from './provider/sqlite.js'; diff --git a/plugins/orm/index.js b/plugins/orm/index.js index 61b8368..e44d221 100644 --- a/plugins/orm/index.js +++ b/plugins/orm/index.js @@ -1,69 +1,8 @@ /** * @module - * CORS Middleware for Gaman. - * Implements Cross-Origin Resource Sharing (CORS) with customizable options. + * Gaman ORM Plugin. + * Provides Object-Relational Mapping with support for casting and relations. */ -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. - */ -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'; +export { BaseModel } from './model/base.js'; +export { SQLiteProvider } from './provider/sqlite.js'; diff --git a/plugins/orm/index.ts b/plugins/orm/index.ts index f172e42..2b9f2c9 100644 --- a/plugins/orm/index.ts +++ b/plugins/orm/index.ts @@ -1,107 +1,10 @@ /** * @module - * CORS Middleware for Gaman. - * Implements Cross-Origin Resource Sharing (CORS) with customizable options. + * Gaman ORM Plugin. + * Provides Object-Relational Mapping with support for casting and relations. */ -import { next } from "@gaman/core/next"; -import { AppConfig, Context, Handler } from "@gaman/core/types"; - -/** - * CORS middleware options. - */ -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[]; -}; - -/** - * Middleware for handling Cross-Origin Resource Sharing (CORS). - * @param options - The options for configuring CORS behavior. - * @returns Middleware function for handling CORS. - */ - - - -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(); - }; -}; +export { GamanORM } from './orm.js'; +export { BaseModel, BaseModelOptions } from './model/base.js'; +export { GamanProvider } from './provider/base.js'; +export { SQLiteProvider } from './provider/sqlite.js'; diff --git a/plugins/orm/model/base.d.ts b/plugins/orm/model/base.d.ts index 1a57434..866e330 100644 --- a/plugins/orm/model/base.d.ts +++ b/plugins/orm/model/base.d.ts @@ -1,13 +1,21 @@ +import { GamanORM } from '../orm.js'; export interface BaseModelOptions { table: string; validate?: (data: any) => T; + casts?: Record; } -export interface BaseModel { - table: string; - validate?: (data: any) => T; +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; + hasMany>(relatedModel: new (orm: GamanORM, options: any) => Related, foreignKey: string, localKey?: string): Related[]; + belongsTo>(relatedModel: new (orm: GamanORM, options: any) => Related, foreignKey: string, ownerKey?: string): Related | null; + hasOne>(relatedModel: new (orm: GamanORM, options: any) => Related, foreignKey: string, localKey?: string): Related | null; } diff --git a/plugins/orm/model/base.js b/plugins/orm/model/base.js index cb0ff5c..2a5c079 100644 --- a/plugins/orm/model/base.js +++ b/plugins/orm/model/base.js @@ -1 +1,77 @@ -export {}; +export class BaseModel { + constructor(orm, options) { + this.orm = orm; + this.options = options; + } + // Casting logic + 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; + } + // CRUD operations + 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); + } + // Relation methods + hasMany(relatedModel, foreignKey, localKey = 'id') { + // This would need to be implemented with query builders + // For now, return a placeholder + return []; + } + belongsTo(relatedModel, foreignKey, ownerKey = 'id') { + // Placeholder for belongsTo relation + return null; + } + hasOne(relatedModel, foreignKey, localKey = 'id') { + // Placeholder for hasOne relation + return null; + } +} diff --git a/plugins/orm/model/base.ts b/plugins/orm/model/base.ts index 605da69..3c80bc1 100644 --- a/plugins/orm/model/base.ts +++ b/plugins/orm/model/base.ts @@ -1,15 +1,110 @@ +import { GamanORM } from '../orm.js'; + 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; +export abstract class BaseModel { + protected orm: GamanORM; + protected options: BaseModelOptions; + + constructor(orm: GamanORM, options: BaseModelOptions) { + this.orm = orm; + this.options = options; + } + + // Casting logic + 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; + } + + // CRUD operations + 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) as T; + } + + 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); + } + + // Relation methods + hasMany>( + relatedModel: new (orm: GamanORM, options: any) => Related, + foreignKey: string, + localKey: string = 'id', + ): Related[] { + // This would need to be implemented with query builders + // For now, return a placeholder + return []; + } + + belongsTo>( + relatedModel: new (orm: GamanORM, options: any) => Related, + foreignKey: string, + ownerKey: string = 'id', + ): Related | null { + // Placeholder for belongsTo relation + return null; + } - create(data: any): Promise; - find(query?: Partial): Promise; - findOne(query: Partial): Promise; - update(query: Partial, data: Partial): Promise; - delete(query: Partial): Promise; + hasOne>( + relatedModel: new (orm: GamanORM, options: any) => Related, + foreignKey: string, + localKey: string = 'id', + ): Related | null { + // Placeholder for hasOne relation + return null; + } } diff --git a/plugins/orm/orm.d.ts b/plugins/orm/orm.d.ts index 0cb7558..ca7fc44 100644 --- a/plugins/orm/orm.d.ts +++ b/plugins/orm/orm.d.ts @@ -1,4 +1,4 @@ -import { GamanProvider } from './provider/base'; +import { GamanProvider } from './provider/base.js'; export declare class GamanORM { private provider; constructor(provider: GamanProvider); diff --git a/plugins/orm/orm.ts b/plugins/orm/orm.ts index 9794cdd..fbebcfd 100644 --- a/plugins/orm/orm.ts +++ b/plugins/orm/orm.ts @@ -1,5 +1,5 @@ // orm.ts -import { GamanProvider } from './provider/base'; +import { GamanProvider } from './provider/base.js'; export class GamanORM { constructor(private provider: GamanProvider) {} diff --git a/plugins/orm/package.json b/plugins/orm/package.json deleted file mode 100644 index 38a53a0..0000000 --- a/plugins/orm/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@gaman/orm", - "version": "1.0.0", - "type": "module", - "main": "index.js", - "author": "angga7togk", - "license": "MIT", - "repository": { - "url": "git+https://github.com/7TogkID/gaman.git", - "directory": "packages/orm" - }, - "bugs": { - "url": "https://github.com/7TogkID/gaman/issues" - }, - "homepage": "https://gaman.7togk.id", - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org" - }, - "gitHead": "8d1bf3bbac4b72847e9157e2c95d75b5e90c89cf" -} diff --git a/plugins/orm/provider/sqlite.d.ts b/plugins/orm/provider/sqlite.d.ts index 1ebd5b5..0e62899 100644 --- a/plugins/orm/provider/sqlite.d.ts +++ b/plugins/orm/provider/sqlite.d.ts @@ -1,5 +1,5 @@ import { Database } from 'sqlite'; -import { GamanProvider } from './base'; +import { GamanProvider } from './base.js'; export declare class SQLiteProvider implements GamanProvider { db: Database; connect(): Promise; diff --git a/plugins/orm/provider/sqlite.ts b/plugins/orm/provider/sqlite.ts index acdbf18..ae204ce 100644 --- a/plugins/orm/provider/sqlite.ts +++ b/plugins/orm/provider/sqlite.ts @@ -1,6 +1,6 @@ import sqlite3 from 'sqlite3'; import { open, Database } from 'sqlite'; -import { GamanProvider } from './base'; +import { GamanProvider } from './base.js'; export class SQLiteProvider implements GamanProvider { db!: Database; diff --git a/plugins/orm/sample-model.ts b/plugins/orm/sample-model.ts new file mode 100644 index 0000000..9d0ad16 --- /dev/null +++ b/plugins/orm/sample-model.ts @@ -0,0 +1,56 @@ +import { BaseModel, BaseModelOptions } from './model/base.js'; +import { GamanORM } from './orm.js'; + +interface User { + id: number; + name: string; + email: string; + created_at: Date; + settings: object; +} + +export class UserModel extends BaseModel { + constructor(orm: GamanORM) { + const options: BaseModelOptions = { + table: 'users', + casts: { + id: 'int', + created_at: 'datetime', + settings: 'json', + }, + }; + super(orm, options); + } + + // Example relation: User has many posts + hasManyPosts() { + return this.hasMany(PostModel, 'user_id'); + } +} + +interface Post { + id: number; + user_id: number; + title: string; + content: string; + published: boolean; +} + +export class PostModel extends BaseModel { + constructor(orm: GamanORM) { + const options: BaseModelOptions = { + table: 'posts', + casts: { + id: 'int', + user_id: 'int', + published: 'boolean', + }, + }; + super(orm, options); + } + + // Example relation: Post belongs to user + belongsToUser() { + return this.belongsTo(UserModel, 'user_id'); + } +} diff --git a/plugins/orm/test/orm.test.ts b/plugins/orm/test/orm.test.ts new file mode 100644 index 0000000..5be7e34 --- /dev/null +++ b/plugins/orm/test/orm.test.ts @@ -0,0 +1,159 @@ +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 { + constructor(orm: GamanORM) { + const options: BaseModelOptions = { + table: 'users', + casts: { + id: 'int', + created_at: 'datetime', + settings: 'json', + }, + }; + super(orm, options); + } + + hasManyPosts() { + return this.hasMany(PostModel, 'user_id'); + } +} + +interface Post { + id: number; + user_id: number; + title: string; + content: string; + published: boolean; +} + +class PostModel extends BaseModel { + constructor(orm: GamanORM) { + const options: BaseModelOptions = { + table: 'posts', + casts: { + id: 'int', + user_id: 'int', + published: 'boolean', + }, + }; + super(orm, options); + } + + belongsToUser() { + return this.belongsTo(UserModel, 'user_id'); + } +} + +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', () => { + const userModel = new UserModel(orm); + const postModel = new PostModel(orm); + + // These are placeholders, but should not throw errors + expect(() => userModel.hasManyPosts()).not.toThrow(); + expect(() => postModel.belongsToUser()).not.toThrow(); + }); + }); +}); diff --git a/plugins/orm/test/simple.test.ts b/plugins/orm/test/simple.test.ts new file mode 100644 index 0000000..f51df21 --- /dev/null +++ b/plugins/orm/test/simple.test.ts @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import { GamanORM } from '../orm'; +import { BaseModel } from '../model/base'; + +describe('Simple Test', () => { + it('should import GamanORM', () => { + expect(GamanORM).toBeDefined(); + }); + + it('should import BaseModel', () => { + expect(BaseModel).toBeDefined(); + }); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json index 99de3c4..d660dfb 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" ], "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": [], From 6bbe4ecddcb0fca878004a04a0fb5fcdbd5c499d Mon Sep 17 00:00:00 2001 From: fiandev Date: Sun, 19 Oct 2025 10:11:18 +0700 Subject: [PATCH 2/3] feat(orm): improve relation method implementation Refactors relation methods in BaseModel to correctly handle asynchronous operations and pass necessary parameters. Updates type definitions and tests to reflect these changes. --- data.db | Bin 12288 -> 12288 bytes plugins/orm/index.d.ts | 35 +++++- plugins/orm/index.js | 32 +++++- plugins/orm/index.ts | 38 ++++++- plugins/orm/model/base.d.ts | 44 +++++++- plugins/orm/model/base.js | 59 ++++++++-- plugins/orm/model/base.ts | 124 +++++++++++++++++---- plugins/orm/orm.d.ts | 64 +++++++++++ plugins/orm/orm.js | 64 +++++++++++ plugins/orm/orm.ts | 86 +++++++++++++-- plugins/orm/provider/base.d.ts | 51 +++++++++ plugins/orm/provider/base.js | 3 + plugins/orm/provider/base.ts | 58 ++++++++++ plugins/orm/provider/sqlite.d.ts | 63 +++++++++++ plugins/orm/provider/sqlite.js | 60 ++++++++++ plugins/orm/provider/sqlite.ts | 182 +++++++++++++++++++++---------- plugins/orm/sample-model.ts | 95 +++++++++++----- plugins/orm/test/orm.test.ts | 46 +++++--- plugins/orm/test/simple.test.ts | 13 --- 19 files changed, 952 insertions(+), 165 deletions(-) delete mode 100644 plugins/orm/test/simple.test.ts diff --git a/data.db b/data.db index 9f929413bd957858741b7c5d7b49ef88183b3f35..bc2c9a89f3df4b5ba913908b43b36ef677064f3e 100644 GIT binary patch delta 554 zcmXBQEl2}l9LMq9aJ%R3dE0L9yY04H5Q8uUlXYWB#2{!`IINZph8P4FjgvDN77P=s z!=h*~7%T>xK}5kIGHq7T{eR{A>EC|P4+gH$JS;Vg<~SJXmT$TdeXV!&qME2PwWZ4P zNrrM&TH;RZi)H@9&-pf=wcqTayGGOD$3h)I2pu%~CVeG&Mzas7Y#qs;P=9se;O>Hr1jss!5GgW7A%Ic{!uy z^L9i(>q~v4J9;M1& diff --git a/plugins/orm/index.d.ts b/plugins/orm/index.d.ts index 10009a3..ae8bf37 100644 --- a/plugins/orm/index.d.ts +++ b/plugins/orm/index.d.ts @@ -1,9 +1,38 @@ /** - * @module - * Gaman ORM Plugin. - * Provides Object-Relational Mapping with support for casting and relations. + * @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 + * } + * ``` + */ +/** + * The main ORM class that handles database connections and operations. */ export { GamanORM } from './orm.js'; +/** + * 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'; +/** + * 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 e44d221..d4da410 100644 --- a/plugins/orm/index.js +++ b/plugins/orm/index.js @@ -1,8 +1,34 @@ /** - * @module - * Gaman ORM Plugin. - * Provides Object-Relational Mapping with support for casting and relations. + * @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 + * } + * ``` + */ +/** + * The main ORM class that handles database connections and operations. */ 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 2b9f2c9..b25040e 100644 --- a/plugins/orm/index.ts +++ b/plugins/orm/index.ts @@ -1,10 +1,42 @@ /** - * @module - * Gaman ORM Plugin. - * Provides Object-Relational Mapping with support for casting and relations. + * @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 + * } + * ``` */ +/** + * The main ORM class that handles database connections and operations. + */ export { GamanORM } from './orm.js'; + +/** + * 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'; + +/** + * 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 866e330..5ac0825 100644 --- a/plugins/orm/model/base.d.ts +++ b/plugins/orm/model/base.d.ts @@ -1,9 +1,38 @@ +/** + * @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; } +/** + * 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; @@ -15,7 +44,16 @@ export declare abstract class BaseModel { findOne(query: Partial): Promise; update(query: Partial, data: Partial): Promise; delete(query: Partial): Promise; - hasMany>(relatedModel: new (orm: GamanORM, options: any) => Related, foreignKey: string, localKey?: string): Related[]; - belongsTo>(relatedModel: new (orm: GamanORM, options: any) => Related, foreignKey: string, ownerKey?: string): Related | null; - hasOne>(relatedModel: new (orm: GamanORM, options: any) => Related, foreignKey: string, localKey?: string): Related | null; + /** + * 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 2a5c079..fe51423 100644 --- a/plugins/orm/model/base.js +++ b/plugins/orm/model/base.js @@ -1,9 +1,17 @@ +/** + * @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; } - // Casting logic castAttribute(key, value) { const cast = this.options.casts?.[key]; if (!cast) @@ -35,7 +43,6 @@ export class BaseModel { } return casted; } - // CRUD operations async create(data) { if (this.options.validate) { data = this.options.validate(data); @@ -45,7 +52,7 @@ export class BaseModel { } async find(query) { const results = await this.orm.find(this.options.table, query); - return results.map(result => this.castAttributes(result)); + return results.map((result) => this.castAttributes(result)); } async findOne(query) { const result = await this.orm.findOne(this.options.table, query); @@ -60,18 +67,46 @@ export class BaseModel { async delete(query) { await this.orm.delete(this.options.table, query); } - // Relation methods - hasMany(relatedModel, foreignKey, localKey = 'id') { - // This would need to be implemented with query builders - // For now, return a placeholder - return []; + /** + * 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)); } - belongsTo(relatedModel, foreignKey, ownerKey = 'id') { - // Placeholder for belongsTo relation + /** + * 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; } - hasOne(relatedModel, foreignKey, localKey = 'id') { - // Placeholder for hasOne relation + /** + * 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 3c80bc1..ce53055 100644 --- a/plugins/orm/model/base.ts +++ b/plugins/orm/model/base.ts @@ -1,11 +1,43 @@ +/** + * @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; } +/** + * 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; @@ -15,7 +47,6 @@ export abstract class BaseModel { this.options = options; } - // Casting logic protected castAttribute(key: string, value: any): any { const cast = this.options.casts?.[key]; if (!cast) return value; @@ -49,13 +80,12 @@ export abstract class BaseModel { return casted as T; } - // CRUD operations 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) as T; + return this.castAttributes(data); } async find(query?: Partial): Promise { @@ -79,32 +109,84 @@ export abstract class BaseModel { await this.orm.delete(this.options.table, query); } - // Relation methods - hasMany>( - relatedModel: new (orm: GamanORM, options: any) => Related, + /** + * Defines a hasMany relation. + */ + async hasMany( + relatedOptions: BaseModelOptions, + relatedModel: new ( + orm: GamanORM, + options: BaseModelOptions, + ) => BaseModel, foreignKey: string, - localKey: string = 'id', - ): Related[] { - // This would need to be implemented with query builders - // For now, return a placeholder - return []; + 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)); } - belongsTo>( - relatedModel: new (orm: GamanORM, options: any) => Related, + /** + * Defines a belongsTo relation. + */ + async belongsTo( + relatedOptions: BaseModelOptions, + relatedModel: new ( + orm: GamanORM, + options: BaseModelOptions, + ) => BaseModel, foreignKey: string, - ownerKey: string = 'id', - ): Related | null { - // Placeholder for belongsTo relation + 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; } - hasOne>( - relatedModel: new (orm: GamanORM, options: any) => Related, + /** + * Defines a hasOne relation. + */ + async hasOne( + relatedOptions: BaseModelOptions, + relatedModel: new ( + orm: GamanORM, + options: BaseModelOptions, + ) => BaseModel, foreignKey: string, - localKey: string = 'id', - ): Related | null { - // Placeholder for hasOne relation + 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 ca7fc44..95288a3 100644 --- a/plugins/orm/orm.d.ts +++ b/plugins/orm/orm.d.ts @@ -1,12 +1,76 @@ +/** + * @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 fbebcfd..e53faac 100644 --- a/plugins/orm/orm.ts +++ b/plugins/orm/orm.ts @@ -1,34 +1,102 @@ -// orm.ts -import { GamanProvider } from './provider/base.js'; +/** + * @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/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 0e62899..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.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 ae204ce..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.js'; - -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 index 9d0ad16..0091991 100644 --- a/plugins/orm/sample-model.ts +++ b/plugins/orm/sample-model.ts @@ -1,33 +1,61 @@ +/** + * @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: object; + 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) { - const options: BaseModelOptions = { - table: 'users', - casts: { - id: 'int', - created_at: 'datetime', - settings: 'json', - }, - }; - super(orm, options); + super(orm, UserModel.options); } - // Example relation: User has many posts - hasManyPosts() { - return this.hasMany(PostModel, 'user_id'); + /** + * 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; @@ -36,21 +64,38 @@ interface Post { 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) { - const options: BaseModelOptions = { - table: 'posts', - casts: { - id: 'int', - user_id: 'int', - published: 'boolean', - }, - }; - super(orm, options); + super(orm, PostModel.options); } - // Example relation: Post belongs to user - belongsToUser() { - return this.belongsTo(UserModel, 'user_id'); + /** + * 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 index 5be7e34..6a193a0 100644 --- a/plugins/orm/test/orm.test.ts +++ b/plugins/orm/test/orm.test.ts @@ -12,8 +12,8 @@ interface User { } class UserModel extends BaseModel { - constructor(orm: GamanORM) { - const options: BaseModelOptions = { + static getOptions(): BaseModelOptions { + return { table: 'users', casts: { id: 'int', @@ -21,11 +21,20 @@ class UserModel extends BaseModel { settings: 'json', }, }; - super(orm, options); } - hasManyPosts() { - return this.hasMany(PostModel, 'user_id'); + constructor(orm: GamanORM) { + super(orm, UserModel.getOptions()); + } + + async hasManyPosts(userId: number) { + return this.hasMany( + PostModel.getOptions(), + PostModel, + 'user_id', + 'id', + userId, + ); } } @@ -38,8 +47,8 @@ interface Post { } class PostModel extends BaseModel { - constructor(orm: GamanORM) { - const options: BaseModelOptions = { + static getOptions(): BaseModelOptions { + return { table: 'posts', casts: { id: 'int', @@ -47,11 +56,20 @@ class PostModel extends BaseModel { published: 'boolean', }, }; - super(orm, options); } - belongsToUser() { - return this.belongsTo(UserModel, 'user_id'); + constructor(orm: GamanORM) { + super(orm, PostModel.getOptions()); + } + + async belongsToUser(postId: number) { + return this.belongsTo( + UserModel.getOptions(), + UserModel, + 'user_id', + 'id', + postId, + ); } } @@ -147,13 +165,13 @@ describe('GamanORM', () => { }); describe('Relations', () => { - it('should have relation methods available', () => { + it('should have relation methods available', async () => { const userModel = new UserModel(orm); const postModel = new PostModel(orm); - // These are placeholders, but should not throw errors - expect(() => userModel.hasManyPosts()).not.toThrow(); - expect(() => postModel.belongsToUser()).not.toThrow(); + // 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/plugins/orm/test/simple.test.ts b/plugins/orm/test/simple.test.ts deleted file mode 100644 index f51df21..0000000 --- a/plugins/orm/test/simple.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { GamanORM } from '../orm'; -import { BaseModel } from '../model/base'; - -describe('Simple Test', () => { - it('should import GamanORM', () => { - expect(GamanORM).toBeDefined(); - }); - - it('should import BaseModel', () => { - expect(BaseModel).toBeDefined(); - }); -}); From 11578b67fcdf84480c2f54c7edad9b96544b6248 Mon Sep 17 00:00:00 2001 From: fiandev Date: Sun, 19 Oct 2025 10:17:59 +0700 Subject: [PATCH 3/3] feat(orm): release v1.1.0 with improved relation methods Includes refactored type definitions, updated tests, and enhanced TypeScript support. --- plugins/orm/CHANGELOG.md | 19 ++++ plugins/orm/README.md | 203 +++++++++++++++++++++++++++++++++++++++ plugins/orm/package.json | 21 ++++ 3 files changed, 243 insertions(+) create mode 100644 plugins/orm/CHANGELOG.md create mode 100644 plugins/orm/README.md create mode 100644 plugins/orm/package.json 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/package.json b/plugins/orm/package.json new file mode 100644 index 0000000..3288d73 --- /dev/null +++ b/plugins/orm/package.json @@ -0,0 +1,21 @@ +{ + "name": "@gaman/orm", + "version": "1.1.0", + "type": "module", + "main": "index.js", + "author": "angga7togk", + "license": "MIT", + "repository": { + "url": "git+https://github.com/7TogkID/gaman.git", + "directory": "packages/orm" + }, + "bugs": { + "url": "https://github.com/7TogkID/gaman/issues" + }, + "homepage": "https://gaman.7togk.id", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "gitHead": "8d1bf3bbac4b72847e9157e2c95d75b5e90c89cf" +}