From db2f309313c7980ddf8c3ddd81f131e05ecdd83e Mon Sep 17 00:00:00 2001 From: Dimitrije Glibic Date: Sun, 22 Jun 2025 17:46:14 +0200 Subject: [PATCH 1/4] Add presha lib - related with #23 --- core/lib/controller/ExpressController.ts | 78 ++++++++ .../controller/PreshaControllerInterface.ts | 7 + core/lib/kit/PreshaKit.ts | 28 +++ core/lib/kit/PreshaKitInterface.ts | 0 core/lib/model/DrizzleModel.ts | 177 ++++++++++++++++++ core/lib/model/PreshaModelInterface.ts | 7 + core/lib/repository/PreshaRepository.ts | 26 +++ .../repository/PreshaRepositoryInterface.ts | 7 + core/lib/service/PreshaService.ts | 26 +++ core/lib/service/PreshaServiceInterface.ts | 7 + core/package-lock.json | 126 +++++++++++++ core/package.json | 1 + 12 files changed, 490 insertions(+) create mode 100644 core/lib/controller/ExpressController.ts create mode 100644 core/lib/controller/PreshaControllerInterface.ts create mode 100644 core/lib/kit/PreshaKit.ts create mode 100644 core/lib/kit/PreshaKitInterface.ts create mode 100644 core/lib/model/DrizzleModel.ts create mode 100644 core/lib/model/PreshaModelInterface.ts create mode 100644 core/lib/repository/PreshaRepository.ts create mode 100644 core/lib/repository/PreshaRepositoryInterface.ts create mode 100644 core/lib/service/PreshaService.ts create mode 100644 core/lib/service/PreshaServiceInterface.ts diff --git a/core/lib/controller/ExpressController.ts b/core/lib/controller/ExpressController.ts new file mode 100644 index 0000000..94235b0 --- /dev/null +++ b/core/lib/controller/ExpressController.ts @@ -0,0 +1,78 @@ +import { Router, Request, Response } from 'express'; +import { PreshaControllerInterface } from './PreshaControllerInterface'; +import { PreshaServiceInterface } from '../service/PreshaServiceInterface.js'; + +export class ExpressController implements PreshaControllerInterface { + public readonly router: Router; + + constructor(private service: PreshaServiceInterface) { + this.router = Router(); + + this.router.get('/', this.getAllHandler); + this.router.get('/:id', this.getHandler); + this.router.post('/', this.createHandler); + this.router.put('/:id', this.updateHandler); + this.router.delete('/:id', this.deleteHandler); + } + + public async getAll(): Promise { + return this.service.getAll(); + } + + public async get(id: string): Promise { + return this.service.get(id); + } + + public async create(data: Partial): Promise { + return this.service.create(data); + } + + public async update(id: string, data: Partial): Promise { + return this.service.update(id, data); + } + + public async delete(id: string): Promise { + await this.service.delete(id); + } + + private getAllHandler = async ( + req: Request, + res: Response + ): Promise => { + const result = await this.getAll(); + res.json(result); + }; + + private getHandler = async (req: Request, res: Response): Promise => { + const result = await this.get(req.params.id); + if (!result) { + res.status(404).json({ message: 'Not found' }); + return; + } + res.json(result); + }; + + private createHandler = async ( + req: Request, + res: Response + ): Promise => { + const result = await this.create(req.body); + res.status(201).json(result); + }; + + private updateHandler = async ( + req: Request, + res: Response + ): Promise => { + const result = await this.update(req.params.id, req.body); + res.json(result); + }; + + private deleteHandler = async ( + req: Request, + res: Response + ): Promise => { + await this.delete(req.params.id); + res.status(204).send(); + }; +} diff --git a/core/lib/controller/PreshaControllerInterface.ts b/core/lib/controller/PreshaControllerInterface.ts new file mode 100644 index 0000000..01053e7 --- /dev/null +++ b/core/lib/controller/PreshaControllerInterface.ts @@ -0,0 +1,7 @@ +export interface PreshaControllerInterface { + create(data: Partial): Promise; + get(id: string): Promise; + getAll(): Promise; + update(id: string, data: Partial): Promise; + delete(id: string): Promise; +} diff --git a/core/lib/kit/PreshaKit.ts b/core/lib/kit/PreshaKit.ts new file mode 100644 index 0000000..f8795c5 --- /dev/null +++ b/core/lib/kit/PreshaKit.ts @@ -0,0 +1,28 @@ +import { ExpressController } from '../controller/ExpressController.js'; +import { PreshaControllerInterface } from '../controller/PreshaControllerInterface.js'; +import { PreshaModelInterface } from '../model/PreshaModelInterface.js'; +import { PreshaRepository } from '../repository/PreshaRepository.js'; +import { PreshaRepositoryInterface } from '../repository/PreshaRepositoryInterface.js'; +import { PreshaService } from '../service/PreshaService.js'; +import { PreshaServiceInterface } from '../service/PreshaServiceInterface.js'; + +export abstract class PreshaKit { + public readonly model: PreshaModelInterface; + public readonly repository: PreshaRepositoryInterface; + public readonly service: PreshaServiceInterface; + public readonly controller: PreshaControllerInterface; + + constructor( + model: PreshaModelInterface, + repo?: PreshaRepositoryInterface, + svc?: PreshaServiceInterface, + ctrl?: PreshaControllerInterface + ) { + this.model = model; + this.repository = repo ?? new PreshaRepository(model); + this.service = svc ?? new PreshaService(this.repository); + this.controller = ctrl ?? new ExpressController(this.service); + } + + // TODO: add support for hooks +} diff --git a/core/lib/kit/PreshaKitInterface.ts b/core/lib/kit/PreshaKitInterface.ts new file mode 100644 index 0000000..e69de29 diff --git a/core/lib/model/DrizzleModel.ts b/core/lib/model/DrizzleModel.ts new file mode 100644 index 0000000..4fa6cb3 --- /dev/null +++ b/core/lib/model/DrizzleModel.ts @@ -0,0 +1,177 @@ +// core/DrizzleModel.ts + +import { ModelInterface } from './interfaces/ModelInterface.js'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import { + pgTable, + text, + timestamp, + uuid, + boolean, + integer, + json +} from 'drizzle-orm/pg-core'; +import { eq } from 'drizzle-orm'; +import { PreshaModelInterface } from './PreshaModelInterface.js'; + +export type PreshaFieldType = + | 'string' + | 'number' + | 'boolean' + | 'uuid' + | 'timestamp' + | 'json'; + +export interface PreshaField { + type: PreshaFieldType; + primary?: boolean; + unique?: boolean; + nullable?: boolean; + default?: any; +} + +export interface DrizzleModelSchema { + name: string; + fields: Record; +} + +// Database connection +const pool = new Pool({ + connectionString: + process.env.DATABASE_URL || + 'postgresql://gliba:test@localhost:5432/test-app' +}); + +const db = drizzle(pool); + +// Helper function to convert PreshaField to Drizzle column +function createDrizzleColumn(fieldName: string, field: PreshaField) { + const isNullable = field.nullable !== false; + + switch (field.type) { + case 'string': + return text(fieldName).notNull(); + case 'number': + return integer(fieldName).notNull(); + case 'boolean': + return boolean(fieldName).notNull(); + case 'uuid': + return uuid(fieldName).notNull(); + case 'timestamp': + return timestamp(fieldName).notNull(); + case 'json': + return json(fieldName).notNull(); + default: + return text(fieldName).notNull(); + } +} + +// MODEL SHOULD BE INTERFACE, so we can have like DrizzlePreshaModel, PrismaPreshaModel, SequelizePreshaModel, etc. + +export class DrizzleModel implements PreshaModelInterface { + name: string; + fields: Record; + private table: any; + + constructor(schema: DrizzleModelSchema) { + this.name = schema.name; + this.fields = schema.fields; + this.table = this.createTable(); + } + + private createTable() { + const columns: Record = {}; + + // Add all fields as columns + Object.entries(this.fields).forEach(([fieldName, field]) => { + columns[fieldName] = createDrizzleColumn(fieldName, field); + }); + + return pgTable(this.name.toLowerCase(), columns); + } + + getPrimaryKey(): string | undefined { + return Object.entries(this.fields).find(([_, meta]) => meta.primary)?.[0]; + } + + getFieldNames(): string[] { + return Object.keys(this.fields); + } + + getFieldMetadata(field: string): PreshaField | undefined { + return this.fields[field]; + } + + async create(data: any): Promise { + try { + const result = await db.insert(this.table).values(data).returning(); + return (result as any[])[0]; + } catch (error) { + console.error(`Error creating ${this.name}:`, error); + throw error; + } + } + + async findById(id: string): Promise { + try { + const primaryKey = this.getPrimaryKey(); + if (!primaryKey) { + throw new Error('No primary key defined for this model'); + } + + const result = await db + .select() + .from(this.table) + .where(eq(this.table[primaryKey], id)); + return result[0] || null; + } catch (error) { + console.error(`Error finding ${this.name} by id:`, error); + throw error; + } + } + + async findAll(): Promise { + try { + const result = await db.select().from(this.table); + return result; + } catch (error) { + console.error(`Error finding all ${this.name}:`, error); + throw error; + } + } + + async update(id: string, data: any): Promise { + try { + const primaryKey = this.getPrimaryKey(); + if (!primaryKey) { + throw new Error('No primary key defined for this model'); + } + + const result = await db + .update(this.table) + .set(data) + .where(eq(this.table[primaryKey], id)) + .returning(); + + return (result as any[])[0]; + } catch (error) { + console.error(`Error updating ${this.name}:`, error); + throw error; + } + } + + async delete(id: string): Promise { + try { + const primaryKey = this.getPrimaryKey(); + if (!primaryKey) { + throw new Error('No primary key defined for this model'); + } + + await db.delete(this.table).where(eq(this.table[primaryKey], id)); + } catch (error) { + console.error(`Error deleting ${this.name}:`, error); + throw error; + } + } +} diff --git a/core/lib/model/PreshaModelInterface.ts b/core/lib/model/PreshaModelInterface.ts new file mode 100644 index 0000000..39994f1 --- /dev/null +++ b/core/lib/model/PreshaModelInterface.ts @@ -0,0 +1,7 @@ +export interface PreshaModelInterface { + create(data: Partial): Promise; + findById(id: string): Promise; + findAll(): Promise; + update(id: string, data: Partial): Promise; + delete(id: string): Promise; +} diff --git a/core/lib/repository/PreshaRepository.ts b/core/lib/repository/PreshaRepository.ts new file mode 100644 index 0000000..d309e1e --- /dev/null +++ b/core/lib/repository/PreshaRepository.ts @@ -0,0 +1,26 @@ +import { PreshaModelInterface } from '../model/PreshaModelInterface.js'; +import { PreshaRepositoryInterface } from './PreshaRepositoryInterface.js'; + +export class PreshaRepository implements PreshaRepositoryInterface { + constructor(private model: PreshaModelInterface) {} + + async findById(id: string): Promise { + return this.model.findById(id); + } + + async findAll(): Promise { + return this.model.findAll(); + } + + async create(data: Partial): Promise { + return this.model.create(data); + } + + async update(id: string, data: Partial): Promise { + return this.model.update(id, data); + } + + async delete(id: string): Promise { + return this.model.delete(id); + } +} diff --git a/core/lib/repository/PreshaRepositoryInterface.ts b/core/lib/repository/PreshaRepositoryInterface.ts new file mode 100644 index 0000000..2829cc0 --- /dev/null +++ b/core/lib/repository/PreshaRepositoryInterface.ts @@ -0,0 +1,7 @@ +export interface PreshaRepositoryInterface { + findById(id: string): Promise; + findAll(): Promise; + create(data: Partial): Promise; + update(id: string, data: Partial): Promise; + delete(id: string): Promise; +} diff --git a/core/lib/service/PreshaService.ts b/core/lib/service/PreshaService.ts new file mode 100644 index 0000000..292aa0d --- /dev/null +++ b/core/lib/service/PreshaService.ts @@ -0,0 +1,26 @@ +import { PreshaRepositoryInterface } from '../repository/PreshaRepositoryInterface.js'; +import { PreshaServiceInterface } from './PreshaServiceInterface.js'; + +export class PreshaService implements PreshaServiceInterface { + constructor(protected repository: PreshaRepositoryInterface) {} + + async get(id: string): Promise { + return this.repository.findById(id); + } + + async getAll(): Promise { + return this.repository.findAll(); + } + + async create(data: Partial): Promise { + return this.repository.create(data); + } + + async update(id: string, data: Partial): Promise { + return this.repository.update(id, data); + } + + async delete(id: string): Promise { + return this.repository.delete(id); + } +} diff --git a/core/lib/service/PreshaServiceInterface.ts b/core/lib/service/PreshaServiceInterface.ts new file mode 100644 index 0000000..1cd7804 --- /dev/null +++ b/core/lib/service/PreshaServiceInterface.ts @@ -0,0 +1,7 @@ +export interface PreshaServiceInterface { + get(id: string): Promise; + getAll(): Promise; + create(data: Partial): Promise; + update(id: string, data: Partial): Promise; + delete(id: string): Promise; +} diff --git a/core/package-lock.json b/core/package-lock.json index 90215ab..765948a 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "commander": "^14.0.0", + "drizzle-orm": "^0.44.2", "ejs": "^3.1.10", "express": "^5.1.0", "ts-morph": "^26.0.0", @@ -1932,6 +1933,131 @@ "node": ">= 0.8" } }, + "node_modules/drizzle-orm": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.2.tgz", + "integrity": "sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/core/package.json b/core/package.json index 334f804..724097c 100644 --- a/core/package.json +++ b/core/package.json @@ -35,6 +35,7 @@ "homepage": "https://github.com/preshalabs/presha.js#readme", "dependencies": { "commander": "^14.0.0", + "drizzle-orm": "^0.44.2", "ejs": "^3.1.10", "express": "^5.1.0", "ts-morph": "^26.0.0", From 0da99b2e9f2038788e513e36dc1379e95297d89c Mon Sep 17 00:00:00 2001 From: Dimitrije Glibic Date: Sun, 22 Jun 2025 17:50:37 +0200 Subject: [PATCH 2/4] Add PreshaKit and PreshaKitInterface - previously called PreshaUnit --- core/lib/kit/PreshaKit.ts | 3 ++- core/lib/kit/PreshaKitInterface.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/lib/kit/PreshaKit.ts b/core/lib/kit/PreshaKit.ts index f8795c5..c57528d 100644 --- a/core/lib/kit/PreshaKit.ts +++ b/core/lib/kit/PreshaKit.ts @@ -5,8 +5,9 @@ import { PreshaRepository } from '../repository/PreshaRepository.js'; import { PreshaRepositoryInterface } from '../repository/PreshaRepositoryInterface.js'; import { PreshaService } from '../service/PreshaService.js'; import { PreshaServiceInterface } from '../service/PreshaServiceInterface.js'; +import { PreshaKitInterface } from './PreshaKitInterface.js'; -export abstract class PreshaKit { +export abstract class PreshaKit implements PreshaKitInterface { public readonly model: PreshaModelInterface; public readonly repository: PreshaRepositoryInterface; public readonly service: PreshaServiceInterface; diff --git a/core/lib/kit/PreshaKitInterface.ts b/core/lib/kit/PreshaKitInterface.ts index e69de29..aa4039c 100644 --- a/core/lib/kit/PreshaKitInterface.ts +++ b/core/lib/kit/PreshaKitInterface.ts @@ -0,0 +1,11 @@ +import { PreshaControllerInterface } from '../controller/PreshaControllerInterface.js'; +import { PreshaModelInterface } from '../model/PreshaModelInterface.js'; +import { PreshaRepositoryInterface } from '../repository/PreshaRepositoryInterface.js'; +import { PreshaServiceInterface } from '../service/PreshaServiceInterface.js'; + +export interface PreshaKitInterface { + readonly model: PreshaModelInterface; + readonly repository: PreshaRepositoryInterface; + readonly service: PreshaServiceInterface; + readonly controller: PreshaControllerInterface; +} From 1b0175bdec49530b0f76b5290e697d1dac8cded9 Mon Sep 17 00:00:00 2001 From: Dimitrije Glibic Date: Sun, 22 Jun 2025 18:02:12 +0200 Subject: [PATCH 3/4] Export lib and use inheritance in example-project --- core/index.js | 5 - core/index.ts | 1 + core/lib/index.ts | 9 ++ core/lib/model/DrizzleModel.ts | 1 - core/package-lock.json | 164 +++++++++++++++++++- core/package.json | 4 +- examples/example-project/src/domain/user.ts | 93 +++++------ 7 files changed, 217 insertions(+), 60 deletions(-) delete mode 100644 core/index.js create mode 100644 core/index.ts create mode 100644 core/lib/index.ts diff --git a/core/index.js b/core/index.js deleted file mode 100644 index 6801118..0000000 --- a/core/index.js +++ /dev/null @@ -1,5 +0,0 @@ -function helloWorld() { - console.log('Hello, World!'); -} - -module.exports = helloWorld; diff --git a/core/index.ts b/core/index.ts new file mode 100644 index 0000000..09c185e --- /dev/null +++ b/core/index.ts @@ -0,0 +1 @@ +export * from './lib/index.js'; diff --git a/core/lib/index.ts b/core/lib/index.ts new file mode 100644 index 0000000..8f6f5f7 --- /dev/null +++ b/core/lib/index.ts @@ -0,0 +1,9 @@ +export * from './controller/PreshaControllerInterface.js'; +export * from './controller/ExpressController.js'; +export * from './model/PreshaModelInterface.js'; +export * from './model/DrizzleModel.js'; +export * from './repository/PreshaRepositoryInterface.js'; +export * from './repository/PreshaRepository.js'; +export * from './service/PreshaServiceInterface.js'; +export * from './service/PreshaService.js'; +export * from './kit/PreshaKit.js'; diff --git a/core/lib/model/DrizzleModel.ts b/core/lib/model/DrizzleModel.ts index 4fa6cb3..8f92669 100644 --- a/core/lib/model/DrizzleModel.ts +++ b/core/lib/model/DrizzleModel.ts @@ -1,6 +1,5 @@ // core/DrizzleModel.ts -import { ModelInterface } from './interfaces/ModelInterface.js'; import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; import { diff --git a/core/package-lock.json b/core/package-lock.json index 765948a..95dbd37 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -13,6 +13,7 @@ "drizzle-orm": "^0.44.2", "ejs": "^3.1.10", "express": "^5.1.0", + "pg": "^8.16.2", "ts-morph": "^26.0.0", "typescript": "^5.8.3", "zod": "^3.25.62" @@ -25,6 +26,7 @@ "@types/ejs": "^3.1.5", "@types/express": "^5.0.3", "@types/node": "^22.15.29", + "@types/pg": "^8.15.4", "chokidar": "^4.0.3", "eslint": "^9.28.0", "eslint-config-prettier": "^10.1.5", @@ -1249,12 +1251,24 @@ "version": "22.15.29", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/pg": { + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz", + "integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -3563,6 +3577,95 @@ "dev": true, "license": "MIT" }, + "node_modules/pg": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.2.tgz", + "integrity": "sha512-OtLWF0mKLmpxelOt9BqVq83QV6bTfsS0XLegIeAKqKjurRnRKie1Dc1iL89MugmSLhftxw6NNCyZhm1yQFLMEQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.2", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.6" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.6.tgz", + "integrity": "sha512-uxmJAnmIgmYgnSFzgOf2cqGQBzwnRYcrEgXuFjJNEkpedEIPBSEzxY7ph4uA9k1mI+l/GR0HjPNS6FKNZe8SBQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.2.tgz", + "integrity": "sha512-Ci7jy8PbaWxfsck2dwZdERcDG2A0MG8JoQILs+uZNjABFuBuItAZCWUNz8sXRDMoui24rJw7WlXqgpMdBSN/vQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3648,6 +3751,45 @@ } } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4085,6 +4227,15 @@ "node": ">= 8" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4517,7 +4668,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unpipe": { @@ -4697,6 +4848,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/core/package.json b/core/package.json index 724097c..4c17040 100644 --- a/core/package.json +++ b/core/package.json @@ -13,7 +13,7 @@ "main": "dist/index.js", "type": "module", "exports": { - ".": "./dist/index.js", + ".": "./dist/index.ts", "./runtime/express": { "types": "./dist/runtime/express.d.ts", "import": "./dist/runtime/express.js" @@ -38,6 +38,7 @@ "drizzle-orm": "^0.44.2", "ejs": "^3.1.10", "express": "^5.1.0", + "pg": "^8.16.2", "ts-morph": "^26.0.0", "typescript": "^5.8.3", "zod": "^3.25.62" @@ -47,6 +48,7 @@ "@types/ejs": "^3.1.5", "@types/express": "^5.0.3", "@types/node": "^22.15.29", + "@types/pg": "^8.15.4", "chokidar": "^4.0.3", "eslint": "^9.28.0", "eslint-config-prettier": "^10.1.5", diff --git a/examples/example-project/src/domain/user.ts b/examples/example-project/src/domain/user.ts index d6949c5..c0f8f24 100644 --- a/examples/example-project/src/domain/user.ts +++ b/examples/example-project/src/domain/user.ts @@ -1,67 +1,58 @@ /** - * This interface defines the structure of the `User` entity. + * This class defines the structure of the `User` entity. * * 🧩 This is a **required** part of the domain layer. - * The `presha.js` framework uses this interface as the source of truth to auto-generate: - * - Zod validation schemas + * The `presha.js` framework uses this class as the source of truth to auto-generate: * - Database models (Prisma, Drizzle) * - Repositories * - Services * - Controllers - * - Routes * - * 🚨 Do not rename this interface or move it outside of the `domain/` folder, - * or it may not be picked up by the framework's parser. */ -export interface User { + +import { DrizzleModel, PreshaKit } from 'presha'; + +// TODO: Try to auto-generate this interface from the DrizzleModel, so we don't have to manually define it. +export interface UserModel { id: string; email: string; - name?: string; createdAt: Date; - updatedAt: Date; } -/** - * Optional lifecycle hooks for the User model. - * - * These hooks allow you to insert logic before/after core actions like create, update, delete, or read. - * You can use them for validation, logging, authorization checks, etc. - * - * If omitted, the framework will simply skip hook execution. - */ -export const hooks = { - beforeCreate: async (data: Partial) => { - if (!data.email?.includes('@')) { - throw new Error('Invalid email'); - } - console.log('[User.beforeCreate]', data); - }, - - afterCreate: async (user: User) => { - console.log('[User.afterCreate]', user.id); - }, - - beforeUpdate: async (data: Partial) => { - console.log('[User.beforeUpdate]', data); - }, - - afterUpdate: async (user: User) => { - console.log('[User.afterUpdate]', user.id); - }, - - beforeDelete: async (id: string) => { - console.log('[User.beforeDelete]', id); - }, - - afterDelete: async (id: string) => { - console.log('[User.afterDelete]', id); - }, - - beforeRead: async (id: string) => { - console.log('[User.beforeRead]', id); - }, +const userModel = new DrizzleModel({ + name: 'User', + fields: { + id: { type: 'uuid', primary: true }, + email: { type: 'string', unique: true }, + createdAt: { type: 'timestamp' } + } +}); - afterRead: async (user: User | null) => { - console.log('[User.afterRead]', user?.id ?? 'not found'); +export class User extends PreshaKit { + constructor() { + super(userModel); } -}; + + /** + * Optional lifecycle hooks for the User model. + * + * These hooks allow you to insert logic before/after core actions like create, update, delete, or read. + * You can use them for validation, logging, authorization checks, etc. + * + * If omitted, the framework will simply skip hook execution. + */ + // TODO: add support for hooks + // optional + // protected async beforeCreate(data: Partial) { + // console.log('🔍 Validating user before create:', data.email); + // if (!data.email?.includes('@')) { + // throw new Error('Invalid email'); + // } + // return data; + // } + + // optional + // protected async afterCreate(user: UserModel) { + // console.log('✅ Created user:', user.id); + // } +} From fb4ea2221c0c5506917388903637ee27230b696f Mon Sep 17 00:00:00 2001 From: Dimitrije Glibic Date: Sun, 29 Jun 2025 15:51:02 +0200 Subject: [PATCH 4/4] Extract types --- core/lib/model/DrizzleModel.ts | 44 ++++++++------------------ core/lib/model/PreshaModelInterface.ts | 4 +++ core/lib/types/PreshaSchema.ts | 20 ++++++++++++ 3 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 core/lib/types/PreshaSchema.ts diff --git a/core/lib/model/DrizzleModel.ts b/core/lib/model/DrizzleModel.ts index 8f92669..578a19c 100644 --- a/core/lib/model/DrizzleModel.ts +++ b/core/lib/model/DrizzleModel.ts @@ -13,27 +13,7 @@ import { } from 'drizzle-orm/pg-core'; import { eq } from 'drizzle-orm'; import { PreshaModelInterface } from './PreshaModelInterface.js'; - -export type PreshaFieldType = - | 'string' - | 'number' - | 'boolean' - | 'uuid' - | 'timestamp' - | 'json'; - -export interface PreshaField { - type: PreshaFieldType; - primary?: boolean; - unique?: boolean; - nullable?: boolean; - default?: any; -} - -export interface DrizzleModelSchema { - name: string; - fields: Record; -} +import { PreshaField, PreshaModelSchema } from '../types/PreshaSchema'; // Database connection const pool = new Pool({ @@ -46,7 +26,7 @@ const db = drizzle(pool); // Helper function to convert PreshaField to Drizzle column function createDrizzleColumn(fieldName: string, field: PreshaField) { - const isNullable = field.nullable !== false; + // const isNullable = field.nullable !== false; switch (field.type) { case 'string': @@ -66,14 +46,16 @@ function createDrizzleColumn(fieldName: string, field: PreshaField) { } } -// MODEL SHOULD BE INTERFACE, so we can have like DrizzlePreshaModel, PrismaPreshaModel, SequelizePreshaModel, etc. +// TODO: Need to introduce ts type for each model in order to get rid of any type, and accomplish full type safety. +// Possible solutions: +// based on schema export class DrizzleModel implements PreshaModelInterface { name: string; fields: Record; private table: any; - constructor(schema: DrizzleModelSchema) { + constructor(schema: PreshaModelSchema) { this.name = schema.name; this.fields = schema.fields; this.table = this.createTable(); @@ -91,7 +73,7 @@ export class DrizzleModel implements PreshaModelInterface { } getPrimaryKey(): string | undefined { - return Object.entries(this.fields).find(([_, meta]) => meta.primary)?.[0]; + return Object.entries(this.fields).find(([, meta]) => meta.primary)?.[0]; } getFieldNames(): string[] { @@ -102,17 +84,17 @@ export class DrizzleModel implements PreshaModelInterface { return this.fields[field]; } - async create(data: any): Promise { + async create(data: Partial): Promise { try { const result = await db.insert(this.table).values(data).returning(); - return (result as any[])[0]; + return result[0]; } catch (error) { console.error(`Error creating ${this.name}:`, error); throw error; } } - async findById(id: string): Promise { + async findById(id: string): Promise { try { const primaryKey = this.getPrimaryKey(); if (!primaryKey) { @@ -130,7 +112,7 @@ export class DrizzleModel implements PreshaModelInterface { } } - async findAll(): Promise { + async findAll(): Promise { try { const result = await db.select().from(this.table); return result; @@ -140,7 +122,7 @@ export class DrizzleModel implements PreshaModelInterface { } } - async update(id: string, data: any): Promise { + async update(id: string, data: Partial): Promise { try { const primaryKey = this.getPrimaryKey(); if (!primaryKey) { @@ -153,7 +135,7 @@ export class DrizzleModel implements PreshaModelInterface { .where(eq(this.table[primaryKey], id)) .returning(); - return (result as any[])[0]; + return result[0]; } catch (error) { console.error(`Error updating ${this.name}:`, error); throw error; diff --git a/core/lib/model/PreshaModelInterface.ts b/core/lib/model/PreshaModelInterface.ts index 39994f1..5b33803 100644 --- a/core/lib/model/PreshaModelInterface.ts +++ b/core/lib/model/PreshaModelInterface.ts @@ -1,4 +1,8 @@ +import { PreshaField } from '../types/PreshaSchema'; + export interface PreshaModelInterface { + name: string; + fields: Record; create(data: Partial): Promise; findById(id: string): Promise; findAll(): Promise; diff --git a/core/lib/types/PreshaSchema.ts b/core/lib/types/PreshaSchema.ts new file mode 100644 index 0000000..19a9bbb --- /dev/null +++ b/core/lib/types/PreshaSchema.ts @@ -0,0 +1,20 @@ +export type PreshaFieldType = + | 'string' + | 'number' + | 'boolean' + | 'uuid' + | 'timestamp' + | 'json'; + +export interface PreshaField { + type: PreshaFieldType; + primary?: boolean; + unique?: boolean; + nullable?: boolean; + default?: unknown; +} + +export interface PreshaModelSchema { + name: string; + fields: Record; +}