Skip to main content

پایگاه داده

دیتابیس 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 — هر کدام دو ستونِ زیر را دارند:

ستوننوعنقش
uidString (یکتا، پیش‌فرض gen_random_uuid())شناسهٔ پایدار و مستقل از DB؛ کلیدِ همگام‌سازی بین دستگاه‌ها
versionBigIntشمارهٔ نسخهٔ مونوتونِ رکورد؛ مبنای مقایسه در همگام‌سازی (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 گسترده‌تر است؛ جزئیات در استاندارد زمان‌ها.)

ستون‌های legacy

چند ستون از پروتکلِ قدیمیِ همگام‌سازی در اسکیما مانده‌اند ولی موتورِ 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/ADMINisActive، isPhoneVerified، otpCode/otpExpiresAt (پنهان)، lastSyncTime، googleCalendarId، deviceId، fcmToken، refreshToken/refreshTokenExpires (پنهان؛ مسیرِ قدیمیِ تک‌توکنی)، notificationSettings (syncNotifications/eventReminders/conflictAlertscreated/updated/deleted.

ایندکس‌ها: email (یکتا)، phone (یکتا).

User ستون‌های uid/version همگام‌سازی را ندارد؛ همگام‌سازی روی Calendar/Event/Task/Reminder انجام می‌شود.

Calendar

id، userId، uid، version، name، description، color (عددِ صحیحِ ARGB، پیش‌فرض -123164116emoji، isDefault (پرچمِ تقویمِ پیش‌فرض)، isVisible، calendarType (JALALI/GREGORIAN، پیش‌فرض JALALIisActive، 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/GREGORIANsource (LOCAL/GOOGLE/ANDROID_PROVIDER/SERVERsourceId، syncStatus (SYNCED/PENDING/CONFLICTcreated/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/URGENTdueDate، 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/WEEKisEnabled، 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/IOSname، fcmToken، refreshTokenHash/ refreshTokenFamily/refreshTokenExpiresAt (پنهان؛ برای چرخش و تشخیصِ replay)، lastSeenAt، created/updated. یکتا روی { userId, uid }؛ ایندکس روی fcmToken. Device حذفِ نرم ندارد؛ حذفِ دستگاه فیزیکی است.

SyncConflict

نسخهٔ بازندهٔ هر تعارضِ LWW این‌جا ذخیره می‌شود تا کاربر بعداً آن را restore یا dismiss کند. فیلدها: id، userId، entityType (EVENT/CALENDAR/TASK/ REMINDERentityUid، loserSnapshot (JSON)، winnerVersion (BigInt)، resolvedAt، resolution، created — تنها created؛ updated/deleted ندارد. ایندکس: { userId, resolvedAt }. برای مدلِ تعارض به همگام‌سازی مراجعه کنید.

PersianHoliday

id، name، persianName (مثل «عید نوروز»)، gregorianDate (منبعِ حقیقت)، persianDate (YYYY/MM/DDisOfficial، 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، پیش‌فرض READacceptanceStatus (PENDING/ACCEPTED/DECLINED، پیش‌فرض PENDINGinvitationMessage، 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 # فقط محیط تولید