summaryrefslogtreecommitdiff
path: root/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service
diff options
context:
space:
mode:
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service')
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/ActivityToActivityContactAssigneesJoinable.php40
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php71
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php277
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php61
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php98
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php140
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php217
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php128
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php118
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php320
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActionScheduleCreationSpecProvider.php27
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php27
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php29
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php32
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php29
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php22
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php34
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php25
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php22
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php22
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php22
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NoteCreationSpecProvider.php28
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php24
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php23
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/RequestSpec.php110
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php117
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php131
30 files changed, 2263 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/ActivityToActivityContactAssigneesJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/ActivityToActivityContactAssigneesJoinable.php
new file mode 100644
index 00000000..191f4389
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/ActivityToActivityContactAssigneesJoinable.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Civi\Api4\Service\Schema\Joinable;
+
+class ActivityToActivityContactAssigneesJoinable extends Joinable {
+ /**
+ * @var string
+ */
+ protected $baseTable = 'civicrm_activity';
+
+ /**
+ * @var string
+ */
+ protected $baseColumn = 'id';
+
+ /**
+ * @param $alias
+ */
+ public function __construct($alias) {
+ $optionValueTable = 'civicrm_option_value';
+ $optionGroupTable = 'civicrm_option_group';
+
+ $subSubSelect = sprintf(
+ 'SELECT id FROM %s WHERE name = "%s"',
+ $optionGroupTable,
+ 'activity_contacts'
+ );
+
+ $subSelect = sprintf(
+ 'SELECT value FROM %s WHERE name = "%s" AND option_group_id = (%s)',
+ $optionValueTable,
+ 'Activity Assignees',
+ $subSubSelect
+ );
+
+ $this->addCondition(sprintf('%s.record_type_id = (%s)', $alias, $subSelect));
+ parent::__construct('civicrm_activity_contact', 'activity_id', $alias);
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php
new file mode 100644
index 00000000..370c5898
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Civi\Api4\Service\Schema\Joinable;
+
+class BridgeJoinable extends Joinable {
+ /**
+ * @var Joinable
+ */
+ protected $middleLink;
+
+ public function __construct($targetTable, $targetColumn, $alias, Joinable $middleLink) {
+ parent::__construct($targetTable, $targetColumn, $alias);
+ $this->middleLink = $middleLink;
+ }
+
+ /**
+ * @return Joinable
+ */
+ public function getMiddleLink() {
+ return $this->middleLink;
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php
new file mode 100644
index 00000000..a1dd1a1d
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Civi\Api4\Service\Schema\Joinable;
+
+use Civi\Api4\CustomField;
+
+class CustomGroupJoinable extends Joinable {
+
+ /**
+ * @var string
+ */
+ protected $joinSide = self::JOIN_SIDE_LEFT;
+
+ /**
+ * @var string
+ *
+ * Name of the custom field column.
+ */
+ protected $columns;
+
+ /**
+ * @param $targetTable
+ * @param $alias
+ * @param bool $isMultiRecord
+ * @param null $entity
+ */
+ public function __construct($targetTable, $alias, $isMultiRecord, $entity, $columns) {
+ $this->entity = $entity;
+ $this->columns = $columns;
+ parent::__construct($targetTable, 'entity_id', $alias);
+ $this->joinType = $isMultiRecord ?
+ self::JOIN_TYPE_ONE_TO_MANY : self::JOIN_TYPE_ONE_TO_ONE;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getEntityFields() {
+ if (!$this->entityFields) {
+ $fields = CustomField::get()
+ ->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_required', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value'])
+ ->addWhere('custom_group.table_name', '=', $this->getTargetTable())
+ ->execute();
+ foreach ($fields as $field) {
+ $this->entityFields[] = \Civi\Api4\Service\Spec\SpecFormatter::arrayToField($field, $this->getEntity());
+ }
+ }
+ return $this->entityFields;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getField($fieldName) {
+ foreach ($this->getEntityFields() as $field) {
+ $name = $field->getName();
+ if ($name === $fieldName || strrpos($name, '.' . $fieldName) === strlen($name) - strlen($fieldName) - 1) {
+ return $field;
+ }
+ }
+ return NULL;
+ }
+
+ /**
+ * @return string
+ */
+ public function getSqlColumn($fieldName) {
+ return $this->columns[$fieldName];
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php
new file mode 100644
index 00000000..0e92e3ab
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php
@@ -0,0 +1,277 @@
+<?php
+
+namespace Civi\Api4\Service\Schema\Joinable;
+
+use Civi\Api4\Service\Spec\FieldSpec;
+use CRM_Core_DAO_AllCoreTables as Tables;
+
+class Joinable {
+
+ const JOIN_SIDE_LEFT = 'LEFT';
+ const JOIN_SIDE_INNER = 'INNER';
+
+ const JOIN_TYPE_ONE_TO_ONE = '1_to_1';
+ const JOIN_TYPE_MANY_TO_ONE = 'n_to_1';
+ const JOIN_TYPE_ONE_TO_MANY = '1_to_n';
+
+ /**
+ * @var string
+ */
+ protected $baseTable;
+
+ /**
+ * @var string
+ */
+ protected $baseColumn;
+
+ /**
+ * @var string
+ */
+ protected $targetTable;
+
+ /**
+ * @var string
+ *
+ * Name (or alias) of the target column)
+ */
+ protected $targetColumn;
+
+ /**
+ * @var string
+ */
+ protected $alias;
+
+ /**
+ * @var array
+ */
+ protected $conditions = [];
+
+ /**
+ * @var string
+ */
+ protected $joinSide = self::JOIN_SIDE_LEFT;
+
+ /**
+ * @var int
+ */
+ protected $joinType = self::JOIN_TYPE_ONE_TO_ONE;
+
+ /**
+ * @var string
+ */
+ protected $entity;
+
+ /**
+ * @var array
+ */
+ protected $entityFields;
+
+ /**
+ * @param $targetTable
+ * @param $targetColumn
+ * @param string|null $alias
+ */
+ public function __construct($targetTable, $targetColumn, $alias = NULL) {
+ $this->targetTable = $targetTable;
+ $this->targetColumn = $targetColumn;
+ if (!$this->entity) {
+ $this->entity = Tables::getBriefName(Tables::getClassForTable($targetTable));
+ }
+ $this->alias = $alias ?: str_replace('civicrm_', '', $targetTable);
+ }
+
+ /**
+ * Gets conditions required when joining to a base table
+ *
+ * @param string|null $baseTableAlias
+ * Name of the base table, if aliased.
+ *
+ * @return array
+ */
+ public function getConditionsForJoin($baseTableAlias = NULL) {
+ $baseCondition = sprintf(
+ '%s.%s = %s.%s',
+ $baseTableAlias ?: $this->baseTable,
+ $this->baseColumn,
+ $this->getAlias(),
+ $this->targetColumn
+ );
+
+ return array_merge([$baseCondition], $this->conditions);
+ }
+
+ /**
+ * @return string
+ */
+ public function getBaseTable() {
+ return $this->baseTable;
+ }
+
+ /**
+ * @param string $baseTable
+ *
+ * @return $this
+ */
+ public function setBaseTable($baseTable) {
+ $this->baseTable = $baseTable;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getBaseColumn() {
+ return $this->baseColumn;
+ }
+
+ /**
+ * @param string $baseColumn
+ *
+ * @return $this
+ */
+ public function setBaseColumn($baseColumn) {
+ $this->baseColumn = $baseColumn;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAlias() {
+ return $this->alias;
+ }
+
+ /**
+ * @param string $alias
+ *
+ * @return $this
+ */
+ public function setAlias($alias) {
+ $this->alias = $alias;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTargetTable() {
+ return $this->targetTable;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTargetColumn() {
+ return $this->targetColumn;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEntity() {
+ return $this->entity;
+ }
+
+ /**
+ * @param $condition
+ *
+ * @return $this
+ */
+ public function addCondition($condition) {
+ $this->conditions[] = $condition;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getExtraJoinConditions() {
+ return $this->conditions;
+ }
+
+ /**
+ * @param array $conditions
+ *
+ * @return $this
+ */
+ public function setConditions($conditions) {
+ $this->conditions = $conditions;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getJoinSide() {
+ return $this->joinSide;
+ }
+
+ /**
+ * @param string $joinSide
+ *
+ * @return $this
+ */
+ public function setJoinSide($joinSide) {
+ $this->joinSide = $joinSide;
+
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getJoinType() {
+ return $this->joinType;
+ }
+
+ /**
+ * @param int $joinType
+ *
+ * @return $this
+ */
+ public function setJoinType($joinType) {
+ $this->joinType = $joinType;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray() {
+ return get_object_vars($this);
+ }
+
+ /**
+ * @return \Civi\Api4\Service\Spec\FieldSpec[]
+ */
+ public function getEntityFields() {
+ if (!$this->entityFields) {
+ $bao = Tables::getClassForTable($this->getTargetTable());
+ if ($bao) {
+ foreach ($bao::fields() as $field) {
+ $this->entityFields[] = \Civi\Api4\Service\Spec\SpecFormatter::arrayToField($field, $this->getEntity());
+ }
+ }
+ }
+ return $this->entityFields;
+ }
+
+ /**
+ * @return \Civi\Api4\Service\Spec\FieldSpec|NULL
+ */
+ public function getField($fieldName) {
+ foreach ($this->getEntityFields() as $field) {
+ if ($field->getName() === $fieldName) {
+ return $field;
+ }
+ }
+ return NULL;
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php
new file mode 100644
index 00000000..96f65488
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Civi\Api4\Service\Schema\Joinable;
+
+class OptionValueJoinable extends Joinable {
+ /**
+ * @var string
+ */
+ protected $optionGroupName;
+
+ /**
+ * @param string $optionGroup
+ * Can be either the option group name or ID
+ * @param string|null $alias
+ * The join alias
+ * @param string $keyColumn
+ * Which column to use to join, defaults to "value"
+ */
+ public function __construct($optionGroup, $alias = NULL, $keyColumn = 'value') {
+ $this->optionGroupName = $optionGroup;
+ $optionValueTable = 'civicrm_option_value';
+
+ // default join alias to option group name, e.g. activity_type
+ if (!$alias && !is_numeric($optionGroup)) {
+ $alias = $optionGroup;
+ }
+
+ parent::__construct($optionValueTable, $keyColumn, $alias);
+
+ if (!is_numeric($optionGroup)) {
+ $subSelect = 'SELECT id FROM civicrm_option_group WHERE name = "%s"';
+ $subQuery = sprintf($subSelect, $optionGroup);
+ $condition = sprintf('%s.option_group_id = (%s)', $alias, $subQuery);
+ }
+ else {
+ $condition = sprintf('%s.option_group_id = %d', $alias, $optionGroup);
+ }
+
+ $this->addCondition($condition);
+ }
+
+ /**
+ * The existing condition must also be re-aliased
+ *
+ * @param string $alias
+ *
+ * @return $this
+ */
+ public function setAlias($alias) {
+ foreach ($this->conditions as $index => $condition) {
+ $search = $this->alias . '.';
+ $replace = $alias . '.';
+ $this->conditions[$index] = str_replace($search, $replace, $condition);
+ }
+
+ parent::setAlias($alias);
+
+ return $this;
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php
new file mode 100644
index 00000000..cb30ab57
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Civi\Api4\Service\Schema;
+
+use Civi\Api4\Query\Api4SelectQuery;
+use Civi\Api4\Service\Schema\Joinable\Joinable;
+
+class Joiner {
+ /**
+ * @var SchemaMap
+ */
+ protected $schemaMap;
+
+ /**
+ * @var Joinable[][]
+ */
+ protected $cache = [];
+
+ /**
+ * @param SchemaMap $schemaMap
+ */
+ public function __construct(SchemaMap $schemaMap) {
+ $this->schemaMap = $schemaMap;
+ }
+
+ /**
+ * @param Api4SelectQuery $query
+ * The query object to do the joins on
+ * @param string $joinPath
+ * A path of aliases in dot notation, e.g. contact.phone
+ * @param string $side
+ * Can be LEFT or INNER
+ *
+ * @throws \Exception
+ * @return Joinable[]
+ * The path used to make the join
+ */
+ public function join(Api4SelectQuery $query, $joinPath, $side = 'LEFT') {
+ $fullPath = $this->getPath($query->getFrom(), $joinPath);
+ $baseTable = $query::MAIN_TABLE_ALIAS;
+
+ foreach ($fullPath as $link) {
+ $target = $link->getTargetTable();
+ $alias = $link->getAlias();
+ $conditions = $link->getConditionsForJoin($baseTable);
+
+ $query->join($side, $target, $alias, $conditions);
+ $query->addJoinedTable($link);
+
+ $baseTable = $link->getAlias();
+ }
+
+ return $fullPath;
+ }
+
+ /**
+ * @param Api4SelectQuery $query
+ * @param $joinPath
+ *
+ * @return bool
+ */
+ public function canJoin(Api4SelectQuery $query, $joinPath) {
+ return !empty($this->getPath($query->getFrom(), $joinPath));
+ }
+
+ /**
+ * @param string $baseTable
+ * @param string $joinPath
+ *
+ * @return array
+ * @throws \Exception
+ */
+ protected function getPath($baseTable, $joinPath) {
+ $cacheKey = sprintf('%s.%s', $baseTable, $joinPath);
+ if (!isset($this->cache[$cacheKey])) {
+ $stack = explode('.', $joinPath);
+ $fullPath = [];
+
+ foreach ($stack as $key => $targetAlias) {
+ $links = $this->schemaMap->getPath($baseTable, $targetAlias);
+
+ if (empty($links)) {
+ throw new \Exception(sprintf('Cannot join %s to %s', $baseTable, $targetAlias));
+ }
+ else {
+ $fullPath = array_merge($fullPath, $links);
+ $lastLink = end($links);
+ $baseTable = $lastLink->getTargetTable();
+ }
+ }
+
+ $this->cache[$cacheKey] = $fullPath;
+ }
+
+ return $this->cache[$cacheKey];
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php
new file mode 100644
index 00000000..3989afeb
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Civi\Api4\Service\Schema;
+
+use Civi\Api4\Service\Schema\Joinable\BridgeJoinable;
+use Civi\Api4\Service\Schema\Joinable\Joinable;
+
+class SchemaMap {
+
+ const MAX_JOIN_DEPTH = 3;
+
+ /**
+ * @var Table[]
+ */
+ protected $tables = [];
+
+ /**
+ * @param $baseTableName
+ * @param $targetTableAlias
+ *
+ * @return Joinable[]
+ * Array of links to the target table, empty if no path found
+ */
+ public function getPath($baseTableName, $targetTableAlias) {
+ $table = $this->getTableByName($baseTableName);
+ $path = [];
+
+ if (!$table) {
+ return $path;
+ }
+
+ $this->findPaths($table, $targetTableAlias, 1, $path);
+
+ foreach ($path as $index => $pathLink) {
+ if ($pathLink instanceof BridgeJoinable) {
+ $start = array_slice($path, 0, $index);
+ $middle = [$pathLink->getMiddleLink()];
+ $end = array_slice($path, $index, count($path) - $index);
+ $path = array_merge($start, $middle, $end);
+ }
+ }
+
+ return $path;
+ }
+
+ /**
+ * @return Table[]
+ */
+ public function getTables() {
+ return $this->tables;
+ }
+
+ /**
+ * @param $name
+ *
+ * @return Table|null
+ */
+ public function getTableByName($name) {
+ foreach ($this->tables as $table) {
+ if ($table->getName() === $name) {
+ return $table;
+ }
+ }
+
+ return NULL;
+ }
+
+ /**
+ * Adds a table to the schema map if it has not already been added
+ *
+ * @param Table $table
+ *
+ * @return $this
+ */
+ public function addTable(Table $table) {
+ if (!$this->getTableByName($table->getName())) {
+ $this->tables[] = $table;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param array $tables
+ */
+ public function addTables(array $tables) {
+ foreach ($tables as $table) {
+ $this->addTable($table);
+ }
+ }
+
+ /**
+ * Recursive function to traverse the schema looking for a path
+ *
+ * @param Table $table
+ * The current table to base fromm
+ * @param string $target
+ * The target joinable table alias
+ * @param int $depth
+ * The current level of recursion which reflects the number of joins needed
+ * @param Joinable[] $path
+ * (By-reference) The possible paths to the target table
+ * @param Joinable[] $currentPath
+ * For internal use only to track the path to reach the target table
+ */
+ private function findPaths(Table $table, $target, $depth, &$path, $currentPath = []
+ ) {
+ static $visited = [];
+
+ // reset if new call
+ if ($depth === 1) {
+ $visited = [];
+ }
+
+ $canBeShorter = empty($path) || count($currentPath) + 1 < count($path);
+ $tooFar = $depth > self::MAX_JOIN_DEPTH;
+ $beenHere = in_array($table->getName(), $visited);
+
+ if ($tooFar || $beenHere || !$canBeShorter) {
+ return;
+ }
+
+ // prevent circular reference
+ $visited[] = $table->getName();
+
+ foreach ($table->getExternalLinks() as $link) {
+ if ($link->getAlias() === $target) {
+ $path = array_merge($currentPath, [$link]);
+ }
+ else {
+ $linkTable = $this->getTableByName($link->getTargetTable());
+ if ($linkTable) {
+ $nextStep = array_merge($currentPath, [$link]);
+ $this->findPaths($linkTable, $target, $depth + 1, $path, $nextStep);
+ }
+ }
+ }
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php
new file mode 100644
index 00000000..b578b73a
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php
@@ -0,0 +1,217 @@
+<?php
+
+namespace Civi\Api4\Service\Schema;
+
+use Civi\Api4\Entity;
+use Civi\Api4\Event\Events;
+use Civi\Api4\Event\SchemaMapBuildEvent;
+use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable;
+use Civi\Api4\Service\Schema\Joinable\Joinable;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Civi\Api4\Service\Schema\Joinable\OptionValueJoinable;
+use CRM_Core_DAO_AllCoreTables as TableHelper;
+use CRM_Utils_Array as UtilsArray;
+
+class SchemaMapBuilder {
+ /**
+ * @var EventDispatcherInterface
+ */
+ protected $dispatcher;
+ /**
+ * @var array
+ */
+ protected $apiEntities;
+
+ /**
+ * @param EventDispatcherInterface $dispatcher
+ */
+ public function __construct(EventDispatcherInterface $dispatcher) {
+ $this->dispatcher = $dispatcher;
+ $this->apiEntities = array_keys((array) Entity::get()->setCheckPermissions(FALSE)->addSelect('name')->execute()->indexBy('name'));
+ }
+
+ /**
+ * @return SchemaMap
+ */
+ public function build() {
+ $map = new SchemaMap();
+ $this->loadTables($map);
+
+ $event = new SchemaMapBuildEvent($map);
+ $this->dispatcher->dispatch(Events::SCHEMA_MAP_BUILD, $event);
+
+ return $map;
+ }
+
+ /**
+ * Add all tables and joins
+ *
+ * @param SchemaMap $map
+ */
+ private function loadTables(SchemaMap $map) {
+ /** @var \CRM_Core_DAO $daoName */
+ foreach (TableHelper::get() as $daoName => $data) {
+ $table = new Table($data['table']);
+ foreach ($daoName::fields() as $field => $fieldData) {
+ $this->addJoins($table, $field, $fieldData);
+ }
+ $map->addTable($table);
+ if (in_array($data['name'], $this->apiEntities)) {
+ $this->addCustomFields($map, $table, $data['name']);
+ }
+ }
+
+ $this->addBackReferences($map);
+ }
+
+ /**
+ * @param Table $table
+ * @param string $field
+ * @param array $data
+ */
+ private function addJoins(Table $table, $field, array $data) {
+ $fkClass = UtilsArray::value('FKClassName', $data);
+
+ // can there be multiple methods e.g. pseudoconstant and fkclass
+ if ($fkClass) {
+ $tableName = TableHelper::getTableForClass($fkClass);
+ $fkKey = UtilsArray::value('FKKeyColumn', $data, 'id');
+ $alias = str_replace('_id', '', $field);
+ $joinable = new Joinable($tableName, $fkKey, $alias);
+ $joinable->setJoinType($joinable::JOIN_TYPE_MANY_TO_ONE);
+ $table->addTableLink($field, $joinable);
+ }
+ elseif (UtilsArray::value('pseudoconstant', $data)) {
+ $this->addPseudoConstantJoin($table, $field, $data);
+ }
+ }
+
+ /**
+ * @param Table $table
+ * @param string $field
+ * @param array $data
+ */
+ private function addPseudoConstantJoin(Table $table, $field, array $data) {
+ $pseudoConstant = UtilsArray::value('pseudoconstant', $data);
+ $tableName = UtilsArray::value('table', $pseudoConstant);
+ $optionGroupName = UtilsArray::value('optionGroupName', $pseudoConstant);
+ $keyColumn = UtilsArray::value('keyColumn', $pseudoConstant, 'id');
+
+ if ($tableName) {
+ $alias = str_replace('civicrm_', '', $tableName);
+ $joinable = new Joinable($tableName, $keyColumn, $alias);
+ $condition = UtilsArray::value('condition', $pseudoConstant);
+ if ($condition) {
+ $joinable->addCondition($condition);
+ }
+ $table->addTableLink($field, $joinable);
+ }
+ elseif ($optionGroupName) {
+ $keyColumn = UtilsArray::value('keyColumn', $pseudoConstant, 'value');
+ $joinable = new OptionValueJoinable($optionGroupName, NULL, $keyColumn);
+
+ if (!empty($data['serialize'])) {
+ $joinable->setJoinType($joinable::JOIN_TYPE_ONE_TO_MANY);
+ }
+
+ $table->addTableLink($field, $joinable);
+ }
+ }
+
+ /**
+ * Loop through existing links and provide link from the other side
+ *
+ * @param SchemaMap $map
+ */
+ private function addBackReferences(SchemaMap $map) {
+ foreach ($map->getTables() as $table) {
+ foreach ($table->getTableLinks() as $link) {
+ // there are too many possible joins from option value so skip
+ if ($link instanceof OptionValueJoinable) {
+ continue;
+ }
+
+ $target = $map->getTableByName($link->getTargetTable());
+ $tableName = $link->getBaseTable();
+ $plural = str_replace('civicrm_', '', $this->getPlural($tableName));
+ $joinable = new Joinable($tableName, $link->getBaseColumn(), $plural);
+ $joinable->setJoinType($joinable::JOIN_TYPE_ONE_TO_MANY);
+ $target->addTableLink($link->getTargetColumn(), $joinable);
+ }
+ }
+ }
+
+ /**
+ * Simple implementation of pluralization.
+ * Could be replaced with symfony/inflector
+ *
+ * @param string $singular
+ *
+ * @return string
+ */
+ private function getPlural($singular) {
+ $last_letter = substr($singular, -1);
+ switch ($last_letter) {
+ case 'y':
+ return substr($singular, 0, -1) . 'ies';
+
+ case 's':
+ return $singular . 'es';
+
+ default:
+ return $singular . 's';
+ }
+ }
+
+ /**
+ * @param \Civi\Api4\Service\Schema\SchemaMap $map
+ * @param \Civi\Api4\Service\Schema\Table $baseTable
+ * @param string $entity
+ */
+ private function addCustomFields(SchemaMap $map, Table $baseTable, $entity) {
+ // Don't be silly
+ if (!array_key_exists($entity, \CRM_Core_SelectValues::customGroupExtends())) {
+ return;
+ }
+ $queryEntity = (array) $entity;
+ if ($entity == 'Contact') {
+ $queryEntity = ['Contact', 'Individual', 'Organization', 'Household'];
+ }
+ $fieldData = \CRM_Utils_SQL_Select::from('civicrm_custom_field f')
+ ->join('custom_group', 'INNER JOIN civicrm_custom_group g ON g.id = f.custom_group_id')
+ ->select(['g.name as custom_group_name', 'g.table_name', 'g.is_multiple', 'f.name', 'label', 'column_name', 'option_group_id'])
+ ->where('g.extends IN (@entity)', ['@entity' => $queryEntity])
+ ->where('g.is_active')
+ ->where('f.is_active')
+ ->execute();
+
+ $links = [];
+
+ while ($fieldData->fetch()) {
+ $tableName = $fieldData->table_name;
+
+ $customTable = $map->getTableByName($tableName);
+ if (!$customTable) {
+ $customTable = new Table($tableName);
+ }
+
+ if (!empty($fieldData->option_group_id)) {
+ $optionValueJoinable = new OptionValueJoinable($fieldData->option_group_id, $fieldData->label);
+ $customTable->addTableLink($fieldData->column_name, $optionValueJoinable);
+ }
+
+ $map->addTable($customTable);
+
+ $alias = $fieldData->custom_group_name;
+ $links[$alias]['tableName'] = $tableName;
+ $links[$alias]['isMultiple'] = !empty($fieldData->is_multiple);
+ $links[$alias]['columns'][$fieldData->name] = $fieldData->column_name;
+ }
+
+ foreach ($links as $alias => $link) {
+ $joinable = new CustomGroupJoinable($link['tableName'], $alias, $link['isMultiple'], $entity, $link['columns']);
+ $baseTable->addTableLink('id', $joinable);
+ }
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php
new file mode 100644
index 00000000..1f464a45
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Civi\Api4\Service\Schema;
+
+use Civi\Api4\Service\Schema\Joinable\Joinable;
+
+class Table {
+
+ /**
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * @var Joinable[]
+ * Array of links to other tables
+ */
+ protected $tableLinks = [];
+
+ /**
+ * @param $name
+ */
+ public function __construct($name) {
+ $this->name = $name;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name) {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * @return Joinable[]
+ */
+ public function getTableLinks() {
+ return $this->tableLinks;
+ }
+
+ /**
+ * @return Joinable[]
+ * Only those links that are not joining the table to itself
+ */
+ public function getExternalLinks() {
+ return array_filter($this->tableLinks, function (Joinable $joinable) {
+ return $joinable->getTargetTable() !== $this->getName();
+ });
+ }
+
+ /**
+ * @param Joinable $linkToRemove
+ */
+ public function removeLink(Joinable $linkToRemove) {
+ foreach ($this->tableLinks as $index => $link) {
+ if ($link === $linkToRemove) {
+ unset($this->tableLinks[$index]);
+ }
+ }
+ }
+
+ /**
+ * @param string $baseColumn
+ * @param Joinable $joinable
+ *
+ * @return $this
+ */
+ public function addTableLink($baseColumn, Joinable $joinable) {
+ $target = $joinable->getTargetTable();
+ $targetCol = $joinable->getTargetColumn();
+ $alias = $joinable->getAlias();
+
+ if (!$this->hasLink($target, $targetCol, $alias)) {
+ if (!$joinable->getBaseTable()) {
+ $joinable->setBaseTable($this->getName());
+ }
+ if (!$joinable->getBaseColumn()) {
+ $joinable->setBaseColumn($baseColumn);
+ }
+ $this->tableLinks[] = $joinable;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param mixed $tableLinks
+ *
+ * @return $this
+ */
+ public function setTableLinks($tableLinks) {
+ $this->tableLinks = $tableLinks;
+
+ return $this;
+ }
+
+ /**
+ * @param $target
+ * @param $targetCol
+ * @param $alias
+ *
+ * @return bool
+ */
+ private function hasLink($target, $targetCol, $alias) {
+ foreach ($this->tableLinks as $link) {
+ if ($link->getTargetTable() === $target
+ && $link->getTargetColumn() === $targetCol
+ && $link->getAlias() === $alias
+ ) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php
new file mode 100644
index 00000000..2c689344
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Civi\Api4\Service\Spec;
+
+class CustomFieldSpec extends FieldSpec {
+ /**
+ * @var int
+ */
+ protected $customFieldId;
+
+ /**
+ * @var int
+ */
+ protected $customGroup;
+
+ /**
+ * @var string
+ */
+ protected $tableName;
+
+ /**
+ * @var string
+ */
+ protected $columnName;
+
+ /**
+ * @inheritDoc
+ */
+ public function setDataType($dataType) {
+ switch ($dataType) {
+ case 'ContactReference':
+ $this->setFkEntity('Contact');
+ $dataType = 'Integer';
+ break;
+
+ case 'File':
+ case 'StateProvince':
+ case 'Country':
+ $this->setFkEntity($dataType);
+ $dataType = 'Integer';
+ break;
+ }
+ return parent::setDataType($dataType);
+ }
+
+ /**
+ * @return int
+ */
+ public function getCustomFieldId() {
+ return $this->customFieldId;
+ }
+
+ /**
+ * @param int $customFieldId
+ *
+ * @return $this
+ */
+ public function setCustomFieldId($customFieldId) {
+ $this->customFieldId = $customFieldId;
+
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getCustomGroupName() {
+ return $this->customGroup;
+ }
+
+ /**
+ * @param string $customGroupName
+ *
+ * @return $this
+ */
+ public function setCustomGroupName($customGroupName) {
+ $this->customGroup = $customGroupName;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCustomTableName() {
+ return $this->tableName;
+ }
+
+ /**
+ * @param string $customFieldColumnName
+ *
+ * @return $this
+ */
+ public function setCustomTableName($customFieldColumnName) {
+ $this->tableName = $customFieldColumnName;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCustomFieldColumnName() {
+ return $this->columnName;
+ }
+
+ /**
+ * @param string $customFieldColumnName
+ *
+ * @return $this
+ */
+ public function setCustomFieldColumnName($customFieldColumnName) {
+ $this->columnName = $customFieldColumnName;
+
+ return $this;
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php
new file mode 100644
index 00000000..1db2941e
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php
@@ -0,0 +1,320 @@
+<?php
+
+namespace Civi\Api4\Service\Spec;
+
+use Civi\Api4\Utils\CoreUtil;
+
+class FieldSpec {
+ /**
+ * @var mixed
+ */
+ protected $defaultValue;
+
+ /**
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * @var string
+ */
+ protected $entity;
+
+ /**
+ * @var string
+ */
+ protected $description;
+
+ /**
+ * @var bool
+ */
+ protected $required = FALSE;
+
+ /**
+ * @var bool
+ */
+ protected $requiredIf;
+
+ /**
+ * @var array|boolean
+ */
+ protected $options;
+
+ /**
+ * @var string
+ */
+ protected $dataType;
+
+ /**
+ * @var string
+ */
+ protected $fkEntity;
+
+ /**
+ * @var int
+ */
+ protected $serialize;
+
+ /**
+ * Aliases for the valid data types
+ *
+ * @var array
+ */
+ public static $typeAliases = [
+ 'Int' => 'Integer',
+ 'Link' => 'Url',
+ 'Memo' => 'Text',
+ ];
+
+ /**
+ * @param string $name
+ * @param string $entity
+ * @param string $dataType
+ */
+ public function __construct($name, $entity, $dataType = 'String') {
+ $this->entity = $entity;
+ $this->setName($name);
+ $this->setDataType($dataType);
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getDefaultValue() {
+ return $this->defaultValue;
+ }
+
+ /**
+ * @param mixed $defaultValue
+ *
+ * @return $this
+ */
+ public function setDefaultValue($defaultValue) {
+ $this->defaultValue = $defaultValue;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name) {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * @param string $title
+ *
+ * @return $this
+ */
+ public function setTitle($title) {
+ $this->title = $title;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEntity() {
+ return $this->entity;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription() {
+ return $this->description;
+ }
+
+ /**
+ * @param string $description
+ *
+ * @return $this
+ */
+ public function setDescription($description) {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isRequired() {
+ return $this->required;
+ }
+
+ /**
+ * @param bool $required
+ *
+ * @return $this
+ */
+ public function setRequired($required) {
+ $this->required = $required;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function getRequiredIf() {
+ return $this->requiredIf;
+ }
+
+ /**
+ * @param bool $required
+ *
+ * @return $this
+ */
+ public function setRequiredIf($requiredIf) {
+ $this->requiredIf = $requiredIf;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDataType() {
+ return $this->dataType;
+ }
+
+ /**
+ * @param $dataType
+ *
+ * @return $this
+ * @throws \Exception
+ */
+ public function setDataType($dataType) {
+ if (array_key_exists($dataType, self::$typeAliases)) {
+ $dataType = self::$typeAliases[$dataType];
+ }
+
+ if (!in_array($dataType, $this->getValidDataTypes())) {
+ throw new \Exception(sprintf('Invalid data type "%s', $dataType));
+ }
+
+ $this->dataType = $dataType;
+
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getSerialize() {
+ return $this->serialize;
+ }
+
+ /**
+ * @param int|null $serialize
+ */
+ public function setSerialize($serialize) {
+ $this->serialize = $serialize;
+ }
+
+ /**
+ * Add valid types that are not not part of \CRM_Utils_Type::dataTypes
+ *
+ * @return array
+ */
+ private function getValidDataTypes() {
+ $extraTypes = ['Boolean', 'Text', 'Float', 'Url'];
+ $extraTypes = array_combine($extraTypes, $extraTypes);
+
+ return array_merge(\CRM_Utils_Type::dataTypes(), $extraTypes);
+ }
+
+ /**
+ * @return array
+ */
+ public function getOptions() {
+ if (!isset($this->options) || $this->options === TRUE) {
+ $fieldName = $this->getName();
+
+ if ($this instanceof CustomFieldSpec) {
+ // buildOptions relies on the custom_* type of field names
+ $fieldName = sprintf('custom_%d', $this->getCustomFieldId());
+ }
+
+ $dao = CoreUtil::getDAOFromApiName($this->getEntity());
+ $options = $dao::buildOptions($fieldName);
+
+ if (!is_array($options) || !$options) {
+ $options = FALSE;
+ }
+
+ $this->setOptions($options);
+ }
+ return $this->options;
+ }
+
+ /**
+ * @param array|bool $options
+ *
+ * @return $this
+ */
+ public function setOptions($options) {
+ $this->options = $options;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFkEntity() {
+ return $this->fkEntity;
+ }
+
+ /**
+ * @param string $fkEntity
+ *
+ * @return $this
+ */
+ public function setFkEntity($fkEntity) {
+ $this->fkEntity = $fkEntity;
+
+ return $this;
+ }
+
+ /**
+ * @param array $values
+ * @return array
+ */
+ public function toArray($values = []) {
+ $ret = [];
+ foreach (get_object_vars($this) as $key => $val) {
+ $key = strtolower(preg_replace('/(?=[A-Z])/', '_$0', $key));
+ if (!$values || in_array($key, $values)) {
+ $ret[$key] = $val;
+ }
+ }
+ return $ret;
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActionScheduleCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActionScheduleCreationSpecProvider.php
new file mode 100644
index 00000000..660bfec9
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActionScheduleCreationSpecProvider.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\FieldSpec;
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class ActionScheduleCreationSpecProvider implements SpecProviderInterface {
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('title')->setRequired(TRUE);
+ $spec->getFieldByName('mapping_id')->setRequired(TRUE);
+ $spec->getFieldByName('entity_value')->setRequired(TRUE);
+ $spec->getFieldByName('start_action_date')->setRequiredIf('empty($values.absolute_date)');
+ $spec->getFieldByName('absolute_date')->setRequiredIf('empty($values.start_action_date)');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return $entity === 'ActionSchedule' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php
new file mode 100644
index 00000000..dc254342
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\FieldSpec;
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class ActivityCreationSpecProvider implements SpecProviderInterface {
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $sourceContactField = new FieldSpec('source_contact_id', 'Activity', 'Integer');
+ $sourceContactField->setRequired(TRUE);
+ $sourceContactField->setFkEntity('Contact');
+
+ $spec->addFieldSpec($sourceContactField);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return $entity === 'Activity' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php
new file mode 100644
index 00000000..afba9c79
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\FieldSpec;
+use Civi\Api4\Service\Spec\RequestSpec;
+
+
+class AddressCreationSpecProvider implements SpecProviderInterface {
+
+ /**
+ * @param RequestSpec $spec
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('contact_id')->setRequired(TRUE);
+ $spec->getFieldByName('location_type_id')->setRequired(TRUE);
+ }
+
+ /**
+ * @param string $entity
+ * @param string $action
+ *
+ * @return bool
+ */
+ public function applies($entity, $action) {
+ return $entity === 'Address' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php
new file mode 100644
index 00000000..94c68d9d
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class ContactCreationSpecProvider implements SpecProviderInterface {
+
+ /**
+ * @param RequestSpec $spec
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('contact_type')
+ ->setRequired(TRUE)
+ ->setDefaultValue('Individual');
+
+ $spec->getFieldByName('is_opt_out')->setRequired(FALSE);
+ $spec->getFieldByName('is_deleted')->setRequired(FALSE);
+
+ }
+
+ /**
+ * @param string $entity
+ * @param string $action
+ *
+ * @return bool
+ */
+ public function applies($entity, $action) {
+ return $entity === 'Contact' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php
new file mode 100644
index 00000000..f55deb1c
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class ContactTypeCreationSpecProvider implements SpecProviderInterface {
+
+ /**
+ * @param RequestSpec $spec
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('label')->setRequired(TRUE);
+ $spec->getFieldByName('name')->setRequired(TRUE);
+ $spec->getFieldByName('parent_id')->setRequired(TRUE);
+
+ }
+
+ /**
+ * @param string $entity
+ * @param string $action
+ *
+ * @return bool
+ */
+ public function applies($entity, $action) {
+ return $entity === 'ContactType' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php
new file mode 100644
index 00000000..14861871
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class ContributionCreationSpecProvider implements SpecProviderInterface {
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('financial_type_id')->setRequired(TRUE);
+ $spec->getFieldByName('receive_date')->setDefaultValue('now');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return $entity === 'Contribution' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php
new file mode 100644
index 00000000..cd033754
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class CustomGroupCreationSpecProvider implements SpecProviderInterface {
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ return $spec->getFieldByName('extends')->setRequired(TRUE);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return $entity === 'CustomGroup' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php
new file mode 100644
index 00000000..cd82d438
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\FieldSpec;
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class CustomValueSpecProvider implements SpecProviderInterface {
+
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $action = $spec->getAction();
+ if ($action !== 'create') {
+ $idField = new FieldSpec('id', $spec->getEntity(), 'Integer');
+ $idField->setTitle(ts('Custom Value ID'));
+ $spec->addFieldSpec($idField);
+ }
+ $entityField = new FieldSpec('entity_id', $spec->getEntity(), 'Integer');
+ $entityField->setTitle(ts('Entity ID'));
+ $entityField->setRequired($action === 'create');
+ $entityField->setFkEntity('Contact');
+ $spec->addFieldSpec($entityField);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return strstr($entity, 'Custom_');
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php
new file mode 100644
index 00000000..136b0e54
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class EmailCreationSpecProvider implements SpecProviderInterface {
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('contact_id')->setRequired(TRUE);
+ $spec->getFieldByName('email')->setRequired(TRUE);
+ $spec->getFieldByName('on_hold')->setRequired(FALSE);
+ $spec->getFieldByName('is_bulkmail')->setRequired(FALSE);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return $entity === 'Email' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php
new file mode 100644
index 00000000..42b74a6f
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class EventCreationSpecProvider implements SpecProviderInterface {
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('is_template')->setRequired(FALSE);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return $entity === 'Event' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php
new file mode 100644
index 00000000..8af69a0a
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class GroupCreationSpecProvider implements SpecProviderInterface {
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('title')->setRequired(TRUE);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return $entity === 'Group' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php
new file mode 100644
index 00000000..7d5fc270
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class NavigationCreationSpecProvider implements SpecProviderInterface {
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('domain_id')->setRequired(FALSE);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return $entity === 'Navigation' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NoteCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NoteCreationSpecProvider.php
new file mode 100644
index 00000000..f12e592c
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NoteCreationSpecProvider.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+
+class NoteCreationSpecProvider implements SpecProviderInterface {
+
+ /**
+ * @param RequestSpec $spec
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('note')->setRequired(TRUE);
+ $spec->getFieldByName('entity_table')->setDefaultValue('civicrm_contact');
+ }
+
+ /**
+ * @param string $entity
+ * @param string $action
+ *
+ * @return bool
+ */
+ public function applies($entity, $action) {
+ return $entity === 'Note' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php
new file mode 100644
index 00000000..4ea634c1
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class OptionValueCreationSpecProvider implements SpecProviderInterface {
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('weight')->setRequired(FALSE);
+ $spec->getFieldByName('value')->setRequired(FALSE);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return $entity === 'OptionValue' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php
new file mode 100644
index 00000000..bb757d43
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class PhoneCreationSpecProvider implements SpecProviderInterface {
+ /**
+ * @inheritDoc
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $spec->getFieldByName('contact_id')->setRequired(TRUE);
+ $spec->getFieldByName('location_type_id')->setRequired(TRUE);
+ $spec->getFieldByName('phone')->setRequired(TRUE);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function applies($entity, $action) {
+ return $entity === 'Phone' && $action === 'create';
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php
new file mode 100644
index 00000000..8be77e68
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\RequestSpec;
+
+interface SpecProviderInterface {
+ /**
+ * @param RequestSpec $spec
+ *
+ * @return void
+ */
+ public function modifySpec(RequestSpec $spec);
+
+ /**
+ * @param string $entity
+ * @param string $action
+ *
+ * @return bool
+ */
+ public function applies($entity, $action);
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/RequestSpec.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/RequestSpec.php
new file mode 100644
index 00000000..9437d930
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/RequestSpec.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Civi\Api4\Service\Spec;
+
+class RequestSpec {
+
+ /**
+ * @var string
+ */
+ protected $entity;
+
+ /**
+ * @var string
+ */
+ protected $action;
+
+ /**
+ * @var FieldSpec[]
+ */
+ protected $fields = [];
+
+ /**
+ * @param string $entity
+ * @param string $action
+ */
+ public function __construct($entity, $action) {
+ $this->entity = $entity;
+ $this->action = $action;
+ }
+
+ public function addFieldSpec(FieldSpec $field) {
+ $this->fields[] = $field;
+ }
+
+ /**
+ * @param $name
+ *
+ * @return FieldSpec|null
+ */
+ public function getFieldByName($name) {
+ foreach ($this->fields as $field) {
+ if ($field->getName() === $name) {
+ return $field;
+ }
+ }
+
+ return NULL;
+ }
+
+ /**
+ * @return array
+ * Gets all the field names currently part of the specification
+ */
+ public function getFieldNames() {
+ return array_map(function(FieldSpec $field) {
+ return $field->getName();
+ }, $this->fields);
+ }
+
+ /**
+ * @return array|FieldSpec[]
+ */
+ public function getRequiredFields() {
+ return array_filter($this->fields, function (FieldSpec $field) {
+ return $field->isRequired();
+ });
+ }
+
+ /**
+ * @return array|FieldSpec[]
+ */
+ public function getConditionalRequiredFields() {
+ return array_filter($this->fields, function (FieldSpec $field) {
+ return $field->getRequiredIf();
+ });
+ }
+
+ /**
+ * @param array $fieldNames
+ * Optional array of fields to return
+ * @return FieldSpec[]
+ */
+ public function getFields($fieldNames = NULL) {
+ if (!$fieldNames) {
+ return $this->fields;
+ }
+ $fields = [];
+ foreach ($this->fields as $field) {
+ if (in_array($field->getName(), $fieldNames)) {
+ $fields[] = $field;
+ }
+ }
+ return $fields;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEntity() {
+ return $this->entity;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAction() {
+ return $this->action;
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php
new file mode 100644
index 00000000..c8e4d3da
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Civi\Api4\Service\Spec;
+
+use CRM_Utils_Array as ArrayHelper;
+use CRM_Core_DAO_AllCoreTables as TableHelper;
+
+class SpecFormatter {
+ /**
+ * @param FieldSpec[] $fields
+ * @param array $return
+ * @param bool $includeFieldOptions
+ *
+ * @return array
+ */
+ public static function specToArray($fields, $return = [], $includeFieldOptions = FALSE) {
+ $fieldArray = [];
+
+ foreach ($fields as $field) {
+ if ($includeFieldOptions || in_array('options', $return)) {
+ $field->getOptions();
+ }
+ $fieldArray[$field->getName()] = $field->toArray($return);
+ }
+
+ return $fieldArray;
+ }
+
+ /**
+ * @param array $data
+ * @param string $entity
+ *
+ * @return FieldSpec
+ */
+ public static function arrayToField(array $data, $entity) {
+ $dataTypeName = self::getDataType($data);
+
+ if (!empty($data['custom_group_id'])) {
+ $field = new CustomFieldSpec($data['name'], $entity, $dataTypeName);
+ if (strpos($entity, 'Custom_') !== 0) {
+ $field->setName($data['custom_group']['name'] . '.' . $data['name']);
+ }
+ else {
+ $field->setCustomTableName($data['custom_group']['table_name']);
+ $field->setCustomFieldColumnName($data['column_name']);
+ }
+ $field->setCustomFieldId(ArrayHelper::value('id', $data));
+ $field->setCustomGroupName($data['custom_group']['name']);
+ $field->setTitle(ArrayHelper::value('label', $data));
+ $field->setOptions(self::customFieldHasOptions($data));
+ if (\CRM_Core_BAO_CustomField::isSerialized($data)) {
+ $field->setSerialize(\CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND);
+ }
+ }
+ else {
+ $name = ArrayHelper::value('name', $data);
+ $field = new FieldSpec($name, $entity, $dataTypeName);
+ $field->setRequired((bool) ArrayHelper::value('required', $data, FALSE));
+ $field->setTitle(ArrayHelper::value('title', $data));
+ $field->setOptions(!empty($data['pseudoconstant']));
+ $field->setSerialize(ArrayHelper::value('serialize', $data));
+ }
+
+ $field->setDefaultValue(ArrayHelper::value('default', $data));
+ $field->setDescription(ArrayHelper::value('description', $data));
+
+ $fkAPIName = ArrayHelper::value('FKApiName', $data);
+ $fkClassName = ArrayHelper::value('FKClassName', $data);
+ if ($fkAPIName || $fkClassName) {
+ $field->setFkEntity($fkAPIName ?: TableHelper::getBriefName($fkClassName));
+ }
+
+ return $field;
+ }
+
+ /**
+ * Does this custom field have options
+ *
+ * @param array $field
+ * @return bool
+ */
+ private static function customFieldHasOptions($field) {
+ // This will include boolean fields with Yes/No options.
+ if (in_array($field['html_type'], ['Radio', 'CheckBox'])) {
+ return TRUE;
+ }
+ // Do this before the "Select" string search because date fields have a "Select Date" html_type
+ // and contactRef fields have an "Autocomplete-Select" html_type - contacts are an FK not an option list.
+ if (in_array($field['data_type'], ['ContactReference', 'Date'])) {
+ return FALSE;
+ }
+ if (strpos($field['html_type'], 'Select')) {
+ return TRUE;
+ }
+ return !empty($field['option_group_id']);
+ }
+
+ /**
+ * Get the data type from an array. Defaults to 'data_type' with fallback to
+ * mapping for the integer value 'type'
+ *
+ * @param array $data
+ *
+ * @return string
+ */
+ private static function getDataType(array $data) {
+ if (isset($data['data_type'])) {
+ return $data['data_type'];
+ }
+
+ $dataTypeInt = ArrayHelper::value('type', $data);
+ $dataTypeName = \CRM_Utils_Type::typeToString($dataTypeInt);
+
+ return $dataTypeName;
+ }
+
+}
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php
new file mode 100644
index 00000000..b1c83c89
--- /dev/null
+++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Civi\Api4\Service\Spec;
+
+use Civi\Api4\CustomField;
+use Civi\Api4\Service\Spec\Provider\SpecProviderInterface;
+use Civi\Api4\Utils\CoreUtil;
+
+class SpecGatherer {
+
+ /**
+ * @var SpecProviderInterface[]
+ */
+ protected $specProviders = [];
+
+ /**
+ * A cache of DAOs based on entity
+ *
+ * @var \CRM_Core_DAO[]
+ */
+ protected $DAONames;
+
+ /**
+ * Returns a RequestSpec with all the fields available. Uses spec providers
+ * to add or modify field specifications.
+ * For an example @see CustomFieldSpecProvider.
+ *
+ * @param string $entity
+ * @param string $action
+ * @param $includeCustom
+ *
+ * @return \Civi\Api4\Service\Spec\RequestSpec
+ */
+ public function getSpec($entity, $action, $includeCustom) {
+ $specification = new RequestSpec($entity, $action);
+
+ // Real entities
+ if (strpos($entity, 'Custom_') !== 0) {
+ $this->addDAOFields($entity, $action, $specification);
+ if ($includeCustom && array_key_exists($entity, \CRM_Core_SelectValues::customGroupExtends())) {
+ $this->addCustomFields($entity, $specification);
+ }
+ }
+ // Custom pseudo-entities
+ else {
+ $this->getCustomGroupFields(substr($entity, 7), $specification);
+ }
+
+ foreach ($this->specProviders as $provider) {
+ if ($provider->applies($entity, $action)) {
+ $provider->modifySpec($specification);
+ }
+ }
+
+ return $specification;
+ }
+
+ /**
+ * @param SpecProviderInterface $provider
+ */
+ public function addSpecProvider(SpecProviderInterface $provider) {
+ $this->specProviders[] = $provider;
+ }
+
+ /**
+ * @param string $entity
+ * @param RequestSpec $specification
+ */
+ private function addDAOFields($entity, $action, RequestSpec $specification) {
+ $DAOFields = $this->getDAOFields($entity);
+
+ foreach ($DAOFields as $DAOField) {
+ if ($DAOField['name'] == 'id' && $action == 'create') {
+ continue;
+ }
+ if ($action !== 'create' || isset($DAOField['default'])) {
+ $DAOField['required'] = FALSE;
+ }
+ if ($DAOField['name'] == 'is_active' && empty($DAOField['default'])) {
+ $DAOField['default'] = '1';
+ }
+ $field = SpecFormatter::arrayToField($DAOField, $entity);
+ $specification->addFieldSpec($field);
+ }
+ }
+
+ /**
+ * @param string $entity
+ * @param RequestSpec $specification
+ */
+ private function addCustomFields($entity, RequestSpec $specification) {
+ $extends = ($entity == 'Contact') ? ['Contact', 'Individual', 'Organization', 'Household'] : [$entity];
+ $customFields = CustomField::get()
+ ->addWhere('custom_group.extends', 'IN', $extends)
+ ->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value'])
+ ->execute();
+
+ foreach ($customFields as $fieldArray) {
+ $field = SpecFormatter::arrayToField($fieldArray, $entity);
+ $specification->addFieldSpec($field);
+ }
+ }
+
+ /**
+ * @param string $customGroup
+ * @param RequestSpec $specification
+ */
+ private function getCustomGroupFields($customGroup, RequestSpec $specification) {
+ $customFields = CustomField::get()
+ ->addWhere('custom_group.name', '=', $customGroup)
+ ->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value', 'custom_group.table_name', 'column_name'])
+ ->execute();
+
+ foreach ($customFields as $fieldArray) {
+ $field = SpecFormatter::arrayToField($fieldArray, 'Custom_' . $customGroup);
+ $specification->addFieldSpec($field);
+ }
+ }
+
+ /**
+ * @param string $entityName
+ *
+ * @return array
+ */
+ private function getDAOFields($entityName) {
+ $dao = CoreUtil::getDAOFromApiName($entityName);
+
+ return $dao::fields();
+ }
+
+}