WIP work on youtrack rest api plugin.
parent
4e3477c4a0
commit
519c0b074a
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
** Install/Getting started
|
** Install/Getting started
|
||||||
|
|
||||||
Get ~.phar~ file from r, Run configuration wizzard (planned for version 1.0).
|
Get ~.phar~ file from r, Run configruration wizzard (planned for version 1.0).
|
||||||
|
|
||||||
Before version 1.0 is released you have to navigate into ~/app~ directory
|
Before version 1.0 is released you have to navigate into ~/app~ directory
|
||||||
and call command through ~.rprt.php~ file.
|
and call command through ~.rprt.php~ file.
|
||||||
|
@ -75,6 +75,11 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
|
||||||
5 days of development.
|
5 days of development.
|
||||||
- remove errors from reports
|
- remove errors from reports
|
||||||
|
|
||||||
|
* Support the work
|
||||||
|
|
||||||
|
If you find this Free Software useful you can consider donating to my
|
||||||
|
[[https://liberapay.com/tehnoklistir/][@tehno-klistir liberapay account.]]
|
||||||
|
|
||||||
* Development
|
* Development
|
||||||
|
|
||||||
** Testing
|
** Testing
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackRestApiClient;
|
||||||
use function DI\create;
|
use function DI\create;
|
||||||
use function DI\get;
|
use function DI\get;
|
||||||
use function DI\factory;
|
use function DI\factory;
|
||||||
|
@ -60,6 +61,8 @@ return [
|
||||||
get('config.service'),
|
get('config.service'),
|
||||||
get('mpdf')
|
get('mpdf')
|
||||||
),
|
),
|
||||||
|
YoutrackRestApiClient::class => create()->constructor(get('config.service')),
|
||||||
|
'youtrack_rest_api.client' => get(YoutrackRestApiClient::class),
|
||||||
'pdf_export.service' => get(PdfExportInterface::class),
|
'pdf_export.service' => get(PdfExportInterface::class),
|
||||||
// 'locale' => get('config.service')->method('get', 'en'),
|
// 'locale' => get('config.service')->method('get', 'en'),
|
||||||
// Translator::class => create()->constructor('sl')->method('addLoader', 'po', new PoFileLoader),
|
// Translator::class => create()->constructor('sl')->method('addLoader', 'po', new PoFileLoader),
|
||||||
|
|
|
@ -21,4 +21,5 @@ $reportCommand = $container->get(ReportCommand::class);
|
||||||
$application->add($reportCommand);
|
$application->add($reportCommand);
|
||||||
|
|
||||||
// eval(\Psy\sh());
|
// eval(\Psy\sh());
|
||||||
|
|
||||||
$application->run();
|
$application->run();
|
||||||
|
|
|
@ -51,6 +51,9 @@ class InvoiceCommand extends Command
|
||||||
|
|
||||||
protected PdfExportInterface $pdfExport;
|
protected PdfExportInterface $pdfExport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mailer service.
|
||||||
|
*/
|
||||||
protected MailerInterface $mailer;
|
protected MailerInterface $mailer;
|
||||||
|
|
||||||
protected const TYPE_WORK = 1;
|
protected const TYPE_WORK = 1;
|
||||||
|
@ -180,6 +183,9 @@ class InvoiceCommand extends Command
|
||||||
if (!empty($expenses)) {
|
if (!empty($expenses)) {
|
||||||
$data = array_merge($data, $expenses);
|
$data = array_merge($data, $expenses);
|
||||||
}
|
}
|
||||||
|
if (!empty($custom)) {
|
||||||
|
$data = array_merge($data, $custom);
|
||||||
|
}
|
||||||
$table = $this->getTable($output, $data);
|
$table = $this->getTable($output, $data);
|
||||||
$table->render();
|
$table->render();
|
||||||
if ($input->getOption('pdf')) {
|
if ($input->getOption('pdf')) {
|
||||||
|
|
|
@ -145,7 +145,7 @@ class ReportCsv implements ReportCsvInterface
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
'Kosten',
|
'Extra', // Kosten
|
||||||
'EUR',
|
'EUR',
|
||||||
];
|
];
|
||||||
// Don't make next line bold. See RprtCli\PdfExport\PdfExportService::parsedDataToHtml.
|
// Don't make next line bold. See RprtCli\PdfExport\PdfExportService::parsedDataToHtml.
|
||||||
|
@ -161,6 +161,7 @@ class ReportCsv implements ReportCsvInterface
|
||||||
];
|
];
|
||||||
$totalPrice += $invoice_element->getValue();
|
$totalPrice += $invoice_element->getValue();
|
||||||
}
|
}
|
||||||
|
// @todo Add Extra time as well!
|
||||||
}
|
}
|
||||||
if ($add_separator) {
|
if ($add_separator) {
|
||||||
$rows[] = ReportCsvInterface::SEPARATOR_MEDIUM;
|
$rows[] = ReportCsvInterface::SEPARATOR_MEDIUM;
|
||||||
|
|
|
@ -14,35 +14,63 @@ use Attribute;
|
||||||
#[Attribute]
|
#[Attribute]
|
||||||
class EntityDefinition {
|
class EntityDefinition {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $id
|
||||||
|
* @param string $provider
|
||||||
|
* @param string $name
|
||||||
|
* @param array $fields
|
||||||
|
* @param array $filters
|
||||||
|
* @param array $resources
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $id,
|
private string $id,
|
||||||
private string $provider,
|
private string $provider,
|
||||||
private string $name,
|
private string $name,
|
||||||
private array $fields,
|
private array $fields,
|
||||||
private array $filters
|
private array $filters,
|
||||||
|
private array $resources
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public function getId() {
|
public function getId() :string {
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getProvider() {
|
public function getProvider() :string {
|
||||||
return $this->provider;
|
return $this->provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getName() {
|
public function getName() :string {
|
||||||
return $this->name;
|
return $this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFields() {
|
public function getFields() :array {
|
||||||
return $this->fields;
|
return $this->fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFilters() {
|
public function getFilters() :array {
|
||||||
return $this->filters;
|
return $this->filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDefinition() {
|
/**
|
||||||
|
* Should be of type ResourceInterface.
|
||||||
|
*/
|
||||||
|
public function getResources() :array {
|
||||||
|
return $this->resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResource(string $key) :?Resource {
|
||||||
|
return $this->resources[$key] ?? NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get definition as array.
|
||||||
|
*
|
||||||
|
* @todo I'm not really sure why I implemented this method. Definition
|
||||||
|
* is a definition already.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getDefinition() :array {
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'provider' => $this->provider,
|
'provider' => $this->provider,
|
||||||
|
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace RprtCli\Utils\TimeTrackingServices;
|
namespace RprtCli\Utils\TimeTrackingServices;
|
||||||
|
|
||||||
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
|
||||||
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
|
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,6 +13,8 @@ interface EntityManagerInterface {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List supported entity types.
|
* List supported entity types.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function list(): array;
|
public function list(): array;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace RprtCli\Utils\TimeTrackingServices;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines resources for entity type.
|
||||||
|
*
|
||||||
|
* This class could be readonly after php82.
|
||||||
|
*/
|
||||||
|
final class Resource {
|
||||||
|
|
||||||
|
public const GET = 'GET';
|
||||||
|
public const POST = 'POST';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private string $id,
|
||||||
|
private string $method,
|
||||||
|
private string $path,
|
||||||
|
private string $description = ''
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function getId() :string {
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath() :string {
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMethod() :string {
|
||||||
|
return $this->method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription() :string {
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
|
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
|
||||||
|
|
||||||
|
use ReflectionAttribute;
|
||||||
use RprtCli\Utils\TimeTrackingServices\EntityManagerInterface;
|
use RprtCli\Utils\TimeTrackingServices\EntityManagerInterface;
|
||||||
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||||
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
|
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
|
||||||
|
@ -30,8 +31,11 @@ class EntityManager implements EntityManagerInterface {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the entity definitions.
|
* Returns all the entity definitions.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* List of entity definitions keyed with their id.
|
||||||
*/
|
*/
|
||||||
public function listEntityDefinitions() {
|
public function listEntityDefinitions() :array {
|
||||||
if (!$this->entityDefinitions) {
|
if (!$this->entityDefinitions) {
|
||||||
$this->discoverEntities();
|
$this->discoverEntities();
|
||||||
}
|
}
|
||||||
|
@ -40,8 +44,10 @@ class EntityManager implements EntityManagerInterface {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discovers entity definitions.
|
* Discovers entity definitions.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
*/
|
*/
|
||||||
private function discoverEntities() {
|
private function discoverEntities() :?array {
|
||||||
if (!empty($this->entityDefinitions)) {
|
if (!empty($this->entityDefinitions)) {
|
||||||
return $this->entityDefinitions;
|
return $this->entityDefinitions;
|
||||||
}
|
}
|
||||||
|
@ -49,8 +55,8 @@ class EntityManager implements EntityManagerInterface {
|
||||||
$definitions = [];
|
$definitions = [];
|
||||||
// @todo create a proxy service for finder.
|
// @todo create a proxy service for finder.
|
||||||
$this->finder->files()->in($path);
|
$this->finder->files()->in($path);
|
||||||
/** @var SplFileInfo $file */
|
|
||||||
foreach ($this->finder as $file) {
|
foreach ($this->finder as $file) {
|
||||||
|
/** @var SplFileInfo $file */
|
||||||
$class = self::ENTITIES_NAMESPACE . '\\' . self::ENTITIES_DIR . '\\' . $file->getBasename('.php');
|
$class = self::ENTITIES_NAMESPACE . '\\' . self::ENTITIES_DIR . '\\' . $file->getBasename('.php');
|
||||||
$reflection = new \ReflectionClass($class);
|
$reflection = new \ReflectionClass($class);
|
||||||
$attribute = $this->getAttributeOfInstance($reflection, EntityDefinition::class);
|
$attribute = $this->getAttributeOfInstance($reflection, EntityDefinition::class);
|
||||||
|
@ -61,8 +67,10 @@ class EntityManager implements EntityManagerInterface {
|
||||||
}
|
}
|
||||||
$instance = $attribute->newInstance();
|
$instance = $attribute->newInstance();
|
||||||
$id = $instance->getId();
|
$id = $instance->getId();
|
||||||
$content = $instance->getDefinition();
|
$content = [
|
||||||
$content['class'] = $class;
|
'definition' => $instance,
|
||||||
|
'class' => $class,
|
||||||
|
];
|
||||||
$definitions[$id] = $content;
|
$definitions[$id] = $content;
|
||||||
}
|
}
|
||||||
$this->entityDefinitions = $definitions;
|
$this->entityDefinitions = $definitions;
|
||||||
|
@ -77,19 +85,11 @@ class EntityManager implements EntityManagerInterface {
|
||||||
* @param string $instance
|
* @param string $instance
|
||||||
* The instance the attribute should be of.
|
* The instance the attribute should be of.
|
||||||
*
|
*
|
||||||
* @return
|
* @return ?ReflectionAttribute
|
||||||
* The attribute instance.
|
* The attribute instance.
|
||||||
*/
|
*/
|
||||||
protected function getAttributeOfInstance(\ReflectionClass $reflection, string $instance) {
|
protected function getAttributeOfInstance(\ReflectionClass $reflection, string $instance) :?ReflectionAttribute {
|
||||||
$t = $reflection->getAttributes();
|
|
||||||
var_dump($t);
|
|
||||||
var_dump($t[0]->getName());
|
|
||||||
var_dump($t[0]->getArguments());
|
|
||||||
var_dump($t[0]->newInstance());
|
|
||||||
$s = $reflection->getAttributes(EntityDefinition::class, \ReflectionAttribute::IS_INSTANCEOF);
|
|
||||||
var_dump($s);
|
|
||||||
$attributes = $reflection->getAttributes($instance, \ReflectionAttribute::IS_INSTANCEOF);
|
$attributes = $reflection->getAttributes($instance, \ReflectionAttribute::IS_INSTANCEOF);
|
||||||
var_dump($attributes);
|
|
||||||
if (empty($attributes)) {
|
if (empty($attributes)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,15 @@ class EntityManager implements EntityManagerInterface {
|
||||||
return reset($attributes);
|
return reset($attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entity definition by id.
|
||||||
|
*
|
||||||
|
* @param string $id
|
||||||
|
* Id of an entity definition (work_item, issue, project, comment).
|
||||||
|
*
|
||||||
|
* @return ?EntityDefinition
|
||||||
|
* Entity definition (maybe even EntityDefinition object).
|
||||||
|
*/
|
||||||
public function getDefinition(string $id) :?array {
|
public function getDefinition(string $id) :?array {
|
||||||
if (!isset($this->entityDefinitions)) {
|
if (!isset($this->entityDefinitions)) {
|
||||||
$this->list();
|
$this->list();
|
||||||
|
@ -108,6 +117,20 @@ class EntityManager implements EntityManagerInterface {
|
||||||
return $this->entityDefinitions[$id];
|
return $this->entityDefinitions[$id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create entity instance.
|
||||||
|
*
|
||||||
|
* This method should be used for transfering the data between report cli
|
||||||
|
* tools and the youtrack rest api.
|
||||||
|
*
|
||||||
|
* @param string $id
|
||||||
|
* Instance id (issue, project, work_item, comment ...).
|
||||||
|
* @param array $values
|
||||||
|
* Time, description ...
|
||||||
|
*
|
||||||
|
* @return ?EntityInterface
|
||||||
|
*
|
||||||
|
*/
|
||||||
public function createInstance(string $id, array $values) :?EntityInterface {
|
public function createInstance(string $id, array $values) :?EntityInterface {
|
||||||
if (!isset($this->entityDefinitions)) {
|
if (!isset($this->entityDefinitions)) {
|
||||||
$this->list();
|
$this->list();
|
||||||
|
@ -117,8 +140,10 @@ class EntityManager implements EntityManagerInterface {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
$definition = $this->getDefinition($id);
|
$definition = $this->getDefinition($id);
|
||||||
|
// This is not ok. definition is of type EntityDefinition but we want
|
||||||
|
// EntityInterface. This is where $class would come in handy.
|
||||||
$reflection = new \ReflectionClass($definition['class']);
|
$reflection = new \ReflectionClass($definition['class']);
|
||||||
return new $reflection->newInstanceArgs($values);
|
return $reflection->newInstanceArgs($values);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace RprtCli\Utils\TimeTrackingServices;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define filter properties.
|
||||||
|
*/
|
||||||
|
#[Attribute]
|
||||||
|
class FilterAttribute {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $id
|
||||||
|
* @param string $provider
|
||||||
|
* @param string $name
|
||||||
|
* @param array $allowedFields
|
||||||
|
* @param array $allowedEntities
|
||||||
|
* @param array $resources
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $id,
|
||||||
|
private string $provider,
|
||||||
|
private string $name,
|
||||||
|
private array $allowedFields = [],
|
||||||
|
private array $allowedEntities = [],
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function getId() :string {
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProvider() :string {
|
||||||
|
return $this->provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName() :string {
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllowedFields() :array {
|
||||||
|
return $this->allowedFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllowedEntities() :array {
|
||||||
|
return $this->allowedEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get definition as array.
|
||||||
|
*
|
||||||
|
* @todo I'm not really sure why I implemented this method. Definition
|
||||||
|
* is a definition already.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getDefinition() :array {
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'provider' => $this->provider,
|
||||||
|
'name' => $this->name,
|
||||||
|
'allowedFields' => $this->allowedFields,
|
||||||
|
'allowedEntities' => $this->allowedEntities,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\Filters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a filter attribute and some constants.
|
||||||
|
*/
|
||||||
|
interface YoutrackFilterInterface {
|
||||||
|
|
||||||
|
const OPTIONAL = 0;
|
||||||
|
const REQUIRED = 1;
|
||||||
|
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
|
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
|
||||||
|
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||||
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
|
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,4 +12,14 @@ use RprtCli\Utils\TimeTrackingServices\EntityInterface;
|
||||||
*/
|
*/
|
||||||
abstract class YoutrackEntity implements EntityInterface {
|
abstract class YoutrackEntity implements EntityInterface {
|
||||||
|
|
||||||
|
protected function getDefintion() :?EntityDefinition {
|
||||||
|
$reflection = new \ReflectionClass(self::class);
|
||||||
|
$attributes = $reflection->getAttributes(EntityDefinition::class, \ReflectionAttribute::IS_INSTANCEOF);
|
||||||
|
if (empty($attributes)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return reset($attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
|
||||||
|
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\Resource;
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\Filters\YoutarckFilterInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://www.jetbrains.com/help/youtrack/devportal/api-entity-IssueComment.html
|
||||||
|
* https://www.jetbrains.com/help/youtrack/devportal/resource-api-issues-issueID-comments.html
|
||||||
|
*/
|
||||||
|
#[EntityDefinition(
|
||||||
|
id: 'issue_comment',
|
||||||
|
provider: 'youtrack_rest_api',
|
||||||
|
name: 'IssueComment',
|
||||||
|
fields: [
|
||||||
|
'id',
|
||||||
|
'text',
|
||||||
|
'textPreview',
|
||||||
|
'created',
|
||||||
|
'author' => [
|
||||||
|
'id',
|
||||||
|
'fullName',
|
||||||
|
'email',
|
||||||
|
],
|
||||||
|
'issue' => [
|
||||||
|
'id',
|
||||||
|
'idReadable',
|
||||||
|
'project' => [
|
||||||
|
'id',
|
||||||
|
'shortName',
|
||||||
|
],
|
||||||
|
'summary',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
filters: [
|
||||||
|
'issue' => [
|
||||||
|
'required' => YoutrackFilterInterface::REQUIRED,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
resources: [
|
||||||
|
'list' => new Resource(
|
||||||
|
id: 'list',
|
||||||
|
method: Resource::GET,
|
||||||
|
path: '/api/issues/{issueID}/comments',
|
||||||
|
description: 'Lists comments of issue.'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
class YoutrackComment extends YoutrackEntity {
|
||||||
|
|
||||||
|
}
|
|
@ -6,16 +6,38 @@ namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes
|
||||||
|
|
||||||
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
|
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
|
||||||
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://www.jetbrains.com/help/youtrack/devportal/api-entity-Issue.html
|
* https://www.jetbrains.com/help/youtrack/devportal/api-entity-Issue.html
|
||||||
|
* https://www.jetbrains.com/help/youtrack/devportal/resource-api-issues.html
|
||||||
*/
|
*/
|
||||||
#[EntityDefinition(
|
#[EntityDefinition(
|
||||||
'issue',
|
id: 'issue',
|
||||||
'youtrack_rest_api',
|
provider: 'youtrack_rest_api',
|
||||||
'Issue',
|
name: 'Issue',
|
||||||
[],
|
fields: [
|
||||||
[]
|
'id',
|
||||||
|
'idReadable',
|
||||||
|
'project' => [
|
||||||
|
'id',
|
||||||
|
'shortName',
|
||||||
|
],
|
||||||
|
'summary',
|
||||||
|
],
|
||||||
|
filters: [
|
||||||
|
'state',
|
||||||
|
'project',
|
||||||
|
'assignee',
|
||||||
|
],
|
||||||
|
resources: [
|
||||||
|
'list' => new Resource(
|
||||||
|
id: 'list',
|
||||||
|
method: Resource::GET,
|
||||||
|
path: '/api/issues',
|
||||||
|
description: 'Lists issues.'
|
||||||
|
),
|
||||||
|
]
|
||||||
)]
|
)]
|
||||||
class YoutrackIssue extends YoutrackEntity {
|
class YoutrackIssue extends YoutrackEntity {
|
||||||
const ID = 'issue';
|
const ID = 'issue';
|
||||||
|
|
|
@ -4,15 +4,38 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
|
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
|
||||||
|
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\Resource;
|
||||||
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
|
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
|
||||||
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://www.jetbrains.com/help/youtrack/devportal/resource-api-admin-projects.html
|
||||||
|
*/
|
||||||
#[EntityDefinition(
|
#[EntityDefinition(
|
||||||
'project',
|
id: 'project',
|
||||||
'youtrack_rest_api',
|
provider: 'youtrack_rest_api',
|
||||||
'Youtrack Project',
|
name: 'Youtrack Project',
|
||||||
[],
|
fields: [
|
||||||
[]
|
'id',
|
||||||
|
'shortName',
|
||||||
|
'description',
|
||||||
|
'leader' => ['id', 'fullName'],
|
||||||
|
],
|
||||||
|
filters: [],
|
||||||
|
resources: [
|
||||||
|
'list' => new Resource(
|
||||||
|
id: 'list',
|
||||||
|
method: Resource::GET,
|
||||||
|
path: '/api/admin/projects',
|
||||||
|
description: 'List projects'
|
||||||
|
),
|
||||||
|
'read' => new Resource(
|
||||||
|
id: 'read',
|
||||||
|
method: Resource::GET,
|
||||||
|
path: '/api/admin/projects/{projectId}',
|
||||||
|
description: 'Sprecific project'
|
||||||
|
),
|
||||||
|
]
|
||||||
)]
|
)]
|
||||||
class YoutrackProject extends YoutrackEntity {
|
class YoutrackProject extends YoutrackEntity {
|
||||||
|
|
||||||
|
|
|
@ -4,27 +4,57 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
|
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
|
||||||
|
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\Resource;
|
||||||
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
|
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
|
||||||
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://www.jetbrains.com/help/youtrack/devportal/api-entity-IssueWorkItem.html
|
||||||
|
* https://www.jetbrains.com/help/youtrack/devportal/resource-api-workItems.html
|
||||||
|
*/
|
||||||
#[EntityDefinition(
|
#[EntityDefinition(
|
||||||
'work_item',
|
id: 'work_item',
|
||||||
'youtrack_rest_api',
|
provider: 'youtrack_rest_api',
|
||||||
'Issue Work Item',
|
name: 'Issue Work Item',
|
||||||
[
|
fields: [
|
||||||
'id',
|
'id',
|
||||||
'author',
|
'author' => [
|
||||||
|
'id',
|
||||||
|
'fullName',
|
||||||
|
'email'
|
||||||
|
],
|
||||||
'text',
|
'text',
|
||||||
'type',
|
'type',
|
||||||
'duration',
|
'duration' => [
|
||||||
'date',
|
'id',
|
||||||
'issue',
|
'minutes',
|
||||||
|
'presentation'
|
||||||
],
|
],
|
||||||
[
|
'date',
|
||||||
|
'created',
|
||||||
|
'issue' => [
|
||||||
|
'id',
|
||||||
|
'idReadable',
|
||||||
|
'project' => [
|
||||||
|
'id',
|
||||||
|
'shortName'
|
||||||
|
],
|
||||||
|
'summary',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
filters: [
|
||||||
'project',
|
'project',
|
||||||
'issue',
|
'issue',
|
||||||
'user',
|
'user',
|
||||||
'date',
|
'date',
|
||||||
|
],
|
||||||
|
resources: [
|
||||||
|
'list' => new Resource(
|
||||||
|
id: 'list',
|
||||||
|
method: Resource::GET,
|
||||||
|
path: '/api/workItems',
|
||||||
|
description: 'List workItems'
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)]
|
)]
|
||||||
class YoutrackWorkItem extends YoutrackEntity {
|
class YoutrackWorkItem extends YoutrackEntity {
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
|
||||||
|
|
||||||
|
use Cog\YouTrack\Rest\Client\YouTrackClient;
|
||||||
|
use Cog\YouTrack\Rest\Authorizer\TokenAuthorizer;
|
||||||
|
use Cog\YouTrack\Rest\HttpClient\GuzzleHttpClient;
|
||||||
|
use RprtCli\Utils\Configuration\ConfigurationInterface;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YouTrack php sdk abstraction service.
|
||||||
|
*
|
||||||
|
* Returns the client that takes care of authentication.
|
||||||
|
*/
|
||||||
|
class YoutrackRestApiClient {
|
||||||
|
|
||||||
|
protected ConfigurationInterface $config;
|
||||||
|
protected YouTrackClient $client;
|
||||||
|
|
||||||
|
// config
|
||||||
|
public function __construct(ConfigurationInterface $config) {
|
||||||
|
$this->config = $config;
|
||||||
|
$this->client = $this->createYoutrackClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createYoutrackClient() :YouTrackClient {
|
||||||
|
// Could this all go into __construct method?
|
||||||
|
$apiBaseUri = $this->config->get('tracking_service.youtrack.base_url', false);
|
||||||
|
$apiToken = $this->config->get('tracking_service.youtrack.auth_token', false);
|
||||||
|
// Instantiate PSR-7 HTTP Client
|
||||||
|
$psrHttpClient = new \GuzzleHttp\Client([
|
||||||
|
'base_uri' => $apiBaseUri,
|
||||||
|
'debug' => true,
|
||||||
|
]);
|
||||||
|
// Instantiate YouTrack API HTTP Client
|
||||||
|
$httpClient = new GuzzleHttpClient($psrHttpClient);
|
||||||
|
// Instantiate YouTrack API Token Authorizer
|
||||||
|
$authorizer = new TokenAuthorizer($apiToken);
|
||||||
|
// Instantiate YouTrack API Client
|
||||||
|
$client = new YouTrackClient($httpClient, $authorizer);
|
||||||
|
return $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClient() :YouTrackClient {
|
||||||
|
return $this->client;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
|
||||||
|
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\EntityManagerInterface;
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackRestApiClient;
|
||||||
|
|
||||||
|
class YoutrackRestApiRequestBuilder {
|
||||||
|
|
||||||
|
protected EntityManagerInterface $entityManager;
|
||||||
|
protected YoutrackRestApiClient $clientFactory;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entity_manager, YoutrackRestApiClient $clientFactory) {
|
||||||
|
$this->entityManager = $entity_manager;
|
||||||
|
$this->clientFactory = $clientFactory;
|
||||||
|
// EntityMangerInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that creates path to youtrack entity resource.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* list issue => GET '/api/issues?fields=id,idReadable,summary,..'
|
||||||
|
* create issue => POST '/api/issues'
|
||||||
|
* but create action is only allowed on worktItems.
|
||||||
|
* list work_item => GET '/api/workItem?fields=...'
|
||||||
|
*
|
||||||
|
* First impression seems simple. We need path and method.
|
||||||
|
* What bothers me whether it would be better to create a new attribute
|
||||||
|
* wehere these "mappings" would live: allowed actions and then method and path.
|
||||||
|
*
|
||||||
|
* @TODO Add filter system?
|
||||||
|
*
|
||||||
|
* @param string $action
|
||||||
|
* Name of the action: list, create, read, update, delete.
|
||||||
|
* @param string $entity_id
|
||||||
|
* Name of the entity id (project, issue, comment, work_item).
|
||||||
|
*
|
||||||
|
* @return ?string
|
||||||
|
* Path to the resource.
|
||||||
|
*/
|
||||||
|
public function buildPath(string $action, string $entity_id) :?string {
|
||||||
|
$defintion = $this->entityManager->getDefinition($entity_id);
|
||||||
|
if (!isset($defintion['definition'])) {
|
||||||
|
// Missing entity definition!
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$defintion = $defintion['definition'];
|
||||||
|
}
|
||||||
|
$fields = $definition->getFields();
|
||||||
|
$fieldsQuery = $this->fieldsQuery($fields);
|
||||||
|
/** @var RprtCli\Utils\TimeTrackingServices\Resource $resource */
|
||||||
|
$resource = $defintion->getResource($action);
|
||||||
|
if (!$resource) {
|
||||||
|
// Missing resource!
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
$path = $resource->getPath();
|
||||||
|
// @TODO add query, create client request.
|
||||||
|
// @see https://github.com/cybercog/youtrack-rest-php/blob/eb0315133d1d3d161da23d26537201afb253dec9/src/Client/YouTrackClient.php#L89
|
||||||
|
if ($fieldsQuery) {
|
||||||
|
$path .= '?' . $fieldsQuery;
|
||||||
|
}
|
||||||
|
// @TODO create request and handle request.
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fieldsQuery(array $fields) :?string {
|
||||||
|
$query = implode(',', $this->stringifyFields($fields));
|
||||||
|
if (!empty($query)) {
|
||||||
|
return "fields=${query}";
|
||||||
|
}
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function stringifyFields(array $fields) :array {
|
||||||
|
foreach ($fields as $key => &$field) {
|
||||||
|
if (is_array($field)) {
|
||||||
|
$field = $key . '(' . implode(',', $this->stringifyFields($field)) . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ namespace RprtCli\Tests\Unit\YoutrackRestApi;
|
||||||
|
|
||||||
use DI\ContainerBuilder;
|
use DI\ContainerBuilder;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||||
|
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the entity manager service and system.
|
* Test the entity manager service and system.
|
||||||
|
@ -24,15 +26,26 @@ class EntityManagerTest extends TestCase {
|
||||||
// Instance of EntityMangerInterface;
|
// Instance of EntityMangerInterface;
|
||||||
// $this->assertInstanceOf('EntityManagerInterface', $entityManagerService);
|
// $this->assertInstanceOf('EntityManagerInterface', $entityManagerService);
|
||||||
$definitions = $entityManagerService->list();
|
$definitions = $entityManagerService->list();
|
||||||
var_dump($definitions);
|
// var_dump($definitions);
|
||||||
$this->assertCount(3, $definitions);
|
$this->assertCount(3, $definitions);
|
||||||
$entities = ['project', 'issue', 'work_item'];
|
$entities = ['project', 'issue', 'work_item'];
|
||||||
foreach ($entities as $entity) {
|
foreach ($entities as $entity_id) {
|
||||||
$this->assertArrayHasKey($entity, $definitions, 'Check that key exists in entity types definitions.');
|
$this->assertArrayHasKey($entity_id, $definitions, 'Check that key exists in entity types definitions.');
|
||||||
|
var_dump($definitions[$entity_id]);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(EntityDefinition::class, $definitions[$entity_id]['definition'], 'Check that definiton is of type entity definition');
|
||||||
|
$definition = $entityManagerService->getDefinition($entity_id);
|
||||||
|
$entity = $entityManagerService->createInstance($entity_id, []);
|
||||||
|
$this->assertArrayHasKey('definition', $definition);
|
||||||
|
$this->assertArrayHasKey('class', $definition);
|
||||||
|
$this->assertInstanceOf(EntityDefinition::class, $definition['definition']);
|
||||||
|
$this->assertInstanceOf(EntityInterface::class, $entity);
|
||||||
}
|
}
|
||||||
// list method returns definitions: project, issue, worktItem
|
// list method returns definitions: project, issue, worktItem
|
||||||
// Check one definition, compare.
|
// Check one definition, compare.
|
||||||
|
// Test creating the instance
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
7
todo.txt
7
todo.txt
|
@ -15,6 +15,13 @@ TODO:
|
||||||
- invoice command improvements
|
- invoice command improvements
|
||||||
- pdf and output option could be combined
|
- pdf and output option could be combined
|
||||||
|
|
||||||
|
TODO: (start of 2023)
|
||||||
|
- youtrack rest api:
|
||||||
|
- resources class and parameter in EntityDefinition
|
||||||
|
- compose fields method
|
||||||
|
- create filter system
|
||||||
|
- connect filter system to youtrack rest api command
|
||||||
|
|
||||||
TODO (end of 2022):
|
TODO (end of 2022):
|
||||||
- DONE fix project list output (first is missing)
|
- DONE fix project list output (first is missing)
|
||||||
- DONE report selection trait
|
- DONE report selection trait
|
||||||
|
|
Loading…
Reference in New Issue