feat(monorepo): migrate to typescript monorepo
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\Aggregator\Presentation\Console;
|
||||
|
||||
use Basango\Aggregator\Application\UseCase\Command\CreateSource;
|
||||
use Basango\Aggregator\Domain\Model\ValueObject\Scoring\Bias;
|
||||
use Basango\Aggregator\Domain\Model\ValueObject\Scoring\Credibility;
|
||||
use Basango\Aggregator\Domain\Model\ValueObject\Scoring\Reliability;
|
||||
use Basango\Aggregator\Domain\Model\ValueObject\Scoring\Transparency;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandBus;
|
||||
use Basango\SharedKernel\Presentation\Console\AskArgumentFeature;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:create-source',
|
||||
description: 'add a new data source'
|
||||
)]
|
||||
class CreateSourceConsole extends Command
|
||||
{
|
||||
use AskArgumentFeature;
|
||||
|
||||
private SymfonyStyle $io;
|
||||
|
||||
public function __construct(
|
||||
private readonly CommandBus $commandBus
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('source', InputArgument::REQUIRED, 'the website source to crawle');
|
||||
$this->addArgument('displayName', InputArgument::OPTIONAL, 'the display name of the source');
|
||||
$this->addArgument('description', InputArgument::OPTIONAL, 'the description of the source');
|
||||
$this->addOption('bias', 'b', InputArgument::OPTIONAL, 'bias of the source', Bias::NEUTRAL->value);
|
||||
$this->addOption('reliability', 'r', InputArgument::OPTIONAL, 'reliability of the source', Reliability::AVERAGE->value);
|
||||
$this->addOption('transparency', 't', InputArgument::OPTIONAL, 'transparency of the source', Transparency::MEDIUM->value);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
$this->io->title('Create a new data source');
|
||||
|
||||
$this->askArgument($input, 'source');
|
||||
$this->askArgument($input, 'displayName');
|
||||
$this->askOption($input, 'bias');
|
||||
$this->askOption($input, 'reliability');
|
||||
$this->askOption($input, 'transparency');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if (! $this->io->confirm('Do you want to continue?', false)) {
|
||||
$this->io->warning('Process aborted');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
/** @var string $source */
|
||||
$source = $input->getArgument('source');
|
||||
|
||||
/** @var string|null $displayName */
|
||||
$displayName = $input->getArgument('displayName');
|
||||
|
||||
/** @var string|null $description */
|
||||
$description = $input->getArgument('description');
|
||||
|
||||
$credibility = new Credibility(
|
||||
bias: Bias::from($input->getOption('bias')),
|
||||
reliability: Reliability::from($input->getOption('reliability')),
|
||||
transparency: Transparency::from($input->getOption('transparency')),
|
||||
);
|
||||
|
||||
$this->commandBus->handle(new CreateSource($source, $credibility, $displayName, $description));
|
||||
|
||||
$this->io->success('Source add successfully');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\Aggregator\Presentation\Console;
|
||||
|
||||
use Basango\Aggregator\Application\UseCase\Command\DeleteArticles;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandBus;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:delete-articles',
|
||||
description: 'remove all articles from the database by source',
|
||||
)]
|
||||
class DeleteArticlesConsole extends Command
|
||||
{
|
||||
private SymfonyStyle $io;
|
||||
|
||||
public function __construct(
|
||||
private readonly CommandBus $commandBus
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('source', InputArgument::REQUIRED, 'the website source to crawle');
|
||||
$this->addOption('category', null, InputOption::VALUE_OPTIONAL, 'the category to crawle');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
/** @var string $source */
|
||||
$source = $input->getArgument('source');
|
||||
|
||||
/** @var string|null $category */
|
||||
$category = $input->getOption('category');
|
||||
|
||||
if (
|
||||
$this->io->confirm('Delete all articles ?', false) &&
|
||||
$this->io->confirm('Are you sure ?', false)
|
||||
) {
|
||||
|
||||
$confirmation = $this->io->askQuestion(new Question('Specify the source to confirm : '));
|
||||
if ($confirmation === $source) {
|
||||
/** @var int $count */
|
||||
$count = $this->commandBus->handle(new DeleteArticles($source, $category));
|
||||
$this->io->success(sprintf('%d articles from %s removed', $count, $source));
|
||||
} else {
|
||||
$this->io->warning('Source does not match, aborting !');
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\Aggregator\Presentation\Console;
|
||||
|
||||
use Basango\Aggregator\Application\UseCase\Command\ExportArticles;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandBus;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\DateRange;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:export-articles',
|
||||
description: 'export crawled news website',
|
||||
)]
|
||||
final class ExportArticlesConsole extends Command
|
||||
{
|
||||
private SymfonyStyle $io;
|
||||
|
||||
public function __construct(
|
||||
private readonly CommandBus $commandBus
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('source', InputArgument::OPTIONAL, 'the website source to crawle');
|
||||
$this->addOption('date', null, InputOption::VALUE_OPTIONAL, 'Date interval to crawle', null);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
/** @var string|null $source */
|
||||
$source = $input->getArgument('source');
|
||||
|
||||
/** @var string|null $date */
|
||||
$date = $input->getOption('date');
|
||||
|
||||
$confirmation = $this->io->confirm('This can take a while, would like to continue ?', false);
|
||||
if ($confirmation) {
|
||||
$this->commandBus->handle(new ExportArticles(
|
||||
source: $source,
|
||||
date: $date !== null ? DateRange::from($date) : null
|
||||
));
|
||||
}
|
||||
|
||||
$this->io->success('articles exported successfully');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\Aggregator\Presentation\Console;
|
||||
|
||||
use Basango\Aggregator\Application\ReadModel\SourceStatistics;
|
||||
use Basango\Aggregator\Application\ReadModel\SourceStatisticsList;
|
||||
use Basango\Aggregator\Application\UseCase\Query\GetSourceStatisticsList;
|
||||
use Basango\SharedKernel\Application\Messaging\QueryBus;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:stats',
|
||||
description: 'show stats about the articles in the database',
|
||||
)]
|
||||
class GetSourceStatisticsListConsole extends Command
|
||||
{
|
||||
private SymfonyStyle $io;
|
||||
|
||||
public function __construct(
|
||||
private readonly QueryBus $queryBus
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
/** @var SourceStatisticsList $stats */
|
||||
$stats = $this->queryBus->handle(new GetSourceStatisticsList());
|
||||
|
||||
$stopWatch = new Stopwatch(true);
|
||||
$stopWatch->start('app:stats');
|
||||
|
||||
$this->io->table(
|
||||
['Source', 'Articles', 'Metadata', 'CrawledAt'],
|
||||
array_map(
|
||||
fn (SourceStatistics $source): array => [
|
||||
$source->name,
|
||||
number_format($source->articlesCount, decimal_separator: '.', thousands_separator: ','),
|
||||
number_format($source->metadataAvailable, decimal_separator: '.', thousands_separator: ','),
|
||||
$source->crawledAt?->format('Y-m-d H:i:s') ?? 'Never',
|
||||
],
|
||||
$stats->items
|
||||
)
|
||||
);
|
||||
|
||||
$this->io->text((string) $stopWatch->stop('app:stats'));
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\Aggregator\Presentation\Web\Controller;
|
||||
|
||||
use Basango\Aggregator\Application\UseCase\Command\CreateArticle;
|
||||
use Basango\Aggregator\Domain\Model\ValueObject\Link;
|
||||
use Basango\Aggregator\Presentation\WriteModel\AddArticleModel;
|
||||
use Basango\SharedKernel\Presentation\Web\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Requirement\Requirement;
|
||||
|
||||
/**
|
||||
* Class AddArticleController.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class AddArticleController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(env: 'BASANGO_CRAWLER_TOKEN')] private readonly string $token
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/api/aggregator/articles',
|
||||
name: 'aggregator_add_article',
|
||||
requirements: [
|
||||
'token' => Requirement::ASCII_SLUG,
|
||||
],
|
||||
methods: ['POST']
|
||||
)]
|
||||
public function __invoke(
|
||||
#[MapQueryParameter] string $token,
|
||||
#[MapRequestPayload] AddArticleModel $model
|
||||
): JsonResponse {
|
||||
if ($token !== $this->token) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
$this->handleCommand(new CreateArticle(
|
||||
$model->title,
|
||||
Link::from($model->link),
|
||||
$model->categories,
|
||||
$model->body,
|
||||
$model->source,
|
||||
$model->timestamp,
|
||||
$model->metadata,
|
||||
$model->tokenStatistics
|
||||
));
|
||||
|
||||
return new JsonResponse(status: Response::HTTP_CREATED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\Aggregator\Presentation\WriteModel;
|
||||
|
||||
use Basango\Aggregator\Domain\Model\ValueObject\OpenGraph;
|
||||
use Basango\Aggregator\Domain\Model\ValueObject\TokenStatistics;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Class AddArticleModel.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class AddArticleModel
|
||||
{
|
||||
#[Assert\NotBlank]
|
||||
public string $title;
|
||||
|
||||
#[Assert\NotBlank]
|
||||
public string $link;
|
||||
|
||||
#[Assert\NotBlank]
|
||||
public string $body;
|
||||
|
||||
#[Assert\NotBlank]
|
||||
public string $source;
|
||||
|
||||
#[Assert\NotBlank]
|
||||
public int $timestamp;
|
||||
|
||||
public array $categories = [];
|
||||
|
||||
public ?OpenGraph $metadata = null;
|
||||
|
||||
public ?TokenStatistics $tokenStatistics = null;
|
||||
}
|
||||
Reference in New Issue
Block a user