Skip to content

Conversation

@tuj
Copy link
Contributor

@tuj tuj commented Oct 8, 2025

Link to ticket

https://leantime.itkdev.dk/TimeTable/TimeTable?showTicketModal=5538#/tickets/showTicket/5538

Description

  • Changes leantime data provider to use the APIData Plugin: https://github.com/ITK-Leantime/data-api instead of the built in API.
  • Every fetch and update action is a job that can be executed as either synchronous or asynchronous.
  • Removed DAMA testing bundle.

TODOs

  • Manual start of project sync from UI.
  • Tests.
  • Use the deleted endpoint of APIData Plugin for handling deleted entities.
  • Call monitoringUrl when updating jobs are started.
  • Use ThrottlingHttpClient?

Checklist

  • My code is covered by test cases.
  • My code passes our test (all our tests).
  • My code passes our static analysis suite.
  • My code passes our continuous integration process.

@tuj tuj changed the title Feature/new leantime api Leantime data provider changed to use APIData plugin instead of built in API. Oct 8, 2025
protected function configure(): void
{
$this->addOption("job", 'j', InputOption::VALUE_NONE, "Use async job handling");
$this->addOption("modified", "m", InputOption::VALUE_NONE, "Only items modified since last update");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modified implies Datetime as value, should maybe be only-modifed tom imply Bool


$io->info('Processing projects');
$jobHandling = $input->getOption("job");
$modified = $input->getOption("modified");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, modified implies Datetime as value, should maybe be only-modifed tom imply Bool

trait FetchDateTrait
{
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?\DateTimeInterface $fetchTime = null;
Copy link
Contributor

@turegjorup turegjorup Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can this be null? For data that is sync'ed from a data provider we should always have a date.

For legacy data we can either set 1970-01-01 00:00 or NOW() as default value in the db migration. For new entities we can set new DatetomeImmutable() as default in the constructor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not allowing null values will also simplify some of the queries in the repository methods.

$message->modified,
);
} catch (\Exception $e) {
throw new UnrecoverableMessageHandlingException($e->getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question - should we also do $this->logger->error(..)?

I think throwing UnrecoverableMessageHandlingException will put the message in the failed queue, so the error is visible there. But even so, if somebody just logs at the logs when debugging ad don't see errors it might be confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added


trait FetchDateTrait
{
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the queries we do for oldest fetch time we should consider an index on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed approach to not use fetchDate to find modifiedAfter.

use Symfony\Contracts\HttpClient\HttpClientInterface;

class JiraApiService implements DataProviderServiceInterface
class JiraApiService implements DataProviderInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class JiraApiService implements DataProviderInterface
#[\Deprecated(message: "The Jira data provider is deprcated and will be removed", since: "x.y")]
class JiraApiService implements DataProviderInterface

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remove the Service as it does not need to exist with the changes in this PR and it does not work.


private function linkToTicket(string $ticketId, DataProvider $dataProvider): string
{
return $dataProvider->getUrl() . "/errorpage/#/tickets/showTicket/" . $ticketId;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically only valid for Leantime, so does not belong in a generic data provider class. But I don't know how important we consider the support for multiple providers to be going forward?


use App\Enum\IssueStatusEnum;

class UpsertIssueData
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I Don't see the point of having Upsert in the class name for any of these models, but that is semantics and subjective

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will leave it as it ensures that the name does not collide with other IssueData class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about ProviderIssueDataas we talked about?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot about that talk :D

return $qb->getQuery()->getSingleColumnResult();
}

public function getOldestFetchTime(DataProvider $dataProvider, ?array $projectTrackerProjectIds = null): ?\DateTimeInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect this to be getNewestFetchTime? If we always fetch from the oldest and forward will we not always sync everything. Or do I have it the wrong way round?

@tuj tuj self-assigned this Oct 10, 2025
@tuj tuj added the enhancement New feature or request label Oct 10, 2025
@codecov-commenter
Copy link

Codecov Report

❌ Patch coverage is 58.65052% with 239 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (develop@fdecc1a). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/Service/DataProviderService.php 71.65% 53 Missing ⚠️
src/Service/LeantimeApiService.php 77.36% 43 Missing ⚠️
src/Command/SyncCommand.php 0.00% 34 Missing ⚠️
src/MessageHandler/LeantimeUpdateHandler.php 0.00% 15 Missing ⚠️
src/Controller/ProjectController.php 0.00% 12 Missing ⚠️
src/Controller/PlanningController.php 0.00% 11 Missing ⚠️
src/MessageHandler/LeantimeDeleteHandler.php 0.00% 11 Missing ⚠️
src/Command/SyncDeletedCommand.php 0.00% 10 Missing ⚠️
src/Command/SyncModifiedCommand.php 0.00% 10 Missing ⚠️
src/Repository/ProjectRepository.php 0.00% 9 Missing ⚠️
... and 12 more
Additional details and impacted files
@@            Coverage Diff             @@
##             develop     #242   +/-   ##
==========================================
  Coverage           ?   17.38%           
  Complexity         ?     1662           
==========================================
  Files              ?      202           
  Lines              ?     6880           
  Branches           ?        0           
==========================================
  Hits               ?     1196           
  Misses             ?     5684           
  Partials           ?        0           
Flag Coverage Δ
17.38% <58.65%> (?)
unittests 17.38% <58.65%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tuj tuj requested a review from jeppekroghitk November 4, 2025 11:28
@tuj tuj marked this pull request as ready for review November 4, 2025 11:49
Copy link
Contributor

@turegjorup turegjorup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider if we can lower this in docker-compose.server.override.yml

    phpfpm:
        environment:
            - PHP_MEMORY_LIMIT=512M

name: 'app:data-providers:sync-deleted',
description: 'Sync Data Provider deleted data, that has been deleted within the last hour, as jobs. Scheduled to run every 15 minutes.',
)]
#[AsPeriodicTask(frequency: '15 minutes')]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I havn't tried it, but it should be possible to do something like #[SomeAttribute('%env(MY_ENV_VAR)%')], so

Suggested change
#[AsPeriodicTask(frequency: '15 minutes')]
#[AsPeriodicTask(frequency: '%env(SYNC_FREQUENCY)%')]

And then set SYNC_FREQUENCY='15 minutes' in .env so it's configurable per installation

{
// Look at entries modified within the last hour.
$deletedAfter = new \DateTime();
$deletedAfter->sub(new \DateInterval('P1D'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1Dis one DAY, but comment and description says one HOUR. I'm fine with either, as long as code and comment/description match.

Nice to have: P1D as default, but possible to give another value as command argument.

name: 'app:data-providers:sync-modified',
description: 'Sync Data Provider data, that has been modified within the last hour, as jobs. Scheduled to run every 15 minutes.',
)]
#[AsPeriodicTask(frequency: '15 minutes')]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above. If we can move 15 minutes to .env we should

Comment on lines 10 to 37
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?\DateTimeInterface $fetchDate = null;

#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?\DateTimeInterface $sourceModifiedDate = null;

public function getFetchDate(): ?\DateTimeInterface
{
return $this->fetchDate;
}

public function setFetchDate(?\DateTimeInterface $fetchDate): void
{
$this->fetchDate = $fetchDate;
}

public function getSourceModifiedDate(): ?\DateTimeInterface
{
return $this->sourceModifiedDate;
}

public function setSourceModifiedDate(?\DateTimeInterface $sourceModifiedDate): void
{
$this->sourceModifiedDate = $sourceModifiedDate;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm torn between wanting to use an interface and wanting to use DateTimeImmutable (which has no interface). I would do this, but it's only a suggestion. Pick the one you prefer.

Suggested change
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?\DateTimeInterface $fetchDate = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?\DateTimeInterface $sourceModifiedDate = null;
public function getFetchDate(): ?\DateTimeInterface
{
return $this->fetchDate;
}
public function setFetchDate(?\DateTimeInterface $fetchDate): void
{
$this->fetchDate = $fetchDate;
}
public function getSourceModifiedDate(): ?\DateTimeInterface
{
return $this->sourceModifiedDate;
}
public function setSourceModifiedDate(?\DateTimeInterface $sourceModifiedDate): void
{
$this->sourceModifiedDate = $sourceModifiedDate;
}
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $fetchDate = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $sourceModifiedDate = null;
public function getFetchDate(): ?\DateTimeImmutable
{
return $this->fetchDate;
}
public function setFetchDate(?\DateTimeInterface $fetchDate): void
{
if (!$fetchDate instanceof \DateTimeImmutable) {
$fetchDate = \DateTimeImmutable::createFromInterface($fetchDate);
}
$this->fetchDate = $fetchDate;
}
public function getSourceModifiedDate(): ?\DateTimeImmutable
{
return $this->sourceModifiedDate;
}
public function setSourceModifiedDate(?\DateTimeImmutable $sourceModifiedDate): void
{
$this->sourceModifiedDate = $sourceModifiedDate;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I will stick with DateTimeInterface, since this is the approach we use in the other parts of the codebase.

}

return new JsonResponse(['issuesSynced' => $issuesSynced], 200);
return new JsonResponse(['issuesSynced' => 0], 200);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to return this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really... will remove it :)

@tuj
Copy link
Contributor Author

tuj commented Nov 5, 2025

Consider if we can lower this in docker-compose.server.override.yml

    phpfpm:
        environment:
            - PHP_MEMORY_LIMIT=512M

I have removed the override value. We will have to see it we need more than 128M

@jeppekroghitk jeppekroghitk force-pushed the feature/new-leantime-api branch from 641257b to 22bbd84 Compare December 4, 2025 13:22
@jeppekroghitk jeppekroghitk force-pushed the feature/new-leantime-api branch from 4145988 to 013fa32 Compare December 11, 2025 08:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants