From e699a961be1232ba1b9c374b32d8bee9df9ce9b9 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 17 Nov 2025 23:44:45 +0330 Subject: [PATCH] seed --- package-lock.json | 33 ++ package.json | 2 + src/config/mikro-orm.config.dev.ts | 5 + src/modules/foods/entities/food.entity.ts | 4 +- src/seeders/DatabaseSeeder.ts | 466 ++++++++++++++++++++++ src/seeders/index.ts | 4 + tsconfig.json | 7 +- 7 files changed, 517 insertions(+), 4 deletions(-) create mode 100644 src/seeders/DatabaseSeeder.ts create mode 100644 src/seeders/index.ts diff --git a/package-lock.json b/package-lock.json index 6d1f2da..0adba81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.18.0", "@mikro-orm/cli": "^6.5.8", + "@mikro-orm/seeder": "^6.5.8", "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", @@ -3529,6 +3530,38 @@ "@mikro-orm/core": "^6.0.0" } }, + "node_modules/@mikro-orm/seeder": { + "version": "6.5.8", + "resolved": "https://registry.npmjs.org/@mikro-orm/seeder/-/seeder-6.5.8.tgz", + "integrity": "sha512-EVfXNJ+Q7I1jKLlYwWDdQ/67DxhuBSfS2U/XKKo/+uEaxGBl5vDe8kV3RNRwx8I9x6DHsQpt1NzJL+JS3fSt3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "11.3.2", + "globby": "11.1.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, + "node_modules/@mikro-orm/seeder/node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", diff --git a/package.json b/package.json index 4085744..0d8bd0a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "db:create": "npx mikro-orm schema:create --run --config ./src/config/mikro-orm.config.dev.ts", "db:update": "npx mikro-orm schema:update --run --config ./src/config/mikro-orm.config.dev.ts", "db:drop": "npx mikro-orm schema:drop --run --config ./src/config/mikro-orm.config.dev.ts", + "db:seed": "npx mikro-orm seeder:run --config ./src/config/mikro-orm.config.dev.ts", "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start dist/main", @@ -54,6 +55,7 @@ "ulid": "^3.0.1" }, "devDependencies": { + "@mikro-orm/seeder": "^6.5.8", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.18.0", "@mikro-orm/cli": "^6.5.8", diff --git a/src/config/mikro-orm.config.dev.ts b/src/config/mikro-orm.config.dev.ts index d0bc610..9e7439e 100644 --- a/src/config/mikro-orm.config.dev.ts +++ b/src/config/mikro-orm.config.dev.ts @@ -62,6 +62,11 @@ export default defineConfig({ }, connect: true, allowGlobalContext: true, + seeder: { + path: './dist/seeders', + pathTs: './src/seeders', + defaultSeeder: 'DatabaseSeeder', + }, driverOptions: { connection: { keepAlive: true, diff --git a/src/modules/foods/entities/food.entity.ts b/src/modules/foods/entities/food.entity.ts index f2dfa4b..64f3e8f 100644 --- a/src/modules/foods/entities/food.entity.ts +++ b/src/modules/foods/entities/food.entity.ts @@ -1,11 +1,11 @@ -import { Collection, Entity, ManyToMany, OneToOne, Property } from '@mikro-orm/core'; +import { Collection, Entity, ManyToMany, ManyToOne, Property } from '@mikro-orm/core'; import { Category } from './category.entity'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity'; @Entity({ tableName: 'foods' }) export class Food extends BaseEntity { - @OneToOne(() => Restaurant) + @ManyToOne(() => Restaurant) restaurant: Restaurant; @ManyToMany(() => Category) diff --git a/src/seeders/DatabaseSeeder.ts b/src/seeders/DatabaseSeeder.ts new file mode 100644 index 0000000..0e55e93 --- /dev/null +++ b/src/seeders/DatabaseSeeder.ts @@ -0,0 +1,466 @@ +import type { EntityManager } from '@mikro-orm/core'; +import { Seeder } from '@mikro-orm/seeder'; +import { Category } from '../modules/foods/entities/category.entity'; +import { Food } from '../modules/foods/entities/food.entity'; +import { Admin } from '../modules/admin/entities/admin.entity'; +import { Permission } from '../modules/admin/entities/permission.entity'; +import { Role } from '../modules/admin/entities/role.entity'; +import { Restaurant } from '../modules/restaurants/entities/restaurant.entity'; +import { User } from '../modules/users/entities/user.entity'; + +export class DatabaseSeeder extends Seeder { + async run(em: EntityManager) { + // 1. Create Permissions (English) + const permissions = [ + { name: 'manage_restaurants' }, + { name: 'manage_foods' }, + { name: 'manage_categories' }, + { name: 'manage_orders' }, + { name: 'manage_admins' }, + { name: 'manage_users' }, + { name: 'view_reports' }, + { name: 'manage_finances' }, + ]; + + const createdPermissions: Permission[] = []; + for (const permData of permissions) { + let permission = await em.findOne(Permission, { name: permData.name }); + if (!permission) { + permission = em.create(Permission, permData); + em.persist(permission); + } + createdPermissions.push(permission); + } + + // 2. Create Roles (English) + const ownerRole = await em.findOne(Role, { name: 'owner' }); + if (!ownerRole) { + const role = em.create(Role, { name: 'owner' }); + // Add all permissions to owner + createdPermissions.forEach(p => role.permissions.add(p)); + em.persist(role); + } + + const accountantRole = await em.findOne(Role, { name: 'accountant' }); + if (!accountantRole) { + const role = em.create(Role, { name: 'accountant' }); + // Add financial permissions to accountant + const financialPerms = createdPermissions.filter( + p => p.name === 'view_reports' || p.name === 'manage_finances' || p.name === 'manage_orders', + ); + financialPerms.forEach(p => role.permissions.add(p)); + em.persist(role); + } + + await em.flush(); + + // Get roles after flush + const owner = await em.findOne(Role, { name: 'owner' }); + const accountant = await em.findOne(Role, { name: 'accountant' }); + + // 3. Create Restaurants (Farsi) + const zhivanRestaurant = await em.findOne(Restaurant, { slug: 'zhivan' }); + if (!zhivanRestaurant) { + const restaurant = em.create(Restaurant, { + name: 'ژیوان', + slug: 'zhivan', + isActive: true, + }); + em.persist(restaurant); + } + + const booteRestaurant = await em.findOne(Restaurant, { slug: 'boote' }); + if (!booteRestaurant) { + const restaurant = em.create(Restaurant, { + name: 'بوته', + slug: 'boote', + isActive: true, + }); + em.persist(restaurant); + } + + await em.flush(); + + // Get restaurants after flush + const zhivan = await em.findOne(Restaurant, { slug: 'zhivan' }); + const boote = await em.findOne(Restaurant, { slug: 'boote' }); + + // 4. Create Categories (Farsi) + const categories = [ + { title: 'غذای اصلی', restId: zhivan?.id }, + { title: 'پیش غذا', restId: zhivan?.id }, + { title: 'دسر', restId: zhivan?.id }, + { title: 'نوشیدنی', restId: zhivan?.id }, + { title: 'غذای اصلی', restId: boote?.id }, + { title: 'پیش غذا', restId: boote?.id }, + { title: 'دسر', restId: boote?.id }, + { title: 'نوشیدنی', restId: boote?.id }, + ]; + + const createdCategories: Category[] = []; + for (const catData of categories) { + const existing = await em.findOne(Category, { title: catData.title, restId: catData.restId }); + if (!existing) { + const category = em.create(Category, { + title: catData.title, + restId: catData.restId, + isActive: true, + }); + em.persist(category); + createdCategories.push(category); + } else { + createdCategories.push(existing); + } + } + + await em.flush(); + + // 5. Create Foods (Farsi) + const foods = [ + // Zhivan Restaurant Foods + { + title: 'کباب کوبیده', + desc: 'کباب کوبیده خوشمزه با برنج و کره', + price: 150000, + restaurant: zhivan, + categories: [createdCategories[0]], // غذای اصلی + isActive: true, + stock: 50, + stockDefault: 50, + }, + { + title: 'جوجه کباب', + desc: 'جوجه کباب تازه با برنج', + price: 120000, + restaurant: zhivan, + categories: [createdCategories[0]], // غذای اصلی + isActive: true, + stock: 40, + stockDefault: 40, + }, + { + title: 'کباب بختیاری', + desc: 'کباب بختیاری با برنج و کره', + price: 180000, + restaurant: zhivan, + categories: [createdCategories[0]], // غذای اصلی + isActive: true, + stock: 35, + stockDefault: 35, + }, + { + title: 'کباب برگ', + desc: 'کباب برگ مرغوب با برنج', + price: 160000, + restaurant: zhivan, + categories: [createdCategories[0]], // غذای اصلی + isActive: true, + stock: 30, + stockDefault: 30, + }, + { + title: 'سالاد فصل', + desc: 'سالاد تازه با سس مخصوص', + price: 35000, + restaurant: zhivan, + categories: [createdCategories[1]], // پیش غذا + isActive: true, + stock: 30, + stockDefault: 30, + }, + { + title: 'ماست و خیار', + desc: 'ماست و خیار تازه', + price: 25000, + restaurant: zhivan, + categories: [createdCategories[1]], // پیش غذا + isActive: true, + stock: 25, + stockDefault: 25, + }, + { + title: 'ترشی', + desc: 'ترشی مخلوط خانگی', + price: 20000, + restaurant: zhivan, + categories: [createdCategories[1]], // پیش غذا + isActive: true, + stock: 20, + stockDefault: 20, + }, + { + title: 'بستنی', + desc: 'بستنی وانیلی خوشمزه', + price: 25000, + restaurant: zhivan, + categories: [createdCategories[2]], // دسر + isActive: true, + stock: 20, + stockDefault: 20, + }, + { + title: 'شیرینی', + desc: 'شیرینی محلی', + price: 30000, + restaurant: zhivan, + categories: [createdCategories[2]], // دسر + isActive: true, + stock: 15, + stockDefault: 15, + }, + { + title: 'نوشابه', + desc: 'نوشابه گازدار', + price: 15000, + restaurant: zhivan, + categories: [createdCategories[3]], // نوشیدنی + isActive: true, + stock: 100, + stockDefault: 100, + }, + { + title: 'دوغ', + desc: 'دوغ محلی', + price: 20000, + restaurant: zhivan, + categories: [createdCategories[3]], // نوشیدنی + isActive: true, + stock: 50, + stockDefault: 50, + }, + { + title: 'آب معدنی', + desc: 'آب معدنی', + price: 10000, + restaurant: zhivan, + categories: [createdCategories[3]], // نوشیدنی + isActive: true, + stock: 80, + stockDefault: 80, + }, + // Boote Restaurant Foods + { + title: 'پیتزا مخصوص', + desc: 'پیتزا با پنیر و قارچ', + price: 180000, + restaurant: boote, + categories: [createdCategories[4]], // غذای اصلی + isActive: true, + stock: 30, + stockDefault: 30, + }, + { + title: 'پیتزا پپرونی', + desc: 'پیتزا پپرونی تند', + price: 200000, + restaurant: boote, + categories: [createdCategories[4]], // غذای اصلی + isActive: true, + stock: 25, + stockDefault: 25, + }, + { + title: 'برگر', + desc: 'برگر با سیب زمینی سرخ کرده', + price: 140000, + restaurant: boote, + categories: [createdCategories[4]], // غذای اصلی + isActive: true, + stock: 25, + stockDefault: 25, + }, + { + title: 'برگر مخصوص', + desc: 'برگر دوبل با پنیر', + price: 180000, + restaurant: boote, + categories: [createdCategories[4]], // غذای اصلی + isActive: true, + stock: 20, + stockDefault: 20, + }, + { + title: 'سوپ', + desc: 'سوپ گرم و خوشمزه', + price: 45000, + restaurant: boote, + categories: [createdCategories[5]], // پیش غذا + isActive: true, + stock: 15, + stockDefault: 15, + }, + { + title: 'سالاد سزار', + desc: 'سالاد سزار با سس مخصوص', + price: 55000, + restaurant: boote, + categories: [createdCategories[5]], // پیش غذا + isActive: true, + stock: 20, + stockDefault: 20, + }, + { + title: 'سیب زمینی سرخ کرده', + desc: 'سیب زمینی سرخ کرده ترد', + price: 40000, + restaurant: boote, + categories: [createdCategories[5]], // پیش غذا + isActive: true, + stock: 30, + stockDefault: 30, + }, + { + title: 'کیک شکلاتی', + desc: 'کیک شکلاتی خامه‌ای', + price: 60000, + restaurant: boote, + categories: [createdCategories[6]], // دسر + isActive: true, + stock: 10, + stockDefault: 10, + }, + { + title: 'چیزکیک', + desc: 'چیزکیک وانیلی', + price: 65000, + restaurant: boote, + categories: [createdCategories[6]], // دسر + isActive: true, + stock: 12, + stockDefault: 12, + }, + { + title: 'نوشابه', + desc: 'نوشابه گازدار', + price: 15000, + restaurant: boote, + categories: [createdCategories[7]], // نوشیدنی + isActive: true, + stock: 100, + stockDefault: 100, + }, + { + title: 'آبمیوه طبیعی', + desc: 'آبمیوه تازه', + price: 35000, + restaurant: boote, + categories: [createdCategories[7]], // نوشیدنی + isActive: true, + stock: 40, + stockDefault: 40, + }, + { + title: 'قهوه', + desc: 'قهوه اسپرسو', + price: 45000, + restaurant: boote, + categories: [createdCategories[7]], // نوشیدنی + isActive: true, + stock: 50, + stockDefault: 50, + }, + ]; + + for (const foodData of foods) { + if (foodData.restaurant) { + const existing = await em.findOne(Food, { + title: foodData.title, + restaurant: foodData.restaurant, + }); + if (!existing) { + const food = em.create(Food, { + title: foodData.title, + desc: foodData.desc, + price: foodData.price, + restaurant: foodData.restaurant, + isActive: foodData.isActive, + stock: foodData.stock, + stockDefault: foodData.stockDefault, + sat: false, + sun: false, + mon: false, + breakfast: false, + noon: false, + dinner: false, + discount: 0, + rate: 0, + inPlaceServe: false, + pickupServe: false, + }); + // Add categories + if (foodData.categories) { + foodData.categories.forEach(cat => food.categories.add(cat)); + } + em.persist(food); + } + } + } + + await em.flush(); + + // 6. Create Admins (Farsi) + const adminMorteza = await em.findOne(Admin, { phone: '09362532122' }); + if (!adminMorteza && owner && zhivan) { + const admin = em.create(Admin, { + phone: '09362532122', + firstName: 'مرتضی', + lastName: 'مرتضایی', + role: owner, + restaurant: zhivan, + }); + em.persist(admin); + } + + const adminHamid = await em.findOne(Admin, { phone: '09185290775' }); + if (!adminHamid && accountant && boote) { + const admin = em.create(Admin, { + phone: '09185290775', + firstName: 'حمید', + lastName: 'زرگامی', + role: accountant, + restaurant: boote, + }); + em.persist(admin); + } + + await em.flush(); + + // 7. Create Users (Farsi) + const userMorteza = await em.findOne(User, { phone: '09362532122' }); + if (!userMorteza && zhivan) { + const user = em.create(User, { + phone: '09362532122', + firstName: 'مرتضی', + lastName: 'مرتضایی', + restaurant: zhivan, + birthDate: new Date('1990-01-01'), + marriageDate: new Date('2015-01-01'), + isActive: true, + gender: true, + wallet: 0, + points: 0, + }); + em.persist(user); + } + + const userHamid = await em.findOne(User, { phone: '09185290775' }); + if (!userHamid && boote) { + const user = em.create(User, { + phone: '09185290775', + firstName: 'حمید', + lastName: 'زرگامی', + restaurant: boote, + birthDate: new Date('1992-01-01'), + marriageDate: new Date('2017-01-01'), + isActive: true, + gender: true, + wallet: 0, + points: 0, + }); + em.persist(user); + } + + // Final flush + await em.flush(); + } +} diff --git a/src/seeders/index.ts b/src/seeders/index.ts new file mode 100644 index 0000000..974d557 --- /dev/null +++ b/src/seeders/index.ts @@ -0,0 +1,4 @@ +import { DatabaseSeeder } from './DatabaseSeeder'; + +export { DatabaseSeeder }; +export default DatabaseSeeder; diff --git a/tsconfig.json b/tsconfig.json index aba29b0..98b93ab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,13 +13,16 @@ "target": "ES2023", "sourceMap": true, "outDir": "./dist", - "baseUrl": "./", "incremental": true, "skipLibCheck": true, "strictNullChecks": true, "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strictBindCallApply": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "baseUrl": "./", + "paths": { + "src/*": ["./src/*"] + } } }