*/ class DeckSyncMapper extends QBMapper { public function __construct(IDBConnection $db) { parent::__construct($db, 'nuqloud_scrum_deck_sync', DeckSync::class); } public function findForWorkspace(int $workspaceId): DeckSync { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->getTableName()) ->where($qb->expr()->eq('workspace_id', $qb->createNamedParameter($workspaceId, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->isNull('thread_id')); return $this->findEntity($qb); } public function findForThread(int $threadId): DeckSync { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->getTableName()) ->where($qb->expr()->eq('thread_id', $qb->createNamedParameter($threadId, IQueryBuilder::PARAM_INT))); return $this->findEntity($qb); } /** @return DeckSync[] */ public function findPending(int $limit = 100): array { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->getTableName()) ->where($qb->expr()->eq('sync_state', $qb->createNamedParameter('pending', IQueryBuilder::PARAM_STR))) ->orderBy('updated_at', 'ASC') ->setMaxResults(max(1, min(500, $limit))); return $this->findEntities($qb); } /** @param array $fields */ public function upsertSync(int $workspaceId, ?int $threadId, array $fields): DeckSync { $now = time(); try { $sync = $threadId === null ? $this->findForWorkspace($workspaceId) : $this->findForThread($threadId); } catch (DoesNotExistException) { $sync = new DeckSync(); $sync->setWorkspaceId($workspaceId); $sync->setThreadId($threadId); $sync->setSyncState('pending'); $sync->setCreatedAt($now); } $sync->setDeckBoardId($this->nullableInt($fields['deckBoardId'] ?? $sync->getDeckBoardId())); $sync->setDeckStackId($this->nullableInt($fields['deckStackId'] ?? $sync->getDeckStackId())); $sync->setDeckCardId($this->nullableInt($fields['deckCardId'] ?? $sync->getDeckCardId())); $sync->setLastSyncAt($this->nullableInt($fields['lastSyncAt'] ?? $sync->getLastSyncAt())); $sync->setSyncState($this->boundedString($fields['syncState'] ?? $sync->getSyncState(), 32, 'pending')); $sync->setSyncError($this->nullableString($fields['syncError'] ?? $sync->getSyncError())); $sync->setUpdatedAt($now); return $sync->getId() === null ? $this->insert($sync) : $this->update($sync); } private function nullableInt(mixed $value): ?int { if ($value === null || $value === '') { return null; } return (int)$value; } private function nullableString(mixed $value): ?string { $value = trim((string)$value); return $value === '' ? null : $value; } private function boundedString(mixed $value, int $maxLength, string $fallback): string { $value = trim((string)$value); if ($value === '') { $value = $fallback; } if (function_exists('mb_substr')) { return mb_substr($value, 0, $maxLength); } return substr($value, 0, $maxLength); } }