Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/Service/DatamodelService.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ public function getClasses(): array
return $classes;
}

public function IsValidClass(string $className): bool
{
$classes = $this->getClasses();
return isset($classes[$className]);
}

public function getClassSchema(string $className): array
{
$classSchema = $this->datamodelCache->get('ClassSchema-'.$className.'-'.$this->language, function(ItemInterface $item) use ($className) {
Expand Down Expand Up @@ -208,5 +214,26 @@ protected function getWorkflowfromXml(string $className): array
}
return $workflow;
}

/**
* Extract the name of the selected class from an OQL query and check that the class is valid for the current datamodel
* @param string $oql
*/
public function getClassFromOQL(string $oql)
{
$aMatches = [];
$oql = trim($oql);
if (!preg_match('/^SELECT +([_a-zA-Z][_a-zA-Z0-9]*) +(?:JOIN|WHERE|AS)/', $oql, $aMatches)) {
if (!preg_match('/^SELECT +(?:[_a-zA-Z][_a-zA-Z0-9, ]*) +FROM +([_a-zA-Z][_a-zA-Z0-9]*)/', $oql, $aMatches)) {
Comment on lines +226 to +227
throw new \InvalidArgumentException("Syntax error: '$oql' does not look like a valid OQL query.");
}
}

$className = $aMatches[1];
if (!$this->IsValidClass($className)) {
throw new \InvalidArgumentException("Class '$className' is not a valid class in the current datamodel. Use the tool '".TOOL_PREFIX."list-all-classes' to get the list of valid classes.");
}
return $className;
}
}

34 changes: 33 additions & 1 deletion src/Tools/core/get/iTopGetTools.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public function getPersonFromTelephone(string $telephone): string
if (count($phones) === 0) {
throw new \Exception('Datamodel issue: there seem to be no phone number attribute on the Person class!');
}
$conditions = array_map(function($item) use($me, $telephone) { return "$item = '".iTopGetTools::quoteString($telephone)."'"; }, $phones);
$conditions = array_map(function($item) use($telephone) { return "$item = '".iTopGetTools::quoteString($telephone)."'"; }, $phones);
$whereClause = implode(' OR ', $conditions);
return $this->runToolFromTemplates('getPersonFromTelephone', 'Anything', [
'query' => 'SELECT Person WHERE '.$whereClause,
Expand Down Expand Up @@ -114,4 +114,36 @@ public function searchUserRequestByCallerStatusStartDate(#[Schema(format: 'email
]
);
}

/**
* This tool searches for any object in iTop based on an OQL query.
* The OQL query must be a valid iTop OQL query that returns objects of the desired class.
* The result will be a list of objects of the same class, with their fields as specified in the datamodel.
* IMPORTANT: Do NOT assume that the datamodel is the "standard" one, use the tool get-class-schema - before creating the OQL query -
* to find the possible values for the class, its fields and its relations.
* @param string $oql The OQL query to execute. It must be a valid OQL query that returns objects of the desired class.
* @param int $limit The number of objects per page (> 0)
* @param int $page The (one based) number of the page (>= 1)
*/
#[McpTool(name: TOOL_PREFIX.'search-any-object-by-oql', annotations: new ToolAnnotations(null, true, false, true, false))]
public function searchAnyObjectByOql(string $oql, int $limit = 20, int $page = 1 ): string
{
if ($limit <= 0) {
throw new \InvalidArgumentException('Limit must be a positive integer');
}
if ($page < 1) {
throw new \InvalidArgumentException('Page must be a positive integer (1 or greater)');
}
$className = $this->datamodel->getClassFromOQL($oql);
$outputFields = $this->datamodel->getListZlist($className);
return $this->runToolFromTemplates('searchAnyObjectByOql', 'Anything',
Comment on lines +129 to +139
[
'class' => $className,
'output_fields' => $outputFields,
'oql' => $oql,
'limit' => $limit,
'page' => $page,
]
);
}
}
2 changes: 1 addition & 1 deletion templates/Anything-output.toon.twig
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class {{ className }}:
{% endfor %}


{{ className }} {
{{ className }} {% if json.message is defined %}(Total number of objects {{json.message|lower}}){% endif %} {
{% for obj in json.objects %}
{{obj.key}}{% for code, value in obj.fields %}{% if code !== 'id' and value is not iterable %}, "{{ value }}"{% endif %}{% endfor ~%}
{% endfor %}
Expand Down
8 changes: 8 additions & 0 deletions templates/searchAnyObjectByOql-input.json.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"operation":"core/get",
"class": "{{class}}",
"key": "{{oql}}",
"limit": {{limit}},
"page": {{page}},
"output_fields": "{{output_fields}}"
}
41 changes: 40 additions & 1 deletion tests/phpunit/DatamodelServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use App\Service\DatamodelService;
use Symfony\Contracts\Cache\CacheInterface;
use PHPUnit\Framework\Attributes\DataProvider;


class DatamodelServiceTest extends KernelTestCase
Expand Down Expand Up @@ -32,7 +33,45 @@ public function testGetClassSchema(): void
$service = new DatamodelService(__DIR__.'/../../data/datamodel-production.xml', $this->cache, 'FR FR');
$schema = $service->getClassSchema('Person');
$expected = 'name,status,org_id,org_name,email,phone,notify,function,cis_list,picture,first_name,employee_number,mobile_phone,location_id,location_name,manager_id,manager_name,team_list,user_list,tickets_list';
$this->assertEquals(explode(',', $expected), array_keys($schema));
$this->assertEquals(explode(',', $expected), array_keys($schema['fields']));
}

public function testGetClassFromOQLOk(): void
{
$service = new DatamodelService(__DIR__.'/../../data/datamodel-production.xml', $this->cache, 'FR FR');
$this->assertEquals('Person', $service->getClassFromOQL(" SELECT Person WHERE name='Foo'"));

$this->assertEquals('Person', $service->getClassFromOQL("SELECT Person WHERE name='Foo'"));

$this->assertEquals('Person', $service->getClassFromOQL("SELECT P FROM Person WHERE name='Foo'"));

$this->assertEquals('Person', $service->getClassFromOQL("SELECT P,O FROM Person JOIN Organization AS O ON P.org_d = O.id WHERE name='Foo'"));

$this->assertEquals('Person', $service->getClassFromOQL("SELECT Person AS P WHERE name='Foo'"));

$this->assertEquals('Person', $service->getClassFromOQL("SELECT Person AS P JOIN Organization AS O WHERE P.name='Foo'"));
}

/**
* @param string $oql
*/
#[DataProvider('oqlKoProvider')]
public function testGetClassFromOQLKo(string $oql): void
{
$service = new DatamodelService(__DIR__.'/../../data/datamodel-production.xml', $this->cache, 'FR FR');

$this->expectException("InvalidArgumentException");
$this->assertEquals('Person', $service->getClassFromOQL($oql));
}

public static function oqlKoProvider(): array
{
return [
["SELECT 123456 WHERE name='Foo'"],
["NOT A SELECT Person"],
["SELECT FROM Person WHERE name='Foo'"],
["SELECT NotAValidClassName WHERE name='Foo'"],
];
}
}

Expand Down
54 changes: 47 additions & 7 deletions tests/phpunit/ToolsFromTemplateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,20 @@ public function testGetPersonFromEmail(): void
]
];
$mockiTopClient = $this->MockiTopClientThatWillReturn($input, $output);
$tools = new iTopGetTools($this->twigEnvironment, $mockiTopClient, $this->logger, $this->datamodel);

$this->assertEquals(

$expected =
<<<EXPECTED
class Person:
id, friendlyname, email, org_id

Person {
Person {
1, "Test Person", "test@demo.com", "1"
}

EXPECTED
, $tools->getPersonFromEmail('test@demo.com'));
EXPECTED;

$tools = new iTopGetTools($this->twigEnvironment, $mockiTopClient, $this->logger, $this->datamodel);
$this->assertEquals($expected, $tools->getPersonFromEmail('test@demo.com'));
}

public function testGetPersonFromTelephone(): void
Expand Down Expand Up @@ -100,13 +101,52 @@ public function testGetPersonFromTelephone(): void
class Person:
id, friendlyname, email, org_id

Person {
Person {
1, "Test Person", "test@demo.com", "1"
}
EXPECTED;

$this->assertEquals(trim($expected), trim($tools->getPersonFromTelephone('123456789')));
}

public function testSearchAnyObjectByOql()
{
// Mock the iTopClient service, to check that the templating works
$oql = "SELECT Person WHERE name LIKE '%Smith%'";
$input =
<<<JSON
{
"operation":"core/get",
"class": "Person",
"key": "SELECT\u0020Person\u0020WHERE\u0020name\u0020LIKE\u0020\u0027\u0025Smith\u0025\u0027",
"limit": 20,
"page": 1,
"output_fields": "friendlyname,name,org_id,status,location_id,email,phone"
}
JSON;
$output = [
'objects' => [
'Person::1' => [
'class' => 'Person',
'key' => 1,
'fields' => ['friendlyname' => 'John Smith', 'email' => 'john.smith@demo.com'],
],
],
];
$mockiTopClient = $this->MockiTopClientThatWillReturn($input, $output);

$expected =
<<<JSON
class Person:
id, friendlyname, email

Person {
1, "John Smith", "john.smith@demo.com"
}
JSON;
$tools = new iTopGetTools($this->twigEnvironment, $mockiTopClient, $this->logger, $this->datamodel);
$this->assertEquals(trim($expected), trim($tools->searchAnyObjectByOql($oql)));
}

protected function MockiTopClientThatWillReturn(string $inputData, array $outputData)
{
Expand Down