diff --git a/config/system.php b/config/system.php index fb5bc4b..f78c16c 100644 --- a/config/system.php +++ b/config/system.php @@ -35,6 +35,43 @@ return [ //'cache.persistent' => 'file', //'cache.blob' => 'file', + // Logging Configuration + 'log' => [ + // Driver: 'file' | 'systemd' | 'syslog' | 'null' + // file - writes JSONL to a local file (see 'path' / 'channel' below) + // systemd - writes to stderr with prefix; journald/systemd parses this natively + // syslog - writes via PHP syslog() (see 'ident' / 'facility' below) + // null - discards all log messages (useful in tests / CI) + 'driver' => 'file', + + // Minimum PSR-3 log level to record. + // Messages below this severity are silently discarded. + // From most to least severe: emergency, alert, critical, error, warning, notice, info, debug + 'level' => 'warning', + + // Per-tenant log files. + // When true, messages are written to: + // {path}/tenant/{tenantIdentifier}/{channel}.jsonl + // once a tenant session is active. Pre-tenant messages (boot phase, + // unknown domain rejections, etc.) fall through to the global log file. + 'per_tenant' => false, + + // ── file driver options ──────────────────────────────────────────────── + // Absolute path to the log directory. null = /var/log + 'path' => null, + + // Log channel — used as the filename without extension. + // 'app' → var/log/app.jsonl (or var/log/tenant/{id}/app.jsonl when per_tenant = true) + 'channel' => 'app', + + // ── syslog driver options ────────────────────────────────────────────── + // Identity tag passed to openlog(); visible in /var/log/syslog and journalctl -t ktrix + 'ident' => 'ktrix', + + // openlog() facility constant. Common values: LOG_USER, LOG_LOCAL0 … LOG_LOCAL7 + 'facility' => LOG_USER, + ], + // Security Configuration 'security.salt' => 'a5418ed8c120b9d12c793ccea10571b74d0dcd4a4db7ca2f75e80fbdafb2bd9b', ]; diff --git a/core/lib/Kernel.php b/core/lib/Kernel.php index c63acbe..c50f8f7 100644 --- a/core/lib/Kernel.php +++ b/core/lib/Kernel.php @@ -21,7 +21,8 @@ use KTXC\Injection\Container; use Psr\Container\ContainerInterface; use KTXC\Module\ModuleManager; use Psr\Log\LoggerInterface; -use KTXC\Logger\FileLogger; +use KTXC\Logger\LoggerFactory; +use KTXC\Logger\TenantAwareLogger; use KTXF\Event\EventBus; use KTXF\Cache\EphemeralCacheInterface; use KTXF\Cache\PersistentCacheInterface; @@ -87,10 +88,8 @@ class Kernel $_SERVER['SHELL_VERBOSITY'] = 3; } - // Create logger with config support - $logDir = $this->config['log.directory'] ?? $this->getLogDir(); - $logChannel = $this->config['log.channel'] ?? 'app'; - $this->logger = new FileLogger($logDir, $logChannel); + // Create logger from config (driver + level-filter; per-tenant wrapping applied later in DI) + $this->logger = LoggerFactory::create($this->config, $this->folderRoot()); $this->initializeErrorHandlers(); @@ -396,8 +395,23 @@ class Kernel // Without this alias, PHP-DI will happily autowire a new empty Container when asked Container::class => \DI\get(ContainerInterface::class), - // Use the kernel's logger instance - LoggerInterface::class => \DI\value($this->logger), + LoggerInterface::class => function (ContainerInterface $c) use ($projectDir) { + $logConfig = $this->config['log'] ?? []; + + $logDir = $logConfig['path'] ?? ($projectDir . '/var/log'); + $channel = $logConfig['channel'] ?? 'app'; + $level = $logConfig['level'] ?? 'debug'; + $perTenant = (bool) ($logConfig['per_tenant'] ?? false); + + return new TenantAwareLogger( + $this->logger, + $c->get(SessionTenant::class), + $logDir, + $channel, + $level, + $perTenant, + ); + }, // EventBus as singleton for consistent event handling EventBus::class => \DI\create(EventBus::class), diff --git a/core/lib/Logger/FileLogger.php b/core/lib/Logger/FileLogger.php index 4662079..447fd80 100644 --- a/core/lib/Logger/FileLogger.php +++ b/core/lib/Logger/FileLogger.php @@ -40,11 +40,18 @@ class FileLogger implements LoggerInterface public function log($level, $message, array $context = []): void { - $timestamp = $this->formatTimestamp(); - $interpolated = $this->interpolate((string)$message, $context); + // Extract tenant id injected by TenantAwareLogger; default to 'system'. + $tenantId = isset($context['__tenant']) && is_string($context['__tenant']) + ? $context['__tenant'] + : 'system'; + unset($context['__tenant']); + + $timestamp = $this->formatTimestamp(); + $interpolated = $this->interpolate((string) $message, $context); $payload = [ - 'time' => $timestamp, - 'level' => strtolower((string)$level), + 'time' => $timestamp, + 'level' => strtolower((string) $level), + 'tenant' => $tenantId, 'channel' => $this->channel, 'message' => $interpolated, 'context' => $this->sanitizeContext($context), @@ -53,11 +60,12 @@ class FileLogger implements LoggerInterface if ($json === false) { // Fallback stringify if encoding fails (should be rare) $json = json_encode([ - 'time' => $timestamp, - 'level' => strtolower((string)$level), - 'channel' => $this->channel, - 'message' => $interpolated, - 'context_error' => 'failed to encode context: '.json_last_error_msg(), + 'time' => $timestamp, + 'level' => strtolower((string) $level), + 'tenant' => $tenantId, + 'channel' => $this->channel, + 'message' => $interpolated, + 'context_error' => 'failed to encode context: ' . json_last_error_msg(), ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '{"error":"logging failure"}'; } $this->write($json); diff --git a/core/lib/Logger/LevelFilterLogger.php b/core/lib/Logger/LevelFilterLogger.php new file mode 100644 index 0000000..3d73104 --- /dev/null +++ b/core/lib/Logger/LevelFilterLogger.php @@ -0,0 +1,47 @@ + alert(1) > critical(2) > error(3) > warning(4) > notice(5) > info(6) > debug(7) + * + * Example: minLevel = 'warning' passes emergency, alert, critical, error, warning + * and silently discards notice, info, debug. + */ +class LevelFilterLogger implements LoggerInterface +{ + private int $minSeverity; + + public function __construct( + private readonly LoggerInterface $inner, + string $minLevel = LogLevel::DEBUG, + ) { + LogLevelSeverity::validate($minLevel); + $this->minSeverity = LogLevelSeverity::severity($minLevel); + } + + public function emergency($message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } + public function alert($message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } + public function critical($message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); } + public function error($message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); } + public function warning($message, array $context = []): void { $this->log(LogLevel::WARNING, $message, $context); } + public function notice($message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); } + public function info($message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); } + public function debug($message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); } + + public function log($level, $message, array $context = []): void + { + // Messages with severity numerically greater than minSeverity are less severe — discard them. + if (LogLevelSeverity::severity((string) $level) > $this->minSeverity) { + return; + } + + $this->inner->log($level, $message, $context); + } +} diff --git a/core/lib/Logger/LogLevelSeverity.php b/core/lib/Logger/LogLevelSeverity.php new file mode 100644 index 0000000..62e856e --- /dev/null +++ b/core/lib/Logger/LogLevelSeverity.php @@ -0,0 +1,68 @@ + 0, + LogLevel::ALERT => 1, + LogLevel::CRITICAL => 2, + LogLevel::ERROR => 3, + LogLevel::WARNING => 4, + LogLevel::NOTICE => 5, + LogLevel::INFO => 6, + LogLevel::DEBUG => 7, + ]; + + /** + * Returns the integer severity for a PSR-3 level string. + * + * @throws \InvalidArgumentException for unknown level strings + */ + public static function severity(string $level): int + { + $normalized = strtolower($level); + + if (!array_key_exists($normalized, self::MAP)) { + throw new \InvalidArgumentException( + sprintf( + 'Unknown log level "%s". Valid levels are: %s.', + $level, + implode(', ', array_keys(self::MAP)) + ) + ); + } + + return self::MAP[$normalized]; + } + + /** + * Validates that a level string is a known PSR-3 level. + * + * @throws \InvalidArgumentException for unknown level strings + */ + public static function validate(string $level): void + { + self::severity($level); // throws on unknown + } + + /** + * Returns all valid PSR-3 level strings ordered from most to least severe. + * + * @return string[] + */ + public static function levels(): array + { + return array_keys(self::MAP); + } +} diff --git a/core/lib/Logger/LoggerFactory.php b/core/lib/Logger/LoggerFactory.php new file mode 100644 index 0000000..9362f42 --- /dev/null +++ b/core/lib/Logger/LoggerFactory.php @@ -0,0 +1,87 @@ + self::buildFileLogger($logConfig, $projectDir, $channel), + 'systemd' => new SystemdLogger($channel), + 'syslog' => new SyslogLogger( + $logConfig['ident'] ?? 'ktrix', + $logConfig['facility'] ?? LOG_USER, + $channel, + ), + 'null' => new NullLogger(), + default => throw new \RuntimeException( + sprintf( + 'Unknown log driver "%s". Supported drivers: file, systemd, syslog, null.', + $driver + ) + ), + }; + + return new LevelFilterLogger($inner, $level); + } + + private static function buildFileLogger(array $logConfig, string $projectDir, string $channel): FileLogger + { + $path = $logConfig['path'] ?? null; + + if ($path === null || $path === '') { + $path = $projectDir . '/var/log'; + } + + return new FileLogger($path, $channel); + } +} diff --git a/core/lib/Logger/SyslogLogger.php b/core/lib/Logger/SyslogLogger.php new file mode 100644 index 0000000..4c727fa --- /dev/null +++ b/core/lib/Logger/SyslogLogger.php @@ -0,0 +1,94 @@ + LOG_EMERG, + LogLevel::ALERT => LOG_ALERT, + LogLevel::CRITICAL => LOG_CRIT, + LogLevel::ERROR => LOG_ERR, + LogLevel::WARNING => LOG_WARNING, + LogLevel::NOTICE => LOG_NOTICE, + LogLevel::INFO => LOG_INFO, + LogLevel::DEBUG => LOG_DEBUG, + ]; + + public function __construct( + private readonly string $ident = 'ktrix', + private readonly int $facility = LOG_USER, + private readonly string $channel = 'app', + ) {} + + public function emergency($message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } + public function alert($message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } + public function critical($message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); } + public function error($message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); } + public function warning($message, array $context = []): void { $this->log(LogLevel::WARNING, $message, $context); } + public function notice($message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); } + public function info($message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); } + public function debug($message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); } + + public function log($level, $message, array $context = []): void + { + // Extract tenant id injected by TenantAwareLogger; default to 'system'. + $tenantId = isset($context['__tenant']) && is_string($context['__tenant']) + ? $context['__tenant'] + : 'system'; + unset($context['__tenant']); + + $level = strtolower((string) $level); + $priority = self::PRIORITY[$level] ?? LOG_DEBUG; + + $interpolated = $this->interpolate((string) $message, $context); + $contextStr = empty($context) ? '' : ' ' . $this->encodeContext($context); + + $entry = sprintf('[%s] [%s] %s%s', $this->channel, $tenantId, $interpolated, $contextStr); + + openlog($this->ident, LOG_NDELAY | LOG_PID, $this->facility); + syslog($priority, $entry); + closelog(); + } + + private function interpolate(string $message, array $context): string + { + if (!str_contains($message, '{')) { + return $message; + } + $replace = []; + foreach ($context as $key => $val) { + if (!is_array($val) && !is_object($val)) { + $replace['{' . $key . '}'] = (string) $val; + } + } + return strtr($message, $replace); + } + + private function encodeContext(array $context): string + { + $clean = []; + foreach ($context as $k => $v) { + if ($v instanceof \Throwable) { + $clean[$k] = ['type' => get_class($v), 'message' => $v->getMessage()]; + } elseif (is_resource($v)) { + $clean[$k] = 'resource(' . get_resource_type($v) . ')'; + } elseif (is_object($v)) { + $clean[$k] = method_exists($v, '__toString') ? (string) $v : ['object' => get_class($v)]; + } else { + $clean[$k] = $v; + } + } + return json_encode($clean, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '{}'; + } +} diff --git a/core/lib/Logger/SystemdLogger.php b/core/lib/Logger/SystemdLogger.php new file mode 100644 index 0000000..d35128c --- /dev/null +++ b/core/lib/Logger/SystemdLogger.php @@ -0,0 +1,112 @@ +" prefix and maps it to the corresponding + * log priority, so log entries appear in the journal with the correct severity. + * + * Format: LEVEL [channel] interpolated-message {"context":"key",...} + */ +class SystemdLogger implements LoggerInterface +{ + /** Maps PSR-3 levels to RFC 5424 / syslog priority numbers */ + private const PRIORITY = [ + LogLevel::EMERGENCY => 0, + LogLevel::ALERT => 1, + LogLevel::CRITICAL => 2, + LogLevel::ERROR => 3, + LogLevel::WARNING => 4, + LogLevel::NOTICE => 5, + LogLevel::INFO => 6, + LogLevel::DEBUG => 7, + ]; + + /** @var resource */ + private $stderr; + + public function __construct(private readonly string $channel = 'app') + { + $this->stderr = fopen('php://stderr', 'w'); + } + + public function __destruct() + { + if (is_resource($this->stderr)) { + fclose($this->stderr); + } + } + + public function emergency($message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } + public function alert($message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } + public function critical($message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); } + public function error($message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); } + public function warning($message, array $context = []): void { $this->log(LogLevel::WARNING, $message, $context); } + public function notice($message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); } + public function info($message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); } + public function debug($message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); } + + public function log($level, $message, array $context = []): void + { + // Extract tenant id injected by TenantAwareLogger; default to 'system'. + $tenantId = isset($context['__tenant']) && is_string($context['__tenant']) + ? $context['__tenant'] + : 'system'; + unset($context['__tenant']); + + $level = strtolower((string) $level); + $priority = self::PRIORITY[$level] ?? 7; + + $interpolated = $this->interpolate((string) $message, $context); + $contextStr = empty($context) ? '' : ' ' . $this->encodeContext($context); + + $line = sprintf( + "<%d>%s [%s] [%s] %s%s\n", + $priority, + strtoupper($level), + $this->channel, + $tenantId, + $interpolated, + $contextStr, + ); + + fwrite($this->stderr, $line); + } + + private function interpolate(string $message, array $context): string + { + if (!str_contains($message, '{')) { + return $message; + } + $replace = []; + foreach ($context as $key => $val) { + if (!is_array($val) && !is_object($val)) { + $replace['{' . $key . '}'] = (string) $val; + } + } + return strtr($message, $replace); + } + + private function encodeContext(array $context): string + { + $clean = []; + foreach ($context as $k => $v) { + if ($v instanceof \Throwable) { + $clean[$k] = ['type' => get_class($v), 'message' => $v->getMessage()]; + } elseif (is_resource($v)) { + $clean[$k] = 'resource(' . get_resource_type($v) . ')'; + } elseif (is_object($v)) { + $clean[$k] = method_exists($v, '__toString') ? (string) $v : ['object' => get_class($v)]; + } else { + $clean[$k] = $v; + } + } + return json_encode($clean, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '{}'; + } +} diff --git a/core/lib/Logger/TenantAwareLogger.php b/core/lib/Logger/TenantAwareLogger.php new file mode 100644 index 0000000..e147aea --- /dev/null +++ b/core/lib/Logger/TenantAwareLogger.php @@ -0,0 +1,93 @@ + Per-tenant logger cache, keyed by tenant identifier. */ + private array $tenantLoggers = []; + + /** + * @param LoggerInterface $globalLogger Fallback logger (also used when perTenant = false). + * @param SessionTenant $sessionTenant Live reference configured by TenantMiddleware. + * @param string $logDir Base log directory (e.g. /var/www/app/var/log). + * @param string $channel Log file basename (e.g. 'app' → app.jsonl). + * @param string $minLevel Minimum PSR-3 level for lazily-created per-tenant loggers. + * @param bool $perTenant When true, route tenant writes to per-tenant files. + */ + public function __construct( + private readonly LoggerInterface $globalLogger, + private readonly SessionTenant $sessionTenant, + private readonly string $logDir, + private readonly string $channel = 'app', + private readonly string $minLevel = 'debug', + private readonly bool $perTenant = false, + ) { + LogLevelSeverity::validate($minLevel); + } + + public function emergency($message, array $context = []): void { $this->log('emergency', $message, $context); } + public function alert($message, array $context = []): void { $this->log('alert', $message, $context); } + public function critical($message, array $context = []): void { $this->log('critical', $message, $context); } + public function error($message, array $context = []): void { $this->log('error', $message, $context); } + public function warning($message, array $context = []): void { $this->log('warning', $message, $context); } + public function notice($message, array $context = []): void { $this->log('notice', $message, $context); } + public function info($message, array $context = []): void { $this->log('info', $message, $context); } + public function debug($message, array $context = []): void { $this->log('debug', $message, $context); } + + public function log($level, $message, array $context = []): void + { + // Resolve current tenant id; fall back to 'system' for CLI / boot phase. + $tenantId = 'system'; + if ($this->sessionTenant->configured()) { + $tenantId = $this->sessionTenant->identifier() ?? 'system'; + } + + // Inject tenant id as a reserved context key that concrete loggers extract. + $context['__tenant'] = $tenantId; + + $this->resolveLogger($tenantId)->log($level, $message, $context); + } + + /** + * Returns the logger to write to for the given tenant. + * When per-tenant routing is disabled (or tenant is "system"), always returns the global logger. + */ + private function resolveLogger(string $tenantId): LoggerInterface + { + if (!$this->perTenant || $tenantId === 'system') { + return $this->globalLogger; + } + + if (!isset($this->tenantLoggers[$tenantId])) { + $tenantLogDir = rtrim($this->logDir, '/') . '/tenant/' . $tenantId; + $this->tenantLoggers[$tenantId] = new LevelFilterLogger( + new FileLogger($tenantLogDir, $this->channel), + $this->minLevel, + ); + } + + return $this->tenantLoggers[$tenantId]; + } +} diff --git a/shared/lib/Chrono/Collection/CollectionMutableAbstract.php b/shared/lib/Chrono/Collection/CollectionMutableAbstract.php index 77d3ac1..1af83d6 100644 --- a/shared/lib/Chrono/Collection/CollectionMutableAbstract.php +++ b/shared/lib/Chrono/Collection/CollectionMutableAbstract.php @@ -40,11 +40,7 @@ abstract class CollectionMutableAbstract extends NodeMutableAbstract implements // Copy all property values $this->properties->setLabel($value->getLabel()); - $this->properties->setDescription($value->getDescription()); - $this->properties->setPriority($value->getPriority()); - $this->properties->setVisibility($value->getVisibility()); - $this->properties->setColor($value->getColor()); - + return $this; } } diff --git a/shared/lib/Chrono/Provider/ProviderServiceTestInterface.php b/shared/lib/Chrono/Provider/ProviderServiceTestInterface.php index 32c5d72..39613d5 100644 --- a/shared/lib/Chrono/Provider/ProviderServiceTestInterface.php +++ b/shared/lib/Chrono/Provider/ProviderServiceTestInterface.php @@ -9,7 +9,7 @@ declare(strict_types=1); namespace KTXF\Chrono\Provider; -use KTXF\Mail\Service\ServiceBaseInterface; +use KTXF\Chrono\Service\ServiceBaseInterface; /** * Chrono Provider Service Test Interface diff --git a/shared/lib/Files/Provider/IProviderBase.php b/shared/lib/Files/Provider/IProviderBase.php deleted file mode 100644 index 169d7f1..0000000 --- a/shared/lib/Files/Provider/IProviderBase.php +++ /dev/null @@ -1,16 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXF\Files\Provider; - -use KTXF\Resource\Provider\ResourceProviderInterface; - -interface IProviderBase extends ResourceProviderInterface { - -} diff --git a/shared/lib/Files/Service/IServiceBase.php b/shared/lib/Files/Service/IServiceBase.php deleted file mode 100644 index 92143e7..0000000 --- a/shared/lib/Files/Service/IServiceBase.php +++ /dev/null @@ -1,333 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXF\Files\Service; - -use JsonSerializable; -use KTXF\Files\Node\INodeBase; -use KTXF\Files\Node\INodeCollectionBase; -use KTXF\Files\Node\INodeEntityBase; -use KTXF\Resource\Filter\IFilter; -use KTXF\Resource\Range\IRange; -use KTXF\Resource\Range\RangeType; -use KTXF\Resource\Sort\ISort; - -interface IServiceBase extends JsonSerializable { - - // Collection Capabilities - public const CAPABILITY_COLLECTION_LIST = 'CollectionList'; - public const CAPABILITY_COLLECTION_LIST_FILTER = 'CollectionListFilter'; - public const CAPABILITY_COLLECTION_LIST_SORT = 'CollectionListSort'; - public const CAPABILITY_COLLECTION_EXTANT = 'CollectionExtant'; - public const CAPABILITY_COLLECTION_FETCH = 'CollectionFetch'; - - // Entity Capabilities - public const CAPABILITY_ENTITY_LIST = 'EntityList'; - public const CAPABILITY_ENTITY_LIST_FILTER = 'EntityListFilter'; - public const CAPABILITY_ENTITY_LIST_SORT = 'EntityListSort'; - public const CAPABILITY_ENTITY_LIST_RANGE = 'EntityListRange'; - public const CAPABILITY_ENTITY_DELTA = 'EntityDelta'; - public const CAPABILITY_ENTITY_EXTANT = 'EntityExtant'; - public const CAPABILITY_ENTITY_FETCH = 'EntityFetch'; - public const CAPABILITY_ENTITY_READ = 'EntityRead'; - public const CAPABILITY_ENTITY_READ_STREAM = 'EntityReadStream'; - public const CAPABILITY_ENTITY_READ_CHUNK = 'EntityReadChunk'; - - // Node Capabilities (recursive/unified) - public const CAPABILITY_NODE_LIST = 'NodeList'; - public const CAPABILITY_NODE_LIST_FILTER = 'NodeListFilter'; - public const CAPABILITY_NODE_LIST_SORT = 'NodeListSort'; - public const CAPABILITY_NODE_LIST_RANGE = 'NodeListRange'; - public const CAPABILITY_NODE_DELTA = 'NodeDelta'; - - // JSON Constants - public const JSON_TYPE = 'files.service'; - public const JSON_PROPERTY_TYPE = '@type'; - public const JSON_PROPERTY_PROVIDER = 'provider'; - public const JSON_PROPERTY_ID = 'id'; - public const JSON_PROPERTY_LABEL = 'label'; - public const JSON_PROPERTY_CAPABILITIES = 'capabilities'; - public const JSON_PROPERTY_ENABLED = 'enabled'; - - /** - * Confirms if specific capability is supported - * - * @since 2025.11.01 - * - * @param string $value required ability e.g. 'EntityList' - * - * @return bool - */ - public function capable(string $value): bool; - - /** - * Lists all supported capabilities - * - * @since 2025.11.01 - * - * @return array - */ - public function capabilities(): array; - - /** - * Unique identifier of the provider this service belongs to - * - * @since 2025.11.01 - */ - public function in(): string; - - /** - * Unique arbitrary text string identifying this service (e.g. 1 or service1 or anything else) - * - * @since 2025.11.01 - */ - public function id(): string|int; - - /** - * Gets the localized human friendly name of this service (e.g. ACME Company File Service) - * - * @since 2025.11.01 - */ - public function getLabel(): string; - - /** - * Gets the active status of this service - * - * @since 2025.11.01 - */ - public function getEnabled(): bool; - - // ==================== Collection Methods ==================== - - /** - * List of accessible collections at a specific location - * - * @since 2025.11.01 - * - * @param string|int|null $location Parent collection identifier, null for root - * - * @return array - */ - public function collectionList(string|int|null $location = null, ?IFilter $filter = null, ?ISort $sort = null): array; - - /** - * Fresh filter for collection list - * - * @since 2025.11.01 - */ - public function collectionListFilter(): IFilter; - - /** - * Fresh sort for collection list - * - * @since 2025.11.01 - */ - public function collectionListSort(): ISort; - - /** - * Confirms if specific collection exists - * - * @since 2025.11.01 - * - * @param string|int|null $identifier Collection identifier - */ - public function collectionExtant(string|int|null $identifier): bool; - - /** - * Fetches details about a specific collection - * - * @since 2025.11.01 - * - * @param string|int|null $identifier Collection identifier - */ - public function collectionFetch(string|int|null $identifier): ?INodeCollectionBase; - - // ==================== Entity Methods ==================== - - /** - * Lists all entities in a specific collection - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * - * @return array - */ - public function entityList(string|int|null $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): array; - - /** - * Fresh filter for entity list - * - * @since 2025.11.01 - */ - public function entityListFilter(): IFilter; - - /** - * Fresh sort for entity list - * - * @since 2025.11.01 - */ - public function entityListSort(): ISort; - - /** - * Fresh range for entity list - * - * @since 2025.11.01 - */ - public function entityListRange(RangeType $type): IRange; - - /** - * Lists all changes from a specific signature - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * @param string $signature Sync token signature - * @param string $detail Detail level: ids | meta | full - * - * @return array{ - * added: array, - * updated: array, - * deleted: array, - * signature: string - * } - */ - public function entityDelta(string|int|null $collection, string $signature, string $detail = 'ids'): array; - - /** - * Confirms if specific entities exist in a collection - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * @param string|int ...$identifiers Entity identifiers - * - * @return array - */ - public function entityExtant(string|int|null $collection, string|int ...$identifiers): array; - - /** - * Fetches details about specific entities in a collection - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * @param string|int ...$identifiers Entity identifiers - * - * @return array - */ - public function entityFetch(string|int|null $collection, string|int ...$identifiers): array; - - /** - * Reads the entire content of an entity as a string - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * @param string|int $identifier Entity identifier - * - * @return string|null File content or null if not found - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function entityRead(string|int|null $collection, string|int $identifier): ?string; - - /** - * Opens a stream to read the content of an entity - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * @param string|int $identifier Entity identifier - * - * @return resource|null Stream resource or null if not found - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function entityReadStream(string|int|null $collection, string|int $identifier); - - /** - * Reads a chunk of content from an entity - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * @param string|int $identifier Entity identifier - * @param int $offset Starting byte position (0-indexed) - * @param int $length Number of bytes to read - * - * @return string|null Chunk content or null if not found - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function entityReadChunk(string|int|null $collection, string|int $identifier, int $offset, int $length): ?string; - - // ==================== Node Methods (Recursive/Unified) ==================== - - /** - * Lists all nodes (collections and entities) at a location, optionally recursive - * Returns a flat list with parent references via in() - * - * @since 2025.11.01 - * - * @param string|int|null $location Starting location, null for root - * @param bool $recursive Whether to list recursively - * - * @return array - */ - public function nodeList(string|int|null $location = null, bool $recursive = false, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): array; - - /** - * Fresh filter for node list - * - * @since 2025.11.01 - */ - public function nodeListFilter(): IFilter; - - /** - * Fresh sort for node list - * - * @since 2025.11.01 - */ - public function nodeListSort(): ISort; - - /** - * Fresh range for node list - * - * @since 2025.11.01 - */ - public function nodeListRange(RangeType $type): IRange; - - /** - * Lists all node changes from a specific signature, optionally recursive - * Returns flat list with parent references - * - * @since 2025.11.01 - * - * @param string|int|null $location Starting location, null for root - * @param string $signature Sync token signature - * @param bool $recursive Whether to include recursive changes - * @param string $detail Detail level: ids | meta | full - * - * @return array{ - * added: array|array, - * updated: array|array, - * deleted: array, - * signature: string - * } - */ - public function nodeDelta(string|int|null $location, string $signature, bool $recursive = false, string $detail = 'ids'): array; - -} diff --git a/shared/lib/Files/Service/IServiceCollectionMutable.php b/shared/lib/Files/Service/IServiceCollectionMutable.php deleted file mode 100644 index 6b0cf4e..0000000 --- a/shared/lib/Files/Service/IServiceCollectionMutable.php +++ /dev/null @@ -1,100 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXF\Files\Service; - -use KTXF\Files\Node\INodeCollectionBase; -use KTXF\Files\Node\INodeCollectionMutable; - -interface IServiceCollectionMutable extends IServiceBase { - - public const CAPABILITY_COLLECTION_CREATE = 'CollectionCreate'; - public const CAPABILITY_COLLECTION_MODIFY = 'CollectionModify'; - public const CAPABILITY_COLLECTION_DESTROY = 'CollectionDestroy'; - public const CAPABILITY_COLLECTION_COPY = 'CollectionCopy'; - public const CAPABILITY_COLLECTION_MOVE = 'CollectionMove'; - - /** - * Creates a new, empty collection node - * - * @since 2025.11.01 - */ - public function collectionFresh(): INodeCollectionMutable; - - /** - * Creates a new collection at the specified location - * - * @since 2025.11.01 - * - * @param string|int|null $location Parent collection, null for root - * @param INodeCollectionMutable $collection The collection to create - * @param array $options Additional options - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function collectionCreate(string|int|null $location, INodeCollectionMutable $collection, array $options = []): INodeCollectionBase; - - /** - * Modifies an existing collection - * - * @since 2025.11.01 - * - * @param string|int $identifier Collection identifier - * @param INodeCollectionMutable $collection The collection with modifications - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function collectionModify(string|int $identifier, INodeCollectionMutable $collection): INodeCollectionBase; - - /** - * Destroys an existing collection - * - * @since 2025.11.01 - * - * @param string|int $identifier Collection identifier - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function collectionDestroy(string|int $identifier): bool; - - /** - * Copies an existing collection to a new location - * - * @since 2025.11.01 - * - * @param string|int $identifier Collection identifier - * @param string|int|null $location Destination parent collection, null for root - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function collectionCopy(string|int $identifier, string|int|null $location): INodeCollectionBase; - - /** - * Moves an existing collection to a new location - * - * @since 2025.11.01 - * - * @param string|int $identifier Collection identifier - * @param string|int|null $location Destination parent collection, null for root - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function collectionMove(string|int $identifier, string|int|null $location): INodeCollectionBase; - -} diff --git a/shared/lib/Files/Service/IServiceEntityMutable.php b/shared/lib/Files/Service/IServiceEntityMutable.php deleted file mode 100644 index e505335..0000000 --- a/shared/lib/Files/Service/IServiceEntityMutable.php +++ /dev/null @@ -1,158 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXF\Files\Service; - -use KTXF\Files\Node\INodeEntityBase; -use KTXF\Files\Node\INodeEntityMutable; - -interface IServiceEntityMutable extends IServiceBase { - - public const CAPABILITY_ENTITY_CREATE = 'EntityCreate'; - public const CAPABILITY_ENTITY_MODIFY = 'EntityModify'; - public const CAPABILITY_ENTITY_DESTROY = 'EntityDestroy'; - public const CAPABILITY_ENTITY_COPY = 'EntityCopy'; - public const CAPABILITY_ENTITY_MOVE = 'EntityMove'; - public const CAPABILITY_ENTITY_WRITE = 'EntityWrite'; - public const CAPABILITY_ENTITY_WRITE_STREAM = 'EntityWriteStream'; - public const CAPABILITY_ENTITY_WRITE_CHUNK = 'EntityWriteChunk'; - - /** - * Creates a new, empty entity node - * - * @since 2025.11.01 - */ - public function entityFresh(): INodeEntityMutable; - - /** - * Creates a new entity in the specified collection - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier, null for root - * @param INodeEntityMutable $entity The entity to create - * @param array $options Additional options - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function entityCreate(string|int|null $collection, INodeEntityMutable $entity, array $options = []): INodeEntityBase; - - /** - * Modifies an existing entity in the specified collection - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * @param string|int $identifier Entity identifier - * @param INodeEntityMutable $entity The entity with modifications - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function entityModify(string|int|null $collection, string|int $identifier, INodeEntityMutable $entity): INodeEntityBase; - - /** - * Destroys an existing entity in the specified collection - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * @param string|int $identifier Entity identifier - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function entityDestroy(string|int|null $collection, string|int $identifier): bool; - - /** - * Copies an existing entity to a new collection - * - * @since 2025.11.01 - * - * @param string|int|null $collection Source collection identifier - * @param string|int $identifier Entity identifier - * @param string|int|null $destination Destination collection identifier, null for root - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function entityCopy(string|int|null $collection, string|int $identifier, string|int|null $destination): INodeEntityBase; - - /** - * Moves an existing entity to a new collection - * - * @since 2025.11.01 - * - * @param string|int|null $collection Source collection identifier - * @param string|int $identifier Entity identifier - * @param string|int|null $destination Destination collection identifier, null for root - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function entityMove(string|int|null $collection, string|int $identifier, string|int|null $destination): INodeEntityBase; - - /** - * Writes the entire content of an entity from a string - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * @param string|int $identifier Entity identifier - * @param string $data Content to write - * - * @return int Number of bytes written - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function entityWrite(string|int|null $collection, string|int $identifier, string $data): int; - - /** - * Opens a stream to write the content of an entity - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * @param string|int $identifier Entity identifier - * - * @return resource Stream resource for writing - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function entityWriteStream(string|int|null $collection, string|int $identifier); - - /** - * Writes a chunk of content to an entity at a specific position - * - * @since 2025.11.01 - * - * @param string|int|null $collection Collection identifier - * @param string|int $identifier Entity identifier - * @param int $offset Starting byte position (0-indexed) - * @param string $data Chunk content to write - * - * @return int Number of bytes written - * - * @throws \KTXF\Resource\Exceptions\InvalidArgumentException - * @throws \KTXF\Resource\Exceptions\UnsupportedException - * @throws \KTXF\Resource\Exceptions\UnauthorizedException - */ - public function entityWriteChunk(string|int|null $collection, string|int $identifier, int $offset, string $data): int; - -} diff --git a/shared/lib/Files/Service/IServiceMutable.php b/shared/lib/Files/Service/IServiceMutable.php deleted file mode 100644 index 43fba83..0000000 --- a/shared/lib/Files/Service/IServiceMutable.php +++ /dev/null @@ -1,30 +0,0 @@ - - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -namespace KTXF\Files\Service; - -use KTXF\Json\JsonDeserializable; - -interface IServiceMutable extends IServiceBase, JsonDeserializable { - - /** - * Sets the localized human friendly name of this service - * - * @since 2025.11.01 - */ - public function setLabel(string $value): static; - - /** - * Sets the active status of this service - * - * @since 2025.11.01 - */ - public function setEnabled(bool $value): static; - -} diff --git a/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php b/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php index 1ae32b0..6dcd267 100644 --- a/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php +++ b/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php @@ -23,13 +23,6 @@ abstract class MessagePropertiesBaseAbstract extends NodePropertiesBaseAbstract public const JSON_TYPE = MessagePropertiesBaseInterface::JSON_TYPE; - /** - * @inheritDoc - */ - public function version(): int { - return $this->data['version'] ?? 1; - } - /** * @inheritDoc */ @@ -337,7 +330,7 @@ abstract class MessagePropertiesBaseAbstract extends NodePropertiesBaseAbstract public function jsonSerialize(): array { $data = [ self::JSON_PROPERTY_TYPE => self::JSON_TYPE, - self::JSON_PROPERTY_VERSION => $this->data['version'] ?? 1, + self::JSON_PROPERTY_SCHEMA => $this->data['schema'] ?? 1, ]; if (!empty($this->data['headers'])) { diff --git a/shared/lib/Resource/Documents/Collection/CollectionBaseAbstract.php b/shared/lib/Resource/Documents/Collection/CollectionBaseAbstract.php new file mode 100644 index 0000000..a5f30b2 --- /dev/null +++ b/shared/lib/Resource/Documents/Collection/CollectionBaseAbstract.php @@ -0,0 +1,31 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Collection; + +use KTXF\Resource\Provider\Node\NodeBaseAbstract; + +/** + * Abstract Chrono Collection Base Class + * + * Provides common implementation for chrono collections + * + * @since 2025.05.01 + */ +abstract class CollectionBase extends NodeBaseAbstract implements CollectionBaseInterface { + + protected CollectionPropertiesBaseAbstract $properties; + + /** + * @inheritDoc + */ + public function getProperties(): CollectionPropertiesBaseInterface { + return $this->properties; + } +} diff --git a/shared/lib/Resource/Documents/Collection/CollectionBaseInterface.php b/shared/lib/Resource/Documents/Collection/CollectionBaseInterface.php new file mode 100644 index 0000000..5eb62ba --- /dev/null +++ b/shared/lib/Resource/Documents/Collection/CollectionBaseInterface.php @@ -0,0 +1,30 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Collection; + +use KTXF\Resource\Provider\Node\NodeBaseInterface; + +/** + * Collection Base Interface + * + * Interface represents a collection in a service + * + * @since 2025.05.01 + */ +interface CollectionBaseInterface extends NodeBaseInterface { + + /** + * Gets the collection properties + * + * @since 2025.05.01 + */ + public function getProperties(): CollectionPropertiesBaseInterface|CollectionPropertiesMutableInterface; + +} diff --git a/shared/lib/Resource/Documents/Collection/CollectionContent.php b/shared/lib/Resource/Documents/Collection/CollectionContent.php new file mode 100644 index 0000000..82b78fc --- /dev/null +++ b/shared/lib/Resource/Documents/Collection/CollectionContent.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Collection; + +use JsonSerializable; + +enum CollectionContent: string implements JsonSerializable { + + case File = 'file'; + case Folder = 'folder'; + + public function jsonSerialize(): string { + return $this->value; + } + +} diff --git a/shared/lib/Resource/Documents/Collection/CollectionMutableAbstract.php b/shared/lib/Resource/Documents/Collection/CollectionMutableAbstract.php new file mode 100644 index 0000000..7c5fbeb --- /dev/null +++ b/shared/lib/Resource/Documents/Collection/CollectionMutableAbstract.php @@ -0,0 +1,46 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Collection; + +use KTXF\Resource\Provider\Node\NodeMutableAbstract; +use KTXF\Resource\Provider\Node\NodePropertiesMutableInterface; + +/** + * Abstract Chrono Collection Mutable Class + * + * Provides common implementation for mutable chrono collections + * + * @since 2025.05.01 + */ +abstract class CollectionMutableAbstract extends NodeMutableAbstract implements CollectionMutableInterface { + + protected CollectionPropertiesMutableAbstract $properties; + + /** + * @inheritDoc + */ + public function getProperties(): CollectionPropertiesMutableInterface { + return $this->properties; + } + + /** + * @inheritDoc + */ + public function setProperties(NodePropertiesMutableInterface $value): static { + if (!$value instanceof CollectionPropertiesMutableInterface) { + throw new \InvalidArgumentException('Properties must implement CollectionPropertiesMutableInterface'); + } + + // Copy all property values + $this->properties->setLabel($value->getLabel()); + + return $this; + } +} diff --git a/shared/lib/Resource/Documents/Collection/CollectionMutableInterface.php b/shared/lib/Resource/Documents/Collection/CollectionMutableInterface.php new file mode 100644 index 0000000..94568aa --- /dev/null +++ b/shared/lib/Resource/Documents/Collection/CollectionMutableInterface.php @@ -0,0 +1,32 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Collection; + +use KTXF\Resource\Provider\Node\NodeMutableInterface; + +/** + * Chrono Collection Mutable Interface + * + * Interface for altering collection properties in a chrono service + * + * @since 2025.05.01 + * + * @method static setProperties(CollectionPropertiesMutableInterface $value) + */ +interface CollectionMutableInterface extends CollectionBaseInterface, NodeMutableInterface { + + /** + * Gets the collection properties (mutable) + * + * @since 2025.05.01 + */ + public function getProperties(): CollectionPropertiesMutableInterface; + +} diff --git a/shared/lib/Resource/Documents/Collection/CollectionPropertiesBaseAbstract.php b/shared/lib/Resource/Documents/Collection/CollectionPropertiesBaseAbstract.php new file mode 100644 index 0000000..c92ec62 --- /dev/null +++ b/shared/lib/Resource/Documents/Collection/CollectionPropertiesBaseAbstract.php @@ -0,0 +1,42 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Collection; + +use KTXF\Resource\Provider\Node\NodePropertiesBaseAbstract; + +/** + * Abstract Collection Properties Base Class + * + * Provides common implementation for collection properties + * + * @since 2025.05.01 + */ +abstract class CollectionPropertiesBaseAbstract extends NodePropertiesBaseAbstract implements CollectionPropertiesBaseInterface { + + public const JSON_TYPE = CollectionPropertiesBaseInterface::JSON_TYPE; + + public function content(): CollectionContent { + $content = $this->data[self::JSON_PROPERTY_CONTENTS] ?? null; + if ($content instanceof CollectionContent) { + return $content; + } + + if (is_string($content)) { + return CollectionContent::tryFrom($content) ?? CollectionContent::File; + } + + return CollectionContent::File; + } + + public function getLabel(): string { + return $this->data[self::JSON_PROPERTY_LABEL] ?? ''; + } + +} diff --git a/shared/lib/Resource/Documents/Collection/CollectionPropertiesBaseInterface.php b/shared/lib/Resource/Documents/Collection/CollectionPropertiesBaseInterface.php new file mode 100644 index 0000000..36c40c3 --- /dev/null +++ b/shared/lib/Resource/Documents/Collection/CollectionPropertiesBaseInterface.php @@ -0,0 +1,29 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Collection; + +use KTXF\Resource\Provider\Node\NodePropertiesBaseInterface; + +interface CollectionPropertiesBaseInterface extends NodePropertiesBaseInterface { + + public const JSON_TYPE = 'document:collection'; + public const JSON_PROPERTY_CONTENTS = 'content'; + public const JSON_PROPERTY_LABEL = 'label'; + + public function content(): CollectionContent; + + /** + * Gets the human friendly name of this collection (e.g. Personal Calendar) + * + * @since 2025.05.01 + */ + public function getLabel(): string; + +} diff --git a/shared/lib/Resource/Documents/Collection/CollectionPropertiesMutableAbstract.php b/shared/lib/Resource/Documents/Collection/CollectionPropertiesMutableAbstract.php new file mode 100644 index 0000000..66e8df6 --- /dev/null +++ b/shared/lib/Resource/Documents/Collection/CollectionPropertiesMutableAbstract.php @@ -0,0 +1,34 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Collection; + +/** + * Abstract Chrono Collection Properties Mutable Class + */ +abstract class CollectionPropertiesMutableAbstract extends CollectionPropertiesBaseAbstract implements CollectionPropertiesMutableInterface { + + public const JSON_TYPE = CollectionPropertiesBaseInterface::JSON_TYPE; + + public function jsonDeserialize(array|string $data): static { + if (is_string($data)) { + $data = json_decode($data, true); + } + + $this->data = $data; + + return $this; + } + + public function setLabel(string $value): static { + $this->data[self::JSON_PROPERTY_LABEL] = $value; + return $this; + } + +} diff --git a/shared/lib/Resource/Documents/Collection/CollectionPropertiesMutableInterface.php b/shared/lib/Resource/Documents/Collection/CollectionPropertiesMutableInterface.php new file mode 100644 index 0000000..5655bc2 --- /dev/null +++ b/shared/lib/Resource/Documents/Collection/CollectionPropertiesMutableInterface.php @@ -0,0 +1,25 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Collection; + +use KTXF\Resource\Provider\Node\NodePropertiesMutableInterface; + +interface CollectionPropertiesMutableInterface extends CollectionPropertiesBaseInterface, NodePropertiesMutableInterface { + + public const JSON_TYPE = CollectionPropertiesBaseInterface::JSON_TYPE; + + /** + * Sets the human friendly name of this collection (e.g. Personal Calendar) + * + * @since 2025.05.01 + */ + public function setLabel(string $value): static; + +} diff --git a/shared/lib/Resource/Documents/Entity/EntityBaseAbstract.php b/shared/lib/Resource/Documents/Entity/EntityBaseAbstract.php new file mode 100644 index 0000000..b205c2d --- /dev/null +++ b/shared/lib/Resource/Documents/Entity/EntityBaseAbstract.php @@ -0,0 +1,31 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Entity; + +use KTXF\Resource\Provider\Node\NodeBaseAbstract; + +/** + * Abstract Chrono Entity Base Class + * + * Provides common implementation for chrono entities + * + * @since 2025.05.01 + */ +abstract class EntityBaseAbstract extends NodeBaseAbstract implements EntityBaseInterface { + + protected EntityPropertiesBaseAbstract $properties; + + /** + * @inheritDoc + */ + public function getProperties(): EntityPropertiesBaseInterface { + return $this->properties; + } +} diff --git a/shared/lib/Resource/Documents/Entity/EntityBaseInterface.php b/shared/lib/Resource/Documents/Entity/EntityBaseInterface.php new file mode 100644 index 0000000..fc440d5 --- /dev/null +++ b/shared/lib/Resource/Documents/Entity/EntityBaseInterface.php @@ -0,0 +1,25 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Entity; + +use KTXF\Resource\Provider\Node\NodeBaseInterface; + +interface EntityBaseInterface extends NodeBaseInterface { + + public const JSON_TYPE = 'document:entity'; + + /** + * Gets the entity properties + * + * @since 2025.05.01 + */ + public function getProperties(): EntityPropertiesBaseInterface|EntityPropertiesMutableInterface; + +} diff --git a/shared/lib/Resource/Documents/Entity/EntityMutableAbstract.php b/shared/lib/Resource/Documents/Entity/EntityMutableAbstract.php new file mode 100644 index 0000000..ac733fa --- /dev/null +++ b/shared/lib/Resource/Documents/Entity/EntityMutableAbstract.php @@ -0,0 +1,47 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Entity; + +use KTXF\Resource\Provider\Node\NodeMutableAbstract; +use KTXF\Resource\Provider\Node\NodePropertiesMutableInterface; + +/** + * Abstract Chrono Entity Mutable Class + * + * Provides common implementation for mutable chrono entities + * + * @since 2025.05.01 + */ +abstract class EntityMutableAbstract extends NodeMutableAbstract implements EntityMutableInterface { + + public const JSON_TYPE = EntityMutableInterface::JSON_TYPE; + + protected EntityPropertiesMutableAbstract $properties; + + /** + * @inheritDoc + */ + public function getProperties(): EntityPropertiesMutableInterface { + return $this->properties; + } + + /** + * @inheritDoc + */ + public function setProperties(NodePropertiesMutableInterface $value): static { + if (!$value instanceof EntityPropertiesMutableInterface) { + throw new \InvalidArgumentException('Properties must implement EntityPropertiesMutableInterface'); + } + + $this->properties->setDataRaw($value->getDataRaw()); + + return $this; + } +} diff --git a/shared/lib/Resource/Documents/Entity/EntityMutableInterface.php b/shared/lib/Resource/Documents/Entity/EntityMutableInterface.php new file mode 100644 index 0000000..504c1fe --- /dev/null +++ b/shared/lib/Resource/Documents/Entity/EntityMutableInterface.php @@ -0,0 +1,28 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Entity; + +use KTXF\Resource\Provider\Node\NodeMutableInterface; + +/** + * @method static setProperties(EntityPropertiesMutableInterface $value) + */ +interface EntityMutableInterface extends EntityBaseInterface, NodeMutableInterface { + + public const JSON_TYPE = EntityBaseInterface::JSON_TYPE; + + /** + * Gets the entity properties (mutable) + * + * @since 2025.05.01 + */ + public function getProperties(): EntityPropertiesMutableInterface; + +} diff --git a/shared/lib/Resource/Documents/Entity/EntityPropertiesBaseAbstract.php b/shared/lib/Resource/Documents/Entity/EntityPropertiesBaseAbstract.php new file mode 100644 index 0000000..3268b0c --- /dev/null +++ b/shared/lib/Resource/Documents/Entity/EntityPropertiesBaseAbstract.php @@ -0,0 +1,38 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Entity; + +use KTXF\Resource\Provider\Node\NodePropertiesBaseAbstract; + +abstract class EntityPropertiesBaseAbstract extends NodePropertiesBaseAbstract implements EntityPropertiesBaseInterface { + + public const JSON_TYPE = EntityPropertiesBaseInterface::JSON_TYPE; + + public function size(): int { + return $this->data[self::JSON_PROPERTY_SIZE] ?? 0; + } + + public function getLabel(): string { + return $this->data[self::JSON_PROPERTY_LABEL] ?? ''; + } + + public function getMime(): string { + return $this->data[self::JSON_PROPERTY_MIME] ?? ''; + } + + public function getFormat(): string { + return $this->data[self::JSON_PROPERTY_FORMAT] ?? ''; + } + + public function getEncoding(): string { + return $this->data[self::JSON_PROPERTY_ENCODING] ?? ''; + } + +} diff --git a/shared/lib/Resource/Documents/Entity/EntityPropertiesBaseInterface.php b/shared/lib/Resource/Documents/Entity/EntityPropertiesBaseInterface.php new file mode 100644 index 0000000..93ad4b1 --- /dev/null +++ b/shared/lib/Resource/Documents/Entity/EntityPropertiesBaseInterface.php @@ -0,0 +1,33 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Entity; + +use KTXF\Resource\Provider\Node\NodePropertiesBaseInterface; + +interface EntityPropertiesBaseInterface extends NodePropertiesBaseInterface { + + public const JSON_TYPE = 'document:file'; + public const JSON_PROPERTY_LABEL = 'label'; + public const JSON_PROPERTY_SIZE = 'size'; + public const JSON_PROPERTY_MIME = 'mime'; + public const JSON_PROPERTY_FORMAT = 'format'; + public const JSON_PROPERTY_ENCODING = 'encoding'; + + public function size(): int; + + public function getLabel(): string; + + public function getMime(): string; + + public function getFormat(): string; + + public function getEncoding(): string; + +} diff --git a/shared/lib/Resource/Documents/Entity/EntityPropertiesMutableAbstract.php b/shared/lib/Resource/Documents/Entity/EntityPropertiesMutableAbstract.php new file mode 100644 index 0000000..26d8242 --- /dev/null +++ b/shared/lib/Resource/Documents/Entity/EntityPropertiesMutableAbstract.php @@ -0,0 +1,46 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Entity; + +abstract class EntityPropertiesMutableAbstract extends EntityPropertiesBaseAbstract implements EntityPropertiesMutableInterface { + + public const JSON_TYPE = EntityPropertiesBaseInterface::JSON_TYPE; + + public function jsonDeserialize(array|string $data): static { + if (is_string($data)) { + $data = json_decode($data, true); + } + + $this->data = $data; + + return $this; + } + + public function setLabel(string $value): static { + $this->data[self::JSON_PROPERTY_LABEL] = $value; + return $this; + } + + public function setMime(string $value): static { + $this->data[self::JSON_PROPERTY_MIME] = $value; + return $this; + } + + public function setFormat(string $value): static { + $this->data[self::JSON_PROPERTY_FORMAT] = $value; + return $this; + } + + public function setEncoding(string $value): static { + $this->data[self::JSON_PROPERTY_ENCODING] = $value; + return $this; + } + +} diff --git a/shared/lib/Resource/Documents/Entity/EntityPropertiesMutableInterface.php b/shared/lib/Resource/Documents/Entity/EntityPropertiesMutableInterface.php new file mode 100644 index 0000000..dd9d1a0 --- /dev/null +++ b/shared/lib/Resource/Documents/Entity/EntityPropertiesMutableInterface.php @@ -0,0 +1,26 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Entity; + +use KTXF\Resource\Provider\Node\NodePropertiesMutableInterface; + +interface EntityPropertiesMutableInterface extends EntityPropertiesBaseInterface, NodePropertiesMutableInterface { + + public const JSON_TYPE = EntityPropertiesBaseInterface::JSON_TYPE; + + public function setLabel(string $value): static; + + public function setMime(string $value): static; + + public function setFormat(string $value): static; + + public function setEncoding(string $value): static; + +} diff --git a/shared/lib/Resource/Documents/Provider/ProviderBaseInterface.php b/shared/lib/Resource/Documents/Provider/ProviderBaseInterface.php new file mode 100644 index 0000000..ab736fb --- /dev/null +++ b/shared/lib/Resource/Documents/Provider/ProviderBaseInterface.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Provider; + +use KTXF\Resource\Provider\ResourceProviderBaseInterface; + +/** + * Chrono Provider Base Interface + * + * @since 2025.05.01 + */ +interface ProviderBaseInterface extends ResourceProviderBaseInterface{ + + public const JSON_TYPE = 'document:provider'; + +} diff --git a/shared/lib/Resource/Documents/Provider/ProviderServiceMutateInterface.php b/shared/lib/Resource/Documents/Provider/ProviderServiceMutateInterface.php new file mode 100644 index 0000000..800783e --- /dev/null +++ b/shared/lib/Resource/Documents/Provider/ProviderServiceMutateInterface.php @@ -0,0 +1,35 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Provider; + +use KTXF\Resource\Provider\ResourceProviderServiceMutateInterface; + +/** + * Chrono Provider Service Mutate Interface + * + * Optional interface for providers that support service CRUD operations. + * + * Implementations return ServiceMutableInterface instances (which extend ResourceServiceMutateInterface). + * + * @since 2025.05.01 + * + * @method ServiceMutableInterface serviceFresh() Construct a new blank chrono service instance + * @method string serviceCreate(string $tenantId, ?string $userId, ServiceMutableInterface $service) Create a chrono service configuration + * @method string serviceModify(string $tenantId, ?string $userId, ServiceMutableInterface $service) Modify a chrono service configuration + * @method bool serviceDestroy(string $tenantId, ?string $userId, ServiceMutableInterface $service) Delete a chrono service configuration + */ +interface ProviderServiceMutateInterface extends ProviderBaseInterface, ResourceProviderServiceMutateInterface { + + public const JSON_TYPE = ProviderBaseInterface::JSON_TYPE; + + // Methods inherited from ResourceProviderServiceMutateInterface + // Implementations should return/accept ServiceMutableInterface instances + +} diff --git a/shared/lib/Resource/Documents/Provider/ProviderServiceTestInterface.php b/shared/lib/Resource/Documents/Provider/ProviderServiceTestInterface.php new file mode 100644 index 0000000..92741b1 --- /dev/null +++ b/shared/lib/Resource/Documents/Provider/ProviderServiceTestInterface.php @@ -0,0 +1,57 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Provider; + +use KTXF\Resource\Documents\Service\ServiceBaseInterface; + +/** + * Chrono Provider Service Test Interface + * + * Optional interface for chrono providers that support testing service connections. + * Providers implementing this interface can validate connection parameters, + * test authentication, and verify service availability before creating a + * persistent service configuration. + * + * Supports two testing modes: + * 1. Testing an existing service (validate current configuration) + * 2. Testing a fresh configuration (validate before saving) + * + * @since 2025.05.01 + */ +interface ProviderServiceTestInterface extends ProviderBaseInterface { + + /** + * Test a service connection + * + * Tests connectivity, authentication, and capabilities of a service. + * + * For new services: use serviceFresh() to create a service, configure it with + * setters, then pass it to this method for testing before persisting. + * + * For existing services: fetch the service and pass it directly. + * + * @since 2025.05.01 + * + * @param ServiceBaseInterface $service Service to test (can be fresh/unsaved or existing) + * @param array $options Provider-specific test options: + * - 'timeout' => int (seconds, default: 10) + * - 'verify_ssl' => bool (default: true) + * - 'test_send' => bool (attempt test send if capable, default: false) + * - 'test_receive' => bool (attempt mailbox access if capable, default: true) + * + * @return array Test results in the format: + * [ + * 'success' => bool, + * 'message' => 'Connection successful' | 'Error message' + * ] + */ + public function serviceTest(ServiceBaseInterface $service, array $options = []): array; + +} diff --git a/shared/lib/Resource/Documents/Service/ServiceBaseInterface.php b/shared/lib/Resource/Documents/Service/ServiceBaseInterface.php new file mode 100644 index 0000000..c29637f --- /dev/null +++ b/shared/lib/Resource/Documents/Service/ServiceBaseInterface.php @@ -0,0 +1,199 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Service; + +use KTXF\Resource\Documents\Collection\CollectionBaseInterface; +use KTXF\Resource\Delta\Delta; +use KTXF\Resource\Filter\IFilter; +use KTXF\Resource\Provider\ResourceServiceBaseInterface; +use KTXF\Resource\Range\IRange; +use KTXF\Resource\Range\RangeType; +use KTXF\Resource\Sort\ISort; + +/** + * Chrono Service Base Interface + * + * @since 2025.05.01 + */ +interface ServiceBaseInterface extends ResourceServiceBaseInterface { + + // Collection capabilities + public const CAPABILITY_COLLECTION_LIST = 'CollectionList'; + public const CAPABILITY_COLLECTION_LIST_FILTER = 'CollectionListFilter'; + public const CAPABILITY_COLLECTION_LIST_SORT = 'CollectionListSort'; + public const CAPABILITY_COLLECTION_EXTANT = 'CollectionExtant'; + public const CAPABILITY_COLLECTION_FETCH = 'CollectionFetch'; + // Collection Filter + public const CAPABILITY_COLLECTION_FILTER_LABEL = 'label'; + public const CAPABILITY_COLLECTION_FILTER_CONTENTS = 'contents'; + // Collection Sort + public const CAPABILITY_COLLECTION_SORT_LABEL = 'label'; + public const CAPABILITY_COLLECTION_SORT_RANK = 'rank'; + // Entity capabilities + public const CAPABILITY_ENTITY_LIST = 'EntityList'; + public const CAPABILITY_ENTITY_LIST_FILTER = 'EntityListFilter'; + public const CAPABILITY_ENTITY_LIST_SORT = 'EntityListSort'; + public const CAPABILITY_ENTITY_LIST_RANGE = 'EntityListRange'; + public const CAPABILITY_ENTITY_DELTA = 'EntityDelta'; + public const CAPABILITY_ENTITY_EXTANT = 'EntityExtant'; + public const CAPABILITY_ENTITY_FETCH = 'EntityFetch'; + public const CAPABILITY_ENTITY_READ = 'EntityRead'; + // Filter capabilities + public const CAPABILITY_ENTITY_FILTER_ALL = '*'; + public const CAPABILITY_ENTITY_FILTER_ID = 'id'; + public const CAPABILITY_ENTITY_FILTER_URID = 'urid'; + public const CAPABILITY_ENTITY_FILTER_LABEL = 'label'; + // Sort capabilities + public const CAPABILITY_ENTITY_SORT_ID = 'id'; + public const CAPABILITY_ENTITY_SORT_URID = 'urid'; + public const CAPABILITY_ENTITY_SORT_LABEL = 'label'; + public const CAPABILITY_ENTITY_SORT_PRIORITY = 'priority'; + // Range capabilities + public const CAPABILITY_ENTITY_RANGE_TALLY = 'tally'; + public const CAPABILITY_ENTITY_RANGE_TALLY_ABSOLUTE = 'absolute'; + public const CAPABILITY_ENTITY_RANGE_TALLY_RELATIVE = 'relative'; + public const CAPABILITY_ENTITY_RANGE_DATE = 'date'; + + public const JSON_TYPE = 'document:service'; + + /** + * Lists all collections in this service + * + * @since 2025.05.01 + * + * @param IFilter|null $filter Optional filter criteria + * @param ISort|null $sort Optional sort order + * + * @return array Collections indexed by ID + */ + public function collectionList(string|int $location, ?IFilter $filter = null, ?ISort $sort = null): array; + + /** + * Creates a filter builder for collections + * + * @since 2025.05.01 + * + * @return IFilter + */ + public function collectionListFilter(): IFilter; + + /** + * Creates a sort builder for collections + * + * @since 2025.05.01 + * + * @return ISort + */ + public function collectionListSort(): ISort; + + /** + * Checks if collections exist + * + * @since 2025.05.01 + * + * @param string|int ...$identifiers Collection IDs to check + * + * @return array Map of ID => exists + */ + public function collectionExtant(string|int $location, string|int ...$identifiers): array; + + /** + * Fetches a single collection + * + * @since 2025.05.01 + * + * @param string|int $identifier Collection ID + * + * @return CollectionBaseInterface|null Collection or null if not found + */ + public function collectionFetch(string|int $identifier): ?CollectionBaseInterface; + + /** + * Lists messages in a collection + * + * @since 2025.05.01 + * + * @param string|int $collection Collection ID + * @param IFilter|null $filter Optional filter criteria + * @param ISort|null $sort Optional sort order + * @param IRange|null $range Optional pagination + * @param array|null $properties Optional message properties to fetch + * + * @return array Messages indexed by ID + */ + public function entityList(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array; + + /** + * Creates a filter builder for messages + * + * @since 2025.05.01 + * + * @return IFilter + */ + public function entityListFilter(): IFilter; + + /** + * Creates a sort builder for messages + * + * @since 2025.05.01 + * + * @return ISort + */ + public function entityListSort(): ISort; + + /** + * Creates a range builder for messages + * + * @since 2025.05.01 + * + * @param RangeType $type Range type (offset, cursor, etc.) + * + * @return IRange + */ + public function entityListRange(RangeType $type): IRange; + + /** + * Gets incremental changes since last sync + * + * @since 2025.05.01 + * + * @param string|int $collection Collection ID + * @param string $signature Sync token from previous sync + * @param string $detail Detail level: 'ids', 'minimal', 'full' + * + * @return array ['signature' => string, 'added' => array, 'modified' => array, 'removed' => array] + */ + public function entityDelta(string|int $collection, string $signature, string $detail = 'ids'): Delta; + + /** + * Checks if messages exist + * + * @since 2025.05.01 + * + * @param string|int $collection Collection ID + * @param string|int ...$identifiers Message IDs to check + * + * @return array Map of ID => exists + */ + public function entityExtant(string|int $collection, string|int ...$identifiers): array; + + /** + * Fetches one or more entities + * + * @since 2025.05.01 + * + * @param string|int $collection Collection ID + * @param string|int ...$identifiers Message IDs to fetch + * + * @return array Messages indexed by ID + */ + public function entityFetch(string|int $collection, string|int ...$identifiers): array; + +} diff --git a/shared/lib/Resource/Documents/Service/ServiceCollectionMutableInterface.php b/shared/lib/Resource/Documents/Service/ServiceCollectionMutableInterface.php new file mode 100644 index 0000000..ff0c9aa --- /dev/null +++ b/shared/lib/Resource/Documents/Service/ServiceCollectionMutableInterface.php @@ -0,0 +1,73 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Service; + +use KTXF\Resource\Documents\Collection\CollectionBaseInterface; +use KTXF\Resource\Documents\Collection\CollectionMutableInterface; + +/** + * Chrono Service Collection Mutable Interface + * + * @since 2025.05.01 + */ +interface ServiceCollectionMutableInterface extends ServiceBaseInterface { + + public const CAPABILITY_COLLECTION_CREATE = 'CollectionCreate'; + public const CAPABILITY_COLLECTION_UPDATE = 'CollectionUpdate'; + public const CAPABILITY_COLLECTION_DELETE = 'CollectionDelete'; + + /** + * Creates a fresh collection instance for configuration + * + * @since 2025.05.01 + * + * @return CollectionMutableInterface Fresh collection object + */ + public function collectionFresh(): CollectionMutableInterface; + + /** + * Creates a new collection + * + * @since 2025.05.01 + * + * @param string|int|null $location Parent collection ID (null for root) + * @param CollectionMutableInterface $collection Collection to create + * @param array $options Protocol-specific options + * + * @return CollectionBaseInterface Created collection with assigned ID + */ + public function collectionCreate(string|int|null $location, CollectionMutableInterface $collection, array $options = []): CollectionBaseInterface; + + /** + * Updates an existing collection + * + * @since 2025.05.01 + * + * @param string|int $identifier Collection ID + * @param CollectionMutableInterface $collection Updated collection data + * + * @return CollectionBaseInterface Updated collection + */ + public function collectionUpdate(string|int $identifier, CollectionMutableInterface $collection): CollectionBaseInterface; + + /** + * Deletes a collection + * + * @since 2025.05.01 + * + * @param string|int $identifier Collection ID + * @param bool $force Force deletion even if not empty + * @param bool $recursive Recursively delete contents + * + * @return bool True if deleted + */ + public function collectionDelete(string|int $identifier, bool $force = false, bool $recursive = false): bool; + +} diff --git a/shared/lib/Resource/Documents/Service/ServiceConfigurableInterface.php b/shared/lib/Resource/Documents/Service/ServiceConfigurableInterface.php new file mode 100644 index 0000000..fd7b998 --- /dev/null +++ b/shared/lib/Resource/Documents/Service/ServiceConfigurableInterface.php @@ -0,0 +1,26 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Service; + +use KTXF\Resource\Provider\ResourceServiceConfigureInterface; + +/** + * Chrono Service Configurable Interface + * + * Extends base service interface with setter methods for mutable properties. + * Used for service configuration and updates. + * + * @since 2025.05.01 + */ +interface ServiceConfigurableInterface extends ServiceMutableInterface, ResourceServiceConfigureInterface { + + public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE; + +} diff --git a/shared/lib/Resource/Documents/Service/ServiceEntityMutableInterface.php b/shared/lib/Resource/Documents/Service/ServiceEntityMutableInterface.php new file mode 100644 index 0000000..09cdb49 --- /dev/null +++ b/shared/lib/Resource/Documents/Service/ServiceEntityMutableInterface.php @@ -0,0 +1,78 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Service; + +use KTXF\Resource\Documents\Entity\EntityBaseInterface; +use KTXF\Resource\Documents\Entity\EntityMutableInterface; + +/** + * Chrono Service Entity Mutable Interface + * + * Optional interface for services that support entity CRUD operations. + * Provides entity creation, modification, deletion, copying, moving, and flag management. + * + * @since 2025.05.01 + */ +interface ServiceEntityMutableInterface extends ServiceBaseInterface { + + public const CAPABILITY_ENTITY_CREATE = 'EntityCreate'; + public const CAPABILITY_ENTITY_UPDATE = 'EntityUpdate'; + public const CAPABILITY_ENTITY_DELETE = 'EntityDelete'; + public const CAPABILITY_ENTITY_COPY = 'EntityCopy'; + public const CAPABILITY_ENTITY_MOVE = 'EntityMove'; + public const CAPABILITY_ENTITY_WRITE = 'EntityWrite'; + + /** + * Creates a fresh entity instance for composition + * + * @since 2025.05.01 + * + * @return EntityMutableInterface Fresh entity object + */ + public function entityFresh(): EntityMutableInterface; + + /** + * Creates/imports an entity into a collection + * + * @since 2025.05.01 + * + * @param string|int $collection collection identifier + * @param EntityMutableInterface $entity Entity data + * @param array $options additional options + * + * @return EntityBaseInterface Created entity + */ + public function entityCreate(string|int $collection, EntityMutableInterface $entity, array $options = []): EntityBaseInterface; + + /** + * Modifies an existing entity + * + * @since 2025.05.01 + * + * @param string|int $collection Collection identifier + * @param string|int $identifier Entity identifier + * @param EntityMutableInterface $entity Entity data + * + * @return EntityBaseInterface Modified entity + */ + public function entityUpdate(string|int $collection, string|int $identifier, EntityMutableInterface $entity): EntityBaseInterface; + /** + * Deletes an existing entity in the specified collection + * + * @since 2025.05.01 + * + * @param string|int $collection Collection identifier + * @param string|int $identifier Entity identifier to delete + * + * @return EntityBaseInterface Deleted entity + */ + public function entityDelete(string|int $collection, string|int $identifier): EntityBaseInterface; + +} diff --git a/shared/lib/Resource/Documents/Service/ServiceMutableInterface.php b/shared/lib/Resource/Documents/Service/ServiceMutableInterface.php new file mode 100644 index 0000000..e2cc5ee --- /dev/null +++ b/shared/lib/Resource/Documents/Service/ServiceMutableInterface.php @@ -0,0 +1,22 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXF\Resource\Documents\Service; + +/** + * Chrono Service Mutable Interface + * + * Extends base service interface with setter methods for mutable properties. + * Used for service configuration and updates. + * + * @since 2025.05.01 + */ +interface ServiceMutableInterface extends ServiceBaseInterface { + +} diff --git a/shared/lib/Resource/Provider/Node/NodePropertiesBaseAbstract.php b/shared/lib/Resource/Provider/Node/NodePropertiesBaseAbstract.php index c61734d..ccc3677 100644 --- a/shared/lib/Resource/Provider/Node/NodePropertiesBaseAbstract.php +++ b/shared/lib/Resource/Provider/Node/NodePropertiesBaseAbstract.php @@ -26,8 +26,8 @@ abstract class NodePropertiesBaseAbstract implements NodePropertiesBaseInterface $data[static::JSON_PROPERTY_TYPE] = static::JSON_TYPE; } - if (!isset($data[static::JSON_PROPERTY_VERSION])) { - $data[static::JSON_PROPERTY_VERSION] = 1; + if (!isset($data[static::JSON_PROPERTY_SCHEMA])) { + $data[static::JSON_PROPERTY_SCHEMA] = 1; } $this->data = $data; @@ -50,8 +50,8 @@ abstract class NodePropertiesBaseAbstract implements NodePropertiesBaseInterface /** * @inheritDoc */ - public function version(): int { - return $this->data[static::JSON_PROPERTY_VERSION]; + public function schema(): int { + return $this->data[static::JSON_PROPERTY_SCHEMA]; } } diff --git a/shared/lib/Resource/Provider/Node/NodePropertiesBaseInterface.php b/shared/lib/Resource/Provider/Node/NodePropertiesBaseInterface.php index 0db937a..551c0f3 100644 --- a/shared/lib/Resource/Provider/Node/NodePropertiesBaseInterface.php +++ b/shared/lib/Resource/Provider/Node/NodePropertiesBaseInterface.php @@ -22,7 +22,7 @@ interface NodePropertiesBaseInterface extends JsonSerializable { public const JSON_TYPE = 'resource.data'; public const JSON_PROPERTY_TYPE = '@type'; - public const JSON_PROPERTY_VERSION = 'version'; + public const JSON_PROPERTY_SCHEMA = 'schema'; /** * Get resource node properties type @@ -30,8 +30,8 @@ interface NodePropertiesBaseInterface extends JsonSerializable { public function type(): string; /** - * Get resource node properties version + * Get resource node properties schema */ - public function version(): int; + public function schema(): int; } diff --git a/shared/lib/Resource/Provider/ProviderInterface.php b/shared/lib/Resource/Provider/ProviderInterface.php index 66027d5..85e9d57 100644 --- a/shared/lib/Resource/Provider/ProviderInterface.php +++ b/shared/lib/Resource/Provider/ProviderInterface.php @@ -11,13 +11,13 @@ interface ProviderInterface { public const TYPE_AUTHENTICATION = 'authentication'; + public const TYPE_DOCUMENT = 'document'; public const TYPE_PEOPLE = 'people'; public const TYPE_CHRONO = 'chrono'; - public const TYPE_FILES = 'files'; public const TYPE_MAIL = 'mail'; /** - * Provider type (e.g., 'authentication', 'storage', 'notification') + * Provider type (e.g., 'authentication', 'document', 'people', 'chrono', 'mail') */ public function type(): string;