پایگاه داده
دیتابیس PostgreSQL است و با Prisma (apps/api/prisma/schema.prisma)
مدیریت میشود. مهاجرتها در apps/api/prisma/migrations/ قرار دارند و هنگام
بالا آمدنِ container با prisma migrate deploy اعمال میشوند.
این صفحه یک مرجعِ سریع است؛ اسکیمای Prisma منبعِ کانونیکال است و اگر جایی
اختلاف دیدید، آن ملاک است. برای نمودارِ کامل و تمام فیلدها، خودِ اسکیما را در
apps/api/prisma/schema.prisma
ببینید.
مدلهای اصلی
| Model | توضیح کوتاه |
|---|---|
User | حساب کاربری، احراز هویت، توکنها |
Calendar | تقویم کاربر (جلالی/میلادی) |
Event | رویداد (با RRULE و exdate) |
Task | وظیفه (سلسلهمراتبی، با subtask) |
Reminder | یادآور (متصل به event یا task) |
Device | دستگاه متصل به حساب (احراز هویتِ مبتنی بر دستگاه) |
SyncConflict | نسخهٔ بازندهٔ تعارضهای همگامسازی |
CalendarShare | اشتراکگذاری تقویم بین کاربران |
PersianHoliday | تعطیلات رسمی ا یران |
ارتباطات کلیدی
- یک User → چند Calendar و چند Device.
- یک Calendar → چند Event و چند Task.
- یک Event → چند Task و چند Reminder.
- یک Task → چند subtask (خودارجاع با
parentTaskId) و چند Reminder. - یک Reminder → دقیقاً یکی از
eventیاtask. - یک User → چند SyncConflict.
ستونهای همگامسازی (uid, version)
چهار موجودیتِ همگامشونده — Calendar، Event، Task، Reminder — هر کدام دو ستونِ زیر را دارند:
| ستون | نوع | نقش |
|---|---|---|
uid | String (یکتا، پیشفرض gen_random_uuid()) | شناسهٔ پایدار و مستقل از DB؛ کلیدِ همگامسازی بین دستگاهها |
version | BigInt | ش مارهٔ نسخهٔ مونوتونِ رکورد؛ مبنای مقایسه در همگامسازی (LWW) |
version از یک دنبالهٔ سراسری بهنام record_version_seq پر میشود. یک تریگر
بهنام bump_record_version() روی هر INSERT/UPDATE از این چهار جدول اجرا
میشود: ابتدا یک advisory lock کاربر-محور میگیرد (تا دو نوشتنِ همزمان برای
یک کاربر نسخهٔ پشتسرهم نگیرند)، سپس version را با nextval('record_version_seq')
و updated را با now() جایگذاری میکند.
چون version از نوع BigInt است، روی wire همیشه بهصورت string بازمیگردد.
(uid/version فقط روی همین چهار موجودیت هستند؛ User, Device, SyncConflict,
CalendarShare, PersianHoliday آنها را ندارند. حذفِ نرم deleted گستردهتر است؛
جزئیات در استاندارد زمانها.)
چند ستون از پروتکلِ قدیمیِ همگامسازی در اسکیما ماندهاند ولی موتورِ sync فعلی هرگز آن ها را نه میخواند و نه مینویسد:
Event.syncStatus(SYNCED/PENDING/CONFLICT) — تنها مصرفکنندههایشGET /api/users/statsوPOST /api/users/reset-syncهستند.User.lastSyncTime— فقطreset-syncآن راnullمیکند.User.googleCalendarId— هیچ کدی آن را مصرف نمیکند.
وضعیتِ همگامسازیِ واقعی از ستونِ version و cursorهای pull ساخته میشود، نه از
این ستونها.
User
id (PK)، email (یکتا، اختیاری)، phone (یکتا، ضروری)، password، name،
role (USER/ADMIN)، isActive، isPhoneVerified، otpCode/otpExpiresAt
(پنهان)، lastSyncTime، googleCalendarId، deviceId، fcmToken،
refreshToken/refreshTokenExpires (پنهان؛ مسیرِ قدیمیِ تکتوکنی)،
notificationSettings (syncNotifications/eventReminders/conflictAlerts)،
created/updated/deleted.
ایندکسها: email (یکتا)، phone (یکتا).
User ستونهای uid/version همگامسازی را ندارد؛ همگامسازی روی
Calendar/Event/Task/Reminder انجام میشود.
Calendar
id، userId، uid، version، name، description، color (عددِ صحیحِ
ARGB، پیشفرض -123164116)، emoji، isDefault (پرچمِ تقویمِ پیشفرض)،
isVisible، calendarType (JALALI/GREGORIAN، پیشفرض JALALI)، isActive،
created/updated/deleted.
ایندکسها: { userId, isDefault }، { userId, isVisible, isActive }،
{ userId, calendarType }، { userId, version }.
Event
id، calendarId، uid، version، title، description، location،
startTime/endTime (UTC، منبعِ حقیقت)، allDay، rrule (رشتهٔ RFC 5545 بدون
پیشوندِ RRULE:)، exdate (کاما-جداشدهٔ ISO-8601 از شروعِ رخدادهای مستثنا)،
rdate/timezone/recurringEventUid/originalStartTime (رزرو برای فازهای بعدی)،
color (عددِ صحیحِ ARGB)، emoji، isPrivate، type (JALALI/GREGORIAN)، source
(LOCAL/GOOGLE/ANDROID_PROVIDER/SERVER)، sourceId، syncStatus
(SYNCED/PENDING/CONFLICT)، created/updated/deleted.
ایندکسها: { calendarId, deleted, updated }، { calendarId, source, sourceId }
(یکتا)، { calendarId, startTime, deleted }، { calendarId, endTime, deleted }،
{ calendarId, type }، { calendarId, version }،
{ recurringEventUid, originalStartTime }.
یک فیلدِ مجازی هم دارد: duration = endTime - startTime.
Task
id، userId، uid، version، calendarId (اختیاری)، eventId (اختیاری)،
parentTaskId (اختیاری، برای subtask)، title، description، isCompleted،
priority (LOW/MEDIUM/HIGH/URGENT)، dueDate، completedAt، color
(عددِ صحیحِ ARGB)، emoji، order، created/updated/deleted.
Task دقیقاً مثل Event ستونهای همگامسازی (uid/version) را دارد و از همان
دنباله و تریگرِ سراسری استفاده میکند؛ بنابراین وظیفهها هم از /api/sync همگام
میشوند.
ایندکسها: { userId, deleted, isCompleted }، { eventId, deleted, order }،
{ parentTaskId, deleted, order }، { userId, eventId, deleted }،
{ userId, parentTaskId, deleted }، { calendarId, deleted, isCompleted }،
{ userId, version }.
Reminder
id، eventId (اختیاری)، taskId (اختیاری)، uid، version، value (عددِ
مثبت)، type (MINUTE/HOUR/DAY/WEEK)، isEnabled،
created/updated/deleted.
دقیقاً یکی از eventId یا taskId باید ست شود؛ قیدِ CHECK بهنام
reminders_parent_xor این را در دیتابیس اجرا میکند.
ایندکسها: { eventId, deleted, isEnabled }، { taskId, deleted, isEnabled }،
{ eventId, version }، { taskId, version }.
زمانبندیِ واقعیِ یادآورها با کلاینتهاست: اندروید با AlarmManager و وب با
مرورگر یادآورها را محلی زمانبندی میکنند. بکاند فقط پیکربندی را ذخیره و بین
پلتفرمها همگام میکند — خودش اعلان زمانبندی نمیکند.
Device
مدلِ دستگاه برای احراز هویتِ مبتنی بر دستگاه.
id، userId، uid (ساختهشده توسط کلاینت)، platform
(ANDROID/WEB/IOS)، name، fcmToken، refreshTokenHash/
refreshTokenFamily/refreshTokenExpiresAt (پنهان؛ برای چرخش و تشخیصِ replay)،
lastSeenAt، created/updated. یکتا روی { userId, uid }؛ ایندکس روی
fcmToken. Device حذفِ نرم ندارد؛ حذفِ دستگاه فیزیکی است.
SyncConflict
نسخهٔ بازندهٔ هر تعارضِ LWW اینجا ذخیره میشود تا کاربر بعداً آن را restore یا
dismiss کند. فیلدها: id، userId، entityType (EVENT/CALENDAR/TASK/
REMINDER)، entityUid، loserSnapshot (JSON)، winnerVersion (BigInt)،
resolvedAt، resolution، created — تنها created؛ updated/deleted ندارد.
ایندکس: { userId, resolvedAt }. برای مدلِ تعارض به همگامسازی مراجعه کنید.
PersianHoliday
id، name، persianName (مثل «عید نوروز»)، gregorianDate (منبعِ حقیقت)،
persianDate (YYYY/MM/DD)، isOfficial، isRecurring، description، category
(مذهبی/ملی/محلی/تعطیلات، پیشفرض ملی)، isActive،
created/updated/deleted.
ایندکسها: { gregorianDate, isActive }، { persianDate, isActive }،
{ category, isActive }، { isRecurring, isActive }،
{ gregorianDate, category, isActive }.
CalendarShare
id، calendarId، sharedWithUserId، sharedByUserId، permission
(READ/WRITE/ADMIN، پیشفرض READ)، acceptanceStatus
(PENDING/ACCEPTED/DECLINED، پیشفرض PENDING)، invitationMessage،
expiresAt، isActive، created/updated/deleted.
ایندکسها: { calendarId, sharedWithUserId } (یکتا)؛
{ sharedWithUserId, acceptanceStatus, isActive }، { calendarId, isActive }،
{ sharedByUserId, isActive }.
اصولِ طراحی
- تاریخهای میلادی منبعِ حقیقتاند — تاریخهای جلالی فقط برای نمایش محاسبه میشوند.
- معماری چندتقویمی — هر کاربر چند تقویم با نوعِ جلالی/میلادی دارد.
- حذفِ نرم —
deletedبهعنوان tombstone روی موجودیتهای همگامشونده کار میکند.
دستورهای Prisma (سریع)
npm run db:generate --workspace taghvimam-backend # بازتولید Prisma client
npm run db:migrate --workspace taghvimam-backend # اعمال migrations
npm run db:studio --workspace taghvimam-backend # Prisma Studio
npx prisma migrate deploy # فقط محیط تولید