# Sahib Data Model

<span style="color: rgb(34, 34, 34); font-family: -apple-system, 'system-ui', 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 3.425em; font-weight: 400;">Saheb Mobile App - Database Schema</span>

---

## 1. Entity Schema (Main Tables - No Translation Table)

**All main tables with their columns. Translation relationships shown in diagram 2.**

<div drawio-diagram="411"><img src="https://italiano.embouzkoura.space/uploads/images/drawio/2026-01/drawing-1-1769861815.png" alt=""/></div>

---

## 2. Translation Schema (Translation Relationships Only)

**Shows how the global `translations` table relates to content tables.**

<div drawio-diagram="412"><img src="https://italiano.embouzkoura.space/uploads/images/drawio/2026-01/drawing-1-1769861859.png" alt=""/></div>

---

## Translation Strategy

- **Location tables** (`regions`, `countries`, `subregions`): Use JSON `translations` column
- **Content tables** (`acts`, `sermons`, `resources`, `qas`, `adhkars`, `sermon_paragraphs`): Use global `translations` table
- **Fallback chain**: Requested language → Record's `default_language` → Global default (`en-US`)

---

## Drizzle Query Examples

### Basic Translation Query

```typescript
import { db } from '@/src/db';
import { acts, translations } from '@/src/db/schemas';
import { eq, and, sql, inArray } from 'drizzle-orm';

// Get act title in specific language
async function getActTitle(actId: string, languageCode: string) {
  const result = await db
    .select({
      id: acts.id,
      title: translations.translatedValue,
    })
    .from(acts)
    .innerJoin(translations, and(
      eq(translations.entityType, 'acts'),
      eq(translations.entityId, acts.id),
      eq(translations.fieldName, 'title'),
      eq(translations.languageCode, languageCode)
    ))
    .where(eq(acts.id, actId))
    .limit(1);
  
  return result[0]?.title || null;
}

```

### Helper Function: Reusable Translation Getter

```typescript
// Generic helper for any entity type
async function getTranslatedValue(
  entityType: 'acts' | 'sermons' | 'resources' | 'qas' | 'adhkars' | 'sermon_paragraphs',
  entityId: string,
  fieldName: string,
  requestedLanguage: string,
  recordDefaultLanguage: string,
  globalDefaultLanguage: string = 'en-US'
): Promise<string | null> {
  // Try requested language first
  let translation = await db
    .select()
    .from(translations)
    .where(and(
      eq(translations.entityType, entityType),
      eq(translations.entityId, entityId),
      eq(translations.fieldName, fieldName),
      eq(translations.languageCode, requestedLanguage)
    ))
    .limit(1);
  
  if (translation[0]) return translation[0].translatedValue;
  
  // Fallback to record's default language
  if (recordDefaultLanguage && recordDefaultLanguage !== requestedLanguage) {
    translation = await db
      .select()
      .from(translations)
      .where(and(
        eq(translations.entityType, entityType),
        eq(translations.entityId, entityId),
        eq(translations.fieldName, fieldName),
        eq(translations.languageCode, recordDefaultLanguage)
      ))
      .limit(1);
    
    if (translation[0]) return translation[0].translatedValue;
  }
  
  return null;
}

// Usage example
const act = await db.select().from(acts).where(eq(acts.id, 'act-1')).limit(1);
const title = await getTranslatedValue(
  'acts',
  'act-1',
  'title',
  'fr-FR', // User's current language
  act[0].defaultLanguage, // Record's default (e.g., 'ar-MA')
  'en-US' // Global default
);

```

---

## Design Notes

- **Pure Translation Approach**: All languages stored in `translations` table (including English)
- **Per-Record Fallback**: Each record has `default_language` for fallback when requested translation missing
- **Location Tables**: Use JSON `translations` column (prepopulated data)
- **Content Tables**: Use global `translations` table (dynamic content)

---

## 3. Scheduling &amp; Event Bus Schema

<div drawio-diagram="413"><img src="https://italiano.embouzkoura.space/uploads/images/drawio/2026-01/drawing-1-1769862042.png" alt=""/></div>

<section class="markdown-section chat-fade-in" data-markdown-raw="
## Scheduling Design Pattern Explanation" data-section-index="0" id="bkmrk-scheduling-design-pa">## Scheduling Design Pattern Explanation

</section><section class="markdown-section chat-fade-in" data-markdown-raw="

```typescript
schedules {
  target_type: 'act' | 'notification' | 'reminder' | 'other'  // What to schedule
  target_id: integer                                           // ID of the target
  recurrence_type: 'daily' | 'weekly' | 'monthly' | 'yearly'  // How often
  interval: integer                                            // Every N periods
  start_at: integer                                            // When to start
  timezone: string                                             // Timezone context
  is_active: boolean                                           // Enable/disable
}
```" data-section-index="6" id="bkmrk-the-scheduling-syste"><div class="markdown-code-outer-container markdown-block-code"><div><div class="composer-code-block-container composer-message-codeblock display-codeblock"><div><div class="composer-code-block-content" data-mode-id="worktree-typescript"><div data-keybinding-context="2343" data-mode-id="worktree-typescript"><div class="monaco-editor no-user-select mac  showUnused showDeprecated vs-dark" data-uri="display-codeblock://zhxvpmh19n" role="code"><div class="overflow-guard" data-mprt="3"><div class="monaco-editor-background textAreaCover"><section class="markdown-section chat-fade-in" data-markdown-raw="
The scheduling system uses a polymorphic, flexible pattern that supports multiple recurrence types and targets." data-section-index="2" id="bkmrk-the-scheduling-syste-1">The scheduling system uses a polymorphic, flexible pattern that supports multiple recurrence types and targets.</section><section class="markdown-section chat-fade-in" data-markdown-raw="

### Core Design: Polymorphic Scheduling" data-section-index="3" id="bkmrk-core-design%3A-polymor">### Core Design: Polymorphic Scheduling

</section><section class="markdown-section chat-fade-in" data-markdown-raw="
The `schedules` table is the central entity that can schedule any type of target:" data-section-index="5" id="bkmrk-the%C2%A0schedules%C2%A0table-">The <span class="markdown-inline-code leading-[1.4]">schedules</span> table is the central entity that can schedule any type of target:</section><section class="markdown-section chat-fade-in" data-markdown-raw="

```typescript
schedules {
  target_type: 'act' | 'notification' | 'reminder' | 'other'  // What to schedule
  target_id: integer                                           // ID of the target
  recurrence_type: 'daily' | 'weekly' | 'monthly' | 'yearly'  // How often
  interval: integer                                            // Every N periods
  start_at: integer                                            // When to start
  timezone: string                                             // Timezone context
  is_active: boolean                                           // Enable/disable
}
```" data-section-index="6" id="bkmrk--14"><div class="markdown-code-outer-container markdown-block-code"><div><div class="composer-code-block-container composer-message-codeblock display-codeblock"><div><div class="composer-code-block-content" data-mode-id="worktree-typescript"><div data-keybinding-context="2343" data-mode-id="worktree-typescript"><div class="monaco-editor no-user-select mac  showUnused showDeprecated vs-dark" data-uri="display-codeblock://zhxvpmh19n" role="code">  
</div></div></div></div></div></div></div></section></div><div aria-hidden="true" class="margin" role="presentation"><div class="glyph-margin">  
</div></div></div></div></div></div></div></div></div></div>```typescript
schedules {
  target_type: 'act' | 'notification' | 'reminder' | 'other'  // What to schedule
  target_id: integer                                           // ID of the target
  recurrence_type: 'daily' | 'weekly' | 'monthly' | 'yearly'  // How often
  interval: integer                                            // Every N periods
  start_at: integer                                            // When to start
  timezone: string                                             // Timezone context
  is_active: boolean                                           // Enable/disable
}
```

<div class="markdown-code-outer-container markdown-block-code"><div><div class="composer-code-block-container composer-message-codeblock display-codeblock"><div><div class="composer-code-block-content" data-mode-id="worktree-typescript"><div data-keybinding-context="2343" data-mode-id="worktree-typescript"><div class="monaco-editor no-user-select mac  showUnused showDeprecated vs-dark" data-uri="display-codeblock://zhxvpmh19n" role="code"><div class="overflow-guard" data-mprt="3"><div aria-hidden="true" class="margin" role="presentation"><div class="glyph-margin"><section class="markdown-section chat-fade-in" data-markdown-raw="

### Pattern: Strategy Pattern with Optional Configuration Tables" data-section-index="7" id="bkmrk-pattern%3A-strategy-pa">### Pattern: Strategy Pattern with Optional Configuration Tables

</section><section class="markdown-section chat-fade-in" data-markdown-raw="
The system uses a strategy pattern where:
- The base `schedules` table defines the general schedule
- Optional configuration tables provide recurrence-specific details" data-section-index="9" id="bkmrk-the-system-uses-a-st">The system uses a strategy pattern where: - The base <span class="markdown-inline-code leading-[1.4]">schedules</span> table defines the general schedule

- Optional configuration tables provide recurrence-specific details

</section><section class="markdown-section chat-fade-in" data-markdown-raw="

### 1. Base Schedule (`schedules` table)" data-section-index="10" id="bkmrk-1.-base-schedule-%28sc">### 1. Base Schedule (<span class="markdown-inline-code leading-[1.4]">schedules</span> table)

</section><section class="markdown-section chat-fade-in" data-markdown-raw="
Defines:
- What to schedule (`target_type` + `target_id`)
- Recurrence pattern (`recurrence_type` + `interval`)
- When to start (`start_at`)
- Timezone context
- Active status" data-section-index="12" id="bkmrk-defines%3A-what-to-sch">Defines: - What to schedule (<span class="markdown-inline-code leading-[1.4]">target\_type</span> + <span class="markdown-inline-code leading-[1.4]">target\_id</span>)

- Recurrence pattern (<span class="markdown-inline-code leading-[1.4]">recurrence\_type</span> + <span class="markdown-inline-code leading-[1.4]">interval</span>)

- When to start (<span class="markdown-inline-code leading-[1.4]">start\_at</span>)

- Timezone context

- Active status

</section><section class="markdown-section chat-fade-in" data-markdown-raw="

### 2. Recurrence-Specific Configuration Tables" data-section-index="13" id="bkmrk-2.%C2%A0recurrence-specif">### 2. Recurrence-Specific Configuration Tables

</section><section class="markdown-section chat-fade-in" data-markdown-raw="
Each recurrence type has its own configuration table:" data-section-index="15" id="bkmrk-each-recurrence-type">Each recurrence type has its own configuration table:</section><section class="markdown-section chat-fade-in" data-markdown-raw="

#### Weekly Schedules (`schedule_weekdays`)
```typescript
// Example: Schedule on Monday, Wednesday, Friday
schedule_weekdays: [
  { schedule_id: 1, weekday: 1 }, // Monday
  { schedule_id: 1, weekday: 3 }, // Wednesday
  { schedule_id: 1, weekday: 5 }, // Friday
]
```
- `weekday`: 0 (Sunday) to 6 (Saturday)
- Multiple weekdays per schedule (many-to-many)" data-section-index="16" id="bkmrk-weekly-schedules-%28sc">#### Weekly Schedules (<span class="markdown-inline-code leading-[1.4]">schedule\_weekdays</span>)

</section></div></div></div></div></div></div></div></div></div></div>```typescript
// Example: Schedule on Monday, Wednesday, Friday
schedule_weekdays: [
  { schedule_id: 1, weekday: 1 }, // Monday
  { schedule_id: 1, weekday: 3 }, // Wednesday
  { schedule_id: 1, weekday: 5 }, // Friday
]
```

<div class="markdown-code-outer-container markdown-block-code"><div><div class="composer-code-block-container composer-message-codeblock display-codeblock"><div><div class="composer-code-block-content" data-mode-id="worktree-typescript"><div data-keybinding-context="2343" data-mode-id="worktree-typescript"><div class="monaco-editor no-user-select mac  showUnused showDeprecated vs-dark" data-uri="display-codeblock://zhxvpmh19n" role="code"><div class="overflow-guard" data-mprt="3"><div aria-hidden="true" class="margin" role="presentation"><div class="glyph-margin"><section class="markdown-section chat-fade-in" data-markdown-raw="

#### Monthly Schedules (`schedule_monthdays`)
```typescript
// Example: Schedule on 1st, 15th, and 30th of each month
schedule_monthdays: [
  { schedule_id: 2, day: 1 },
  { schedule_id: 2, day: 15 },
  { schedule_id: 2, day: 30 },
]
```
- `day`: 1-31
- Multiple days per schedule" data-section-index="17" id="bkmrk-day%3A%C2%A01-31-multiple%C2%A0d">- <span class="markdown-inline-code leading-[1.4]">day</span>: 1-31

- Multiple days per schedule

</section><section class="markdown-section chat-fade-in" data-markdown-raw="

#### Yearly Schedules (`schedule_yearly`)
```typescript
// Example: Schedule on January 1st every year
schedule_yearly: {
  schedule_id: 3,
  month: 1,  // January
  day: 1
}
```
- One entry per schedule (one-to-one)
- `month`: 1-12, `day`: 1-31" data-section-index="18" id="bkmrk-yearly-schedules-%28sc">#### Yearly Schedules (<span class="markdown-inline-code leading-[1.4]">schedule\_yearly</span>)

```typescript
// Example: Schedule on January 1st every year
schedule_yearly: {
  schedule_id: 3,
  month: 1,  // January
  day: 1
}
```

<section class="markdown-section chat-fade-in" data-markdown-raw="

#### Execution Times (`schedule_times`)
```typescript
// Example: Schedule at 8:00 AM and 6:00 PM
schedule_times: [
  { schedule_id: 1, time_seconds: 28800 },  // 8:00 AM (8 * 60 * 60)
  { schedule_id: 1, time_seconds: 64800 },  // 6:00 PM (18 * 60 * 60)
]
```
- `time_seconds`: 0-86399 (seconds from midnight)
- Multiple times per schedule
- Works with any recurrence type" data-section-index="19" id="bkmrk-time_seconds%3A%C2%A00-8639">- <span class="markdown-inline-code leading-[1.4]">time\_seconds</span>: 0-86399 (seconds from midnight)

- Multiple times per schedule

- Works with any recurrence type

</section><section class="markdown-section chat-fade-in" data-markdown-raw="

### 3. Execution History (`schedule_runs`)" data-section-index="20" id="bkmrk-3.-execution-history">### 3. Execution History (<span class="markdown-inline-code leading-[1.4]">schedule\_runs</span>)

</section><section class="markdown-section chat-fade-in" data-markdown-raw="
Tracks execution:" data-section-index="22" id="bkmrk-tracks-execution%3A">Tracks execution:</section>```typescript
schedule_runs {
  schedule_id: integer
  scheduled_for: integer      // When it was supposed to run (timestamp)
  executed_at: integer        // When it actually ran (null if not yet)
  status: 'pending' | 'success' | 'failed' | 'skipped' | 'cancelled'
  error_message: string       // If failed
}
```

### Schedule Implementation

</section><section class="markdown-section chat-fade-in" data-markdown-raw="

### Design Pattern Benefits" data-section-index="24" id="bkmrk--15"></section>  
</div></div></div></div></div></div></div></div></div></div><div class="markdown-code-outer-container markdown-block-code"><div><div class="composer-code-block-container composer-message-codeblock display-codeblock"><div><div class="composer-code-block-content" data-mode-id="worktree-typescript"><div data-keybinding-context="2343" data-mode-id="worktree-typescript"><div class="monaco-editor no-user-select mac  showUnused showDeprecated vs-dark" data-uri="display-codeblock://zhxvpmh19n" role="code"><div class="overflow-guard" data-mprt="3"><div aria-hidden="true" class="margin" role="presentation"><div aria-hidden="true" class="margin-view-zones" role="presentation">  
</div><div aria-hidden="true" class="margin-view-overlays" role="presentation"><div><div class="current-line">  
</div></div></div></div></div></div></div></div></div></div></div></div>## Facade Method Signature

```typescript
interface ScheduleExecution {
  schedule: Schedule;
  executionTime: number; // Unix timestamp in seconds
  targetType: 'act' | 'notification' | 'reminder' | 'other';
  targetId: number;
}
/**
 * Get all schedules that need to be executed within a time range
 * 
 * @param rangeStart - Start of the time range (Unix timestamp in seconds)
 * @param rangeEnd - End of the time range (Unix timestamp in seconds)
 * @param timezone - Optional timezone for calculations (defaults to UTC)
 * @returns Array of schedule execution records with calculated execution times
 */
async function getSchedulesInRange(
  rangeStart: number,
  rangeEnd: number,
  timezone: string = 'UTC'
): Promise<ScheduleExecution[]>
```

#### Example : Get schedules for a specific time window (e.g., next 15 minutes)

```typescript
const now = Math.floor(Date.now() / 1000);
const in15Minutes = now + (15 * 60); // 15 minutes later

const schedules = await getSchedulesInRange(now, in15Minutes);

// Execute immediately if any schedules found
if (schedules.length > 0) {
  console.log(`Executing ${schedules.length} schedules now...`);
  for (const execution of schedules) {
    await executeSchedule(execution);
  }
}
```

#### Example : Database structure - Prayers with schedule\_id

Here's how prayers are linked to schedules:

```typescript
// Schedule for Isha prayer (daily at 8:15 PM)
const ishaSchedule = {
  id: 5,
  target_type: 'reminder',  // Prayers use 'reminder' type
  target_id: 1,  // ID of the prayer reminder/notification
  recurrence_type: 'daily',
  interval: 1,
  start_at: 1234567890,
  timezone: 'Africa/Casablanca',
  is_active: true,
};

// Schedule times: Isha at 8:15 PM (73500 seconds = 20:15:00)
const ishaTimes = [
  { schedule_id: 5, time_seconds: 73500 },
];

// Act with schedule (daily Quran reading at 8:00 PM)
const quranActSchedule = {
  id: 8,
  target_type: 'act',
  target_id: 15,  // Act ID
  recurrence_type: 'daily',
  interval: 1,
  start_at: 1234567890,
  timezone: 'Africa/Casablanca',
  is_active: true,
};

const quranTimes = [
  { schedule_id: 8, time_seconds: 72000 }, // 20:00:00
];

// Usage: Get schedules for next hour (7:30 PM - 8:30 PM)
const now = Math.floor(Date.now() / 1000);
const oneHourLater = now + 3600;

const schedules = await getSchedulesInRange(now, oneHourLater);

// Results:
// - Isha prayer (schedule_id: 5) at 8:15 PM
// - Quran act (act_id: 15) at 8:00 PM
```

<div class="markdown-code-outer-container markdown-block-code"><div><div class="composer-code-block-container composer-message-codeblock display-codeblock"><div><div class="composer-code-block-content" data-mode-id="worktree-typescript"><div data-keybinding-context="2343" data-mode-id="worktree-typescript"><div class="monaco-editor no-user-select mac  showUnused showDeprecated vs-dark" data-uri="display-codeblock://zhxvpmh19n" role="code"><div class="overflow-guard" data-mprt="3"><div aria-hidden="true" class="margin" role="presentation"><div aria-hidden="true" class="margin-view-overlays" role="presentation"><div><div class="current-line">  
</div></div><div>  
</div><div>  
</div><div>  
</div><div>  
</div><div>  
</div><div>  
</div><div>  
</div><div>  
</div></div><div class="glyph-margin-widgets">  
</div></div><div class="monaco-scrollable-element editor-scrollable vs-dark mac" data-mprt="6" role="presentation"><div class="lines-content monaco-editor-background"><div aria-hidden="true" class="view-overlays" role="presentation"><div>  
</div><div>  
</div><div>  
</div><div>  
</div><div>  
</div><div>  
</div></div></div></div></div></div></div></div></div></div></div></div></section>