open api spec

This commit is contained in:
Yurii
2025-12-02 16:14:42 +02:00
parent ddbfa80f82
commit 876df81526
39 changed files with 2683 additions and 5 deletions
@@ -69,4 +69,9 @@ class FieldType
* @since 9.3.0
*/
public const DECIMAL = 'decimal';
/**
* @since 9.3.0
*/
public const URL_MULTIPLE = 'urlMultiple';
}
@@ -103,4 +103,39 @@ class FieldParam
* Foreign field.
*/
public const FIELD = 'field';
/**
* Required.
*
* @since 9.3.0
*/
public const REQUIRED = 'required';
/**
* Disabled.
*
* @since 9.3.0
*/
public const DISABLED = 'disabled';
/**
* Utility. For internal purposes.
*
* @since 9.3.0
*/
public const UTILITY = 'utility';
/**
* Min value.
*
* @since 9.3.0
*/
public const MIN = 'min';
/**
* Max value.
*
* @since 9.3.0
*/
public const MAX = 'max';
}
@@ -67,7 +67,8 @@
"AddressCountry": "Address Country",
"AppSecret": "App Secret",
"OAuthProvider": "OAuth Provider",
"OAuthAccount": "OAuth Account"
"OAuthAccount": "OAuth Account",
"OpenApi": "OpenAPI"
},
"scopeNamesPlural": {
"Note": "Notes",
@@ -125,7 +126,8 @@
"AddressCountry": "Address Countries",
"AppSecret": "App Secrets",
"OAuthProvider": "OAuth Providers",
"OAuthAccount": "OAuth Accounts"
"OAuthAccount": "OAuth Accounts",
"OpenApi": "OpenAPI"
},
"labels": {
"Previous Page": "Previous Page",
@@ -79,7 +79,8 @@
"customizationInlineEditDisabledDisabled": true,
"customizationDefaultView": "views/admin/field-manager/fields/currency-default",
"customizationTooltipTextDisabled": true,
"maxLength": 3
"maxLength": 3,
"apiSpecDisabled": true
},
"converted": {
"type": "currencyConverted",
@@ -0,0 +1,3 @@
{
"acl": "boolean"
}
+5
View File
@@ -500,6 +500,11 @@
"route": "/OAuth/:id/connection",
"actionClassName": "Espo\\Tools\\OAuth\\Api\\DeleteConnection"
},
{
"method": "get",
"route": "/OpenApi",
"actionClassName": "Espo\\Tools\\OpenApi\\Api\\GetSpec"
},
{
"route": "/:controller/:id",
"method": "get",
@@ -0,0 +1,74 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\OpenApi\Api;
use Espo\Core\Acl;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Api\ResponseComposer;
use Espo\Core\Exceptions\Forbidden;
use Espo\Tools\OpenApi\ProviderFactory;
/**
* @noinspection PhpUnused
*/
class GetSpec implements Action
{
private const string SCOPE = 'OpenApi';
public function __construct(
private Acl $acl,
private ProviderFactory $providerFactory,
) {}
public function process(Request $request): Response
{
$this->checkAccess();
$provider = $this->providerFactory->create();
$spec = $provider->get();
return ResponseComposer::empty()
->writeBody($spec)
->setHeader('Content-Type', 'application/json');
}
/**
* @throws Forbidden
*/
private function checkAccess(): void
{
if (!$this->acl->checkScope(self::SCOPE)) {
throw new Forbidden("No access to OpenApi scope.");
}
}
}
@@ -0,0 +1,13 @@
<?php
namespace Espo\Tools\OpenApi;
/**
* Not stable yet. May change.
*
* @internal
*/
interface FieldSchemaBuilder
{
public function build(string $entityType, string $field): FieldSchemaResult;
}
@@ -0,0 +1,97 @@
<?php
namespace Espo\Tools\OpenApi;
use Espo\Core\InjectableFactory;
use Espo\Core\ORM\Type\FieldType;
use Espo\ORM\Defs;
use Espo\ORM\Name\Attribute;
use Espo\Tools\OpenApi\FieldSchemaBuilders\NoSupport;
use Espo\Tools\OpenApi\FieldSchemaBuilders\EnumType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\MultiEnumType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\PhoneType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\TextType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\VarcharType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\BoolType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\IntType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\FloatType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\DecimalType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\AutoincrementType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\CurrencyType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\CurrencyConvertedType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\NumberType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\DateType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\DatetimeType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\DatetimeOptionalType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\ForeignType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\EmailType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\LinkType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\LinkParentType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\LinkMultipleType;
use Espo\Tools\OpenApi\FieldSchemaBuilders\IdType;
class FieldSchemaBuilderFactory
{
/** @var array<string, class-string<FieldSchemaBuilder>> */
private array $map = [
FieldType::VARCHAR => VarcharType::class,
FieldType::URL => VarcharType::class,
FieldType::ENUM => EnumType::class,
FieldType::TEXT => TextType::class,
FieldType::WYSIWYG => TextType::class,
FieldType::NUMBER => NumberType::class,
FieldType::BOOL => BoolType::class,
FieldType::INT => IntType::class,
FieldType::FLOAT => FloatType::class,
FieldType::DECIMAL => DecimalType::class,
FieldType::CURRENCY => CurrencyType::class,
FieldType::AUTOINCREMENT => AutoincrementType::class,
FieldType::CURRENCY_CONVERTED => CurrencyConvertedType::class,
FieldType::FOREIGN => ForeignType::class,
FieldType::EMAIL => EmailType::class,
FieldType::PHONE => PhoneType::class,
FieldType::DATE => DateType::class,
FieldType::DATETIME => DatetimeType::class,
FieldType::DATETIME_OPTIONAL => DatetimeOptionalType::class,
FieldType::MULTI_ENUM => MultiEnumType::class,
FieldType::ARRAY => MultiEnumType::class,
FieldType::CHECKLIST => MultiEnumType::class,
FieldType::URL_MULTIPLE => MultiEnumType::class,
FieldType::LINK => LinkType::class,
FieldType::LINK_ONE => LinkType::class,
FieldType::FILE => LinkType::class,
FieldType::IMAGE => LinkType::class,
FieldType::LINK_PARENT => LinkParentType::class,
FieldType::LINK_MULTIPLE => LinkMultipleType::class,
];
public function __construct(
private InjectableFactory $injectableFactory,
private Defs $defs,
) {}
public function create(string $entityType, string $field): FieldSchemaBuilder
{
$className = $this->getClassName($field, $entityType);
return $this->injectableFactory->create($className);
}
/**
* @return class-string<FieldSchemaBuilder>
*/
private function getClassName(string $field, string $entityType): string
{
if ($field === Attribute::ID) {
return IdType::class;
}
$fieldDefs = $this->defs
->getEntity($entityType)
->getField($field);
$type = $fieldDefs->getType();
return $this->map[$type] ?? NoSupport::class;
}
}
@@ -0,0 +1,28 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class AutoincrementType implements FieldSchemaBuilder
{
public function __construct(
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$schema = (object) [
'type' => Type::INTEGER,
'readOnly' => true,
'description' => 'An auto-increment number.',
];
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
);
}
}
@@ -0,0 +1,32 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class BoolType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [
'type' => Type::BOOLEAN,
'readOnly' => $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false,
];
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
);
}
}
@@ -0,0 +1,32 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class CurrencyConvertedType implements FieldSchemaBuilder
{
public function __construct(
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$schema = (object) [];
$schema->type = Type::NUMBER;
$schema->readOnly = true;
$schema->description = 'A currency amount converted to the default currency.';
$schema->type = [
$schema->type,
Type::NULL,
];
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
);
}
}
@@ -0,0 +1,80 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\Core\Currency\ConfigDataProvider;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class CurrencyType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
private ConfigDataProvider $configDataProvider,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if ($fieldDefs->getParam(FieldParam::DECIMAL)) {
$schema->type = Type::STRING;
$schema->pattern = '^-?\d+(\.\d+)?$';
$schema->description = 'A numeric string';
} else {
$schema->type = Type::NUMBER;
if ($fieldDefs->getParam(FieldParam::MIN) !== null) {
$schema->minimum = $fieldDefs->getParam(FieldParam::MIN);
}
if ($fieldDefs->getParam(FieldParam::MAX) !== null) {
$schema->maximum = $fieldDefs->getParam(FieldParam::MAX);
}
}
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$schema->description = 'A currency amount.';
$codeSchema = (object) [];
$codeSchema->type = Type::STRING;
$codeSchema->enum = $this->configDataProvider->getCurrencyList();
$codeSchema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$codeSchema->type = [
$codeSchema->type,
Type::NULL,
];
}
$codeSchema->description = 'A code.';
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED)) {
$required[] = $field;
$required[] = $field . 'Currency';
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
$field . 'Currency' => get_object_vars($codeSchema),
],
required: $required,
);
}
}
@@ -0,0 +1,48 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class DateType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::STRING;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
$schema->format = 'date';
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$schema->description = 'A date.';
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
required: $required,
);
}
}
@@ -0,0 +1,62 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class DatetimeOptionalType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::STRING;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$schema->pattern = '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$';
$schema->examples = ['2026-11-29 12:34:56'];
$schema->description = 'A timestamp in UTC.';
$schemaDate = (object) [];
$schemaDate->type = Type::STRING;
$schemaDate->format = 'date';
$schemaDate->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
$schemaDate->type = [
$schemaDate->type,
Type::NULL,
];
$schema->description = "Specified if the '$field' field is all-day.";
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
$field . 'Date' => get_object_vars($schemaDate),
],
required: $required,
);
}
}
@@ -0,0 +1,49 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class DatetimeType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::STRING;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$schema->pattern = '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$';
$schema->examples = ['2026-11-29 12:34:56'];
$schema->description = 'A timestamp in UTC.';
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
required: $required,
);
}
}
@@ -0,0 +1,48 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class DecimalType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::STRING;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
$schema->pattern = '^-?\d+(\.\d+)?$';
$schema->description = 'A numeric string.';
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED) && !$fieldDefs->getParam(FieldParam::DEFAULT)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
required: $required,
);
}
}
@@ -0,0 +1,86 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class EmailType implements FieldSchemaBuilder
{
const int DEFAULT_MAX_LENGTH = 255;
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::STRING;
$schema->maxLength = self::DEFAULT_MAX_LENGTH;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$schema->description = 'A primary email address.';
$itemSchema = (object) [];
$itemSchema->type = Type::OBJECT;
$itemSchema->properties = [
'emailAddress' => [
'type' => Type::STRING,
],
'primary' => [
'type' => Type::BOOLEAN,
],
'optOut' => [
'type' => Type::BOOLEAN,
],
'invalid' => [
'type' => Type::BOOLEAN,
],
'lower' => [
'type' => Type::STRING,
'readOnly' => true,
],
];
$itemSchema->required = [
'emailAddress',
'primary',
];
$dataSchema = (object) [];
$dataSchema->type = Type::ARRAY;
$dataSchema->items = get_object_vars($itemSchema);
$dataSchema->description = 'Multiple email addresses';
$dataSchema->type = [
$dataSchema->type,
Type::NULL,
];
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
$field . 'Data' => get_object_vars($dataSchema),
],
required: $required,
);
}
}
@@ -0,0 +1,56 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
use Espo\Tools\OpenApi\Util\EnumOptionsProvider;
class EnumType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
private EnumOptionsProvider $optionsProvider,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::STRING;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
$optionList = $this->optionsProvider->get($fieldDefs);
if ($optionList) {
if (in_array('', $optionList) && !$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
Type::STRING,
Type::NULL,
];
}
$optionList = array_filter($optionList, fn ($it) => $it !== '');
$optionList = array_values($optionList);
$schema->enum = $optionList;
}
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED) && !$fieldDefs->getParam(FieldParam::DEFAULT)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
required: $required,
);
}
}
@@ -0,0 +1,53 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class FloatType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::NUMBER;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if ($fieldDefs->getParam(FieldParam::MIN) !== null) {
$schema->minimum = $fieldDefs->getParam(FieldParam::MIN);
}
if ($fieldDefs->getParam(FieldParam::MAX) !== null) {
$schema->maximum = $fieldDefs->getParam(FieldParam::MAX);
}
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED) && !$fieldDefs->getParam(FieldParam::DEFAULT)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
required: $required,
);
}
}
@@ -0,0 +1,83 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\Core\ORM\Type\FieldType;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class ForeignType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$schema = (object) [];
$schema->readOnly = true;
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$link = $fieldDefs->getParam(FieldParam::LINK);
$foreignField = $fieldDefs->getParam(FieldParam::FIELD);
$foreignEntityType = $this->defs
->getEntity($entityType)
->tryGetRelation($link)
?->tryGetForeignEntityType();
if (!$foreignEntityType) {
return new FieldSchemaResult([]);
}
$foreignFieldDefs = $this->defs
->getEntity($foreignEntityType)
->tryGetField($foreignField);
if (!$foreignFieldDefs) {
return new FieldSchemaResult([]);
}
$fieldType = $foreignFieldDefs->getType();
if (
$fieldType === FieldType::ENUM ||
$fieldType === FieldType::VARCHAR ||
$fieldType === FieldType::TEXT ||
$fieldType === FieldType::DATE ||
$fieldType === FieldType::DATETIME ||
$fieldType === FieldType::EMAIL ||
$fieldType === FieldType::PHONE ||
$fieldType === FieldType::WYSIWYG ||
$fieldType === FieldType::DECIMAL
) {
$schema->type = Type::STRING;
} else if (
$foreignEntityType === FieldType::INT
) {
$schema->type = Type::INTEGER;
} else if (
$foreignEntityType === FieldType::FLOAT
) {
$schema->type = Type::NUMBER;
} else {
return new FieldSchemaResult([]);
}
$schema->type = [
$schema->type,
Type::NULL,
];
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
);
}
}
@@ -0,0 +1,40 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Doctrine\DBAL\Types\Types;
use Espo\ORM\Defs;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class IdType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->tryGetField($field);
$schema = (object) [];
$schema->type = Type::STRING;
$schema->readOnly = true;
$dbType = $fieldDefs?->getParam(Defs\Params\FieldParam::DB_TYPE);
if ($dbType === Types::BIGINT || $dbType === Types::INTEGER) {
$schema->type = Type::INTEGER;
}
$schema->description = 'An ID.';
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
);
}
}
@@ -0,0 +1,53 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class IntType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::INTEGER;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if ($fieldDefs->getParam(FieldParam::MIN) !== null) {
$schema->minimum = $fieldDefs->getParam(FieldParam::MIN);
}
if ($fieldDefs->getParam(FieldParam::MAX) !== null) {
$schema->maximum = $fieldDefs->getParam(FieldParam::MAX);
}
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED) && !$fieldDefs->getParam(FieldParam::DEFAULT)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
required: $required,
);
}
}
@@ -0,0 +1,158 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\Core\ORM\Type\FieldType;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
use Espo\Tools\OpenApi\Util\EnumOptionsProvider;
class LinkMultipleType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
private EnumOptionsProvider $enumOptionsProvider,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$linkDefs = $this->defs->getEntity($entityType)->tryGetRelation($field);
$idsSchema = (object) [
'type' => Type::ARRAY,
'items' => [
'type' => Type::STRING,
],
];
$idsSchema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
$foreignEntityType = null;
if ($linkDefs && $linkDefs->tryGetForeignEntityType()) {
$foreignEntityType = $linkDefs->tryGetForeignEntityType();
$idsSchema->description = "IDs of records of the $foreignEntityType type.";
}
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$idsSchema->type = [
$idsSchema->type,
Type::NULL,
];
}
$namesSchema = (object) [];
$namesSchema->type = Type::OBJECT;
$namesSchema->additionalProperties = [
'type' => Type::STRING,
];
$namesSchema->readOnly = true;
$namesSchema->description = 'An {ID => record name} map.';
$namesSchema->type = [
$namesSchema->type,
Type::NULL,
];
$output = [
$field . 'Ids' => get_object_vars($idsSchema),
$field . 'Names' => get_object_vars($namesSchema),
];
/** @var ?array<string, string> $columns */
$columns = $fieldDefs->getParam('columns');
if (is_array($columns) && $foreignEntityType) {
$columnsSchema = (object) [];
$columnsSchema->type = Type::OBJECT;
$columnsSchema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
$columnsSchema->description = 'An {ID => object} map. Relationship column values.';
$columnsSchema->type = [
$columnsSchema->type,
Type::NULL,
];
$properties = array_map(function ($columnField) use ($entityType, $foreignEntityType) {
return $this->prepareColumnSchema($entityType, $foreignEntityType, $columnField);
}, $columns);
$columnsSchema->additionalProperties = [
'type' => Type::OBJECT,
'properties' => $properties,
];
$output[$field . 'Columns'] = get_object_vars($columnsSchema);
}
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED)) {
$required[] = $field . 'Ids';
}
return new FieldSchemaResult(
properties: $output,
required: $required,
);
}
/**
* @return array<string, mixed>
*/
private function prepareColumnSchema(string $entityType, string $foreignEntityType, string $columnField): array
{
$colSchema = (object) [];
$fieldDefs = $this->defs->getEntity($foreignEntityType)->tryGetField($columnField);
if (!$fieldDefs) {
$fieldDefs = $this->defs->getEntity($entityType)->tryGetField($columnField);
}
if (!$fieldDefs) {
return get_object_vars($colSchema);
}
$optionList = $this->enumOptionsProvider->get($fieldDefs);
if ($optionList) {
$colSchema->type = Type::STRING;
if (in_array('', $optionList)) {
$colSchema->type = [
$colSchema->type,
Type::NULL,
];
}
$optionList = array_filter($optionList, fn($it) => $it !== '');
$optionList = array_values($optionList);
$colSchema->enum = $optionList;
return get_object_vars($colSchema);
}
if ($fieldDefs->getType() === FieldType::BOOL) {
$colSchema->type = Type::BOOLEAN;
} else if ($fieldDefs->getType() === FieldType::INT) {
$colSchema->type = Type::INTEGER;
} else if ($fieldDefs->getType() === FieldType::FLOAT) {
$colSchema->type = Type::NUMBER;
} else if ($fieldDefs->getType() === FieldType::VARCHAR) {
$colSchema->type = Type::STRING;
if ($fieldDefs->getParam(FieldParam::MAX_LENGTH)) {
$colSchema->maxLength = $fieldDefs->getParam(FieldParam::MAX_LENGTH);
}
}
return get_object_vars($colSchema);
}
}
@@ -0,0 +1,78 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class LinkParentType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$idSchema = (object) [];
$idSchema->type = Type::STRING;
$idSchema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
$idSchema->description = "A foreign record ID.";
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$idSchema->type = [
$idSchema->type,
Type::NULL,
];
}
$typeSchema = (object) [];
$typeSchema->type = Type::STRING;
$typeSchema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$typeSchema->type = [
$typeSchema->type,
Type::NULL,
];
}
if ($fieldDefs->getParam('entityList')) {
$typeSchema->enum = $fieldDefs->getParam('entityList');
}
$typeSchema->description = "An entity type.";
$nameSchema = (object) [];
$nameSchema->type = Type::STRING;
$nameSchema->readOnly = true;
$nameSchema->type = [
$nameSchema->type,
Type::NULL,
];
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED)) {
$required[] = $field . 'Id';
$required[] = $field . 'Type';
}
$nameSchema->description = 'A foreign record name.';
return new FieldSchemaResult(
properties: [
$field . 'Id' => get_object_vars($idSchema),
$field . 'Type' => get_object_vars($typeSchema),
$field . 'Name' => get_object_vars($nameSchema),
],
required: $required,
);
}
}
@@ -0,0 +1,63 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class LinkType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$linkDefs = $this->defs->getEntity($entityType)->tryGetRelation($field);
$schema = (object) [];
$schema->type = Type::STRING;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if ($linkDefs && $linkDefs->tryGetForeignEntityType()) {
$foreignEntityType = $linkDefs->tryGetForeignEntityType();
$schema->description = "An ID of the record of the $foreignEntityType type.";
}
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$nameSchema = (object) [];
$nameSchema->type = Type::STRING;
$nameSchema->readOnly = true;
$nameSchema->type = [
$nameSchema->type,
Type::NULL,
];
$nameSchema->description = 'A foreign record name.';
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED)) {
$required[] = $field . 'Id';
}
return new FieldSchemaResult(
properties: [
$field . 'Id' => get_object_vars($schema),
$field . 'Name' => get_object_vars($nameSchema),
],
required: $required,
);
}
}
@@ -0,0 +1,54 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
use Espo\Tools\OpenApi\Util\EnumOptionsProvider;
class MultiEnumType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
private EnumOptionsProvider $optionsProvider,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::ARRAY;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
$optionList = $this->optionsProvider->get($fieldDefs);
$itemSchema = (object) [
'type' => Type::STRING,
];
if ($optionList) {
$itemSchema->enum = $optionList;
}
$schema->items = get_object_vars($itemSchema);
$schema->description = 'A multi-enum.';
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED) && !$fieldDefs->getParam(FieldParam::DEFAULT)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
required: $required,
);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class NoSupport implements FieldSchemaBuilder
{
public function build(string $entityType, string $field): FieldSchemaResult
{
return new FieldSchemaResult(
properties: [],
);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class NumberType implements FieldSchemaBuilder
{
public function __construct(
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$schema = (object) [];
$schema->type = Type::STRING;
$schema->readOnly = true;
$schema->description = 'A number. Auto-incrementing with a prefix.';
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
);
}
}
@@ -0,0 +1,85 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class PhoneType implements FieldSchemaBuilder
{
const int DEFAULT_MAX_LENGTH = 36;
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::STRING;
$schema->maxLength = self::DEFAULT_MAX_LENGTH;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$schema->description = 'A primary phone number.';
$itemSchema = (object) [];
$itemSchema->type = Type::OBJECT;
$itemSchema->properties = [
'phoneNumber' => [
'type' => Type::STRING,
],
'primary' => [
'type' => Type::BOOLEAN,
],
'optOut' => [
'type' => Type::BOOLEAN,
],
'invalid' => [
'type' => Type::BOOLEAN,
],
'type' => [
'type' => Type::STRING,
'enum' => $fieldDefs->getParam('typeList'),
],
];
$itemSchema->required = [
'phoneNumber',
'primary',
];
$dataSchema = (object) [];
$dataSchema->type = Type::ARRAY;
$dataSchema->items = get_object_vars($itemSchema);
$dataSchema->type = [
$dataSchema->type,
Type::NULL,
];
$dataSchema->description = 'Multiple phone numbers.';
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
$field . 'Data' => get_object_vars($dataSchema),
],
required: $required,
);
}
}
@@ -0,0 +1,52 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class TextType implements FieldSchemaBuilder
{
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::STRING;
if ($fieldDefs->getParam(FieldParam::MAX_LENGTH)) {
$schema->maxLength = $fieldDefs->getParam(FieldParam::MAX_LENGTH);
}
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$schema->description = 'A multi-line text.';
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED) && !$fieldDefs->getParam(FieldParam::DEFAULT)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
required: $required,
);
}
}
@@ -0,0 +1,50 @@
<?php
namespace Espo\Tools\OpenApi\FieldSchemaBuilders;
use Espo\Tools\OpenApi\Type;
use Espo\ORM\Defs;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\Tools\OpenApi\FieldSchemaBuilder;
use Espo\Tools\OpenApi\FieldSchemaResult;
class VarcharType implements FieldSchemaBuilder
{
const int DEFAULT_MAX_LENGTH = 255;
public function __construct(
private Defs $defs,
) {}
public function build(string $entityType, string $field): FieldSchemaResult
{
$fieldDefs = $this->defs->getEntity($entityType)->getField($field);
$schema = (object) [];
$schema->type = Type::STRING;
$schema->maxLength = $fieldDefs->getParam(FieldParam::MAX_LENGTH) ?? self::DEFAULT_MAX_LENGTH;
$schema->readOnly = $fieldDefs->getParam(FieldParam::READ_ONLY) ?? false;
if (!$fieldDefs->getParam(FieldParam::REQUIRED)) {
$schema->type = [
$schema->type,
Type::NULL,
];
}
$schema->description = 'A one-line string.';
$required = [];
if ($fieldDefs->getParam(FieldParam::REQUIRED) && !$fieldDefs->getParam(FieldParam::DEFAULT)) {
$required[] = $field;
}
return new FieldSchemaResult(
properties: [
$field => get_object_vars($schema),
],
required: $required,
);
}
}
@@ -0,0 +1,15 @@
<?php
namespace Espo\Tools\OpenApi;
class FieldSchemaResult
{
/**
* @param array<string, array<string, mixed>> $properties
* @param string[] $required
*/
public function __construct(
public array $properties,
public array $required = [],
) {}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
<?php
namespace Espo\Tools\OpenApi;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\InjectableFactory;
class ProviderFactory
{
public function __construct(
private InjectableFactory $injectableFactory,
) {}
public function create(): Provider
{
return $this->injectableFactory->createWithBinding(
Provider::class,
BindingContainerBuilder::create()
->build()
);
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace Espo\Tools\OpenApi;
class Type
{
const string INTEGER = 'integer';
const string NUMBER = 'number';
const string STRING = 'string';
const string BOOLEAN = 'boolean';
const string OBJECT = 'object';
const string ARRAY = 'array';
const string NULL = 'null';
}
@@ -0,0 +1,37 @@
<?php
namespace Espo\Tools\OpenApi\Util;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Defs\FieldDefs;
class EnumOptionsProvider
{
public function __construct(
private Metadata $metadata,
) {}
/**
* @return ?string[]
*/
public function get(FieldDefs $fieldDefs): ?array
{
/** @var ?string $path */
$path = $fieldDefs->getParam('optionsPath');
/** @var ?string $path */
$ref = $fieldDefs->getParam('optionsReference');
if (!$path && $ref && str_contains($ref, '.')) {
[$refEntityType, $refField] = explode('.', $ref);
$path = "entityDefs.$refEntityType.fields.$refField.options";
}
/** @var ?string[] $optionList */
$optionList = $path ?
$this->metadata->get($path) :
$fieldDefs->getParam('options');
return $optionList;
}
}
Generated
+2 -2
View File
@@ -9909,9 +9909,9 @@
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"laminas/laminas-ldap": 20,
"laminas/laminas-mail": 20,
"laminas/laminas-mime": 20,
"laminas/laminas-ldap": 20,
"zordius/lightncandy": 20
},
"prefer-stable": false,
@@ -9930,6 +9930,6 @@
"ext-pdo": "*",
"ext-ctype": "*"
},
"platform-dev": {},
"platform-dev": [],
"plugin-api-version": "2.6.0"
}
+4
View File
@@ -1120,6 +1120,10 @@
"type": "boolean",
"description": "Disables the ability perform mass-update on the field. As of v8.4."
},
"apiSpecDisabled": {
"type": "boolean",
"description": "Disable field definitions in API specification. As of v9.3."
},
"isPersonalData": {
"type": "boolean",
"description": "Whether the field may contain personal data."