From 74b87e947ba9e9749677c2d6b5fd66968541e8c7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 11 Aug 2025 19:30:24 +0000 Subject: [PATCH 01/83] Bump version to 0.1.37 [skip ci] --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index a6933c14..0c530e7f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Submit a [bug report](https://github.com/OpenCatalogi/.github/issues/new/choose) Submit a [feature request](https://github.com/OpenCatalogi/.github/issues/new/choose). ]]> - 0.1.36 + 0.1.37 agpl organization Conduction From 2ec09497ab6d9ab1302c4d87f8f0a50cba66cc8d Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 13 Aug 2025 13:42:51 +0200 Subject: [PATCH 02/83] Simplify organisation syncs --- lib/AppInfo/Application.php | 34 ++--- lib/Service/OrganizationSyncService.php | 170 ++++++++++++++++-------- 2 files changed, 133 insertions(+), 71 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 77ae05ca..787b180f 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -28,6 +28,7 @@ use OCA\OpenRegister\Event\ObjectLockedEvent; use OCA\OpenRegister\Event\ObjectUnlockedEvent; use OCA\OpenRegister\Event\ObjectRevertedEvent; +use OCP\IDBConnection; use OCP\IUserManager; use OCP\IGroupManager; use OCP\IAppConfig; @@ -67,13 +68,13 @@ public function __construct() * Register event listeners and services * * @param IRegistrationContext $context Registration context - * + * * @return void */ public function register(IRegistrationContext $context): void { include_once __DIR__ . '/../../vendor/autoload.php'; - + // Register the handlers as services $context->registerService('OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler', function (ContainerInterface $c) { return new \OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler( @@ -124,10 +125,10 @@ public function register(IRegistrationContext $context): void $context->registerEventListener(ObjectLockedEvent::class, SoftwareCatalogEventListener::class); $context->registerEventListener(ObjectUnlockedEvent::class, SoftwareCatalogEventListener::class); $context->registerEventListener(ObjectRevertedEvent::class, SoftwareCatalogEventListener::class); - + // Organization event listeners removed - now using cron job for organization synchronization // Contact person event listeners are still active for real-time processing - + // Register new focused services $context->registerService(\OCA\SoftwareCatalog\Service\OrganisatieService::class, function ($container) { return new \OCA\SoftwareCatalog\Service\OrganisatieService( @@ -180,7 +181,8 @@ public function register(IRegistrationContext $context): void $container->get(SymfonyEmailService::class), $container->get(IAppConfig::class), $container->get('Psr\Log\LoggerInterface'), - $container->get(SettingsService::class) + $container->get(SettingsService::class), + $container->get(IDBConnection::class) ); }); @@ -233,14 +235,14 @@ public function register(IRegistrationContext $context): void * Boot the application * * @param IBootContext $context Boot context - * + * * @return void */ public function boot(IBootContext $context): void { $container = $context->getServerContainer(); $logger = $container->get(LoggerInterface::class); - + try { $config = $container->get(IAppConfig::class); $appManager = $container->get(IAppManager::class); @@ -256,7 +258,7 @@ public function boot(IBootContext $context): void // Check if we actually have a valid configuration, not just version matching $needsInitialization = false; $initReason = ''; - + if ($lastInitializedVersion !== $currentAppVersion || empty($lastInitializedVersion)) { $needsInitialization = true; $initReason = empty($lastInitializedVersion) ? 'never_initialized' : 'version_changed'; @@ -264,7 +266,7 @@ public function boot(IBootContext $context): void // Even if version matches, check if we have valid configuration $hasValidConfig = $config->getValueString(self::APP_ID, 'voorzieningen_organisatie_schema', '') !== '' || $config->getValueString(self::APP_ID, 'organization_schema', '') !== ''; - + if (!$hasValidConfig) { $needsInitialization = true; $initReason = 'missing_configuration'; @@ -274,25 +276,25 @@ public function boot(IBootContext $context): void ]); } } - + if ($needsInitialization) { $logger->info('SoftwareCatalog boot: Starting initialization', [ 'reason' => $initReason, 'currentVersion' => $currentAppVersion, 'lastInitializedVersion' => $lastInitializedVersion ]); - + try { $settingsService = $container->get(SettingsService::class); $initResult = $settingsService->initialize(); - + $logger->info('SoftwareCatalog boot: Initialization completed', [ 'result' => $initResult, 'hasErrors' => !empty($initResult['errors']) ]); - + // Only update version if initialization was actually successful - if (empty($initResult['errors']) && + if (empty($initResult['errors']) && ($initResult['autoConfigured'] || $initResult['fullyConfigured'])) { $config->setValueString(self::APP_ID, 'last_initialized_version', $currentAppVersion); $logger->info('SoftwareCatalog boot: Version updated to ' . $currentAppVersion . ' (successful init)'); @@ -303,7 +305,7 @@ public function boot(IBootContext $context): void 'fullyConfigured' => $initResult['fullyConfigured'] ?? false ]); } - + } catch (\RuntimeException $e) { // Don't update version if OpenRegister is not available $logger->warning('SoftwareCatalog boot: OpenRegister not available during initialization', [ @@ -320,7 +322,7 @@ public function boot(IBootContext $context): void } else { $logger->debug('SoftwareCatalog boot: Skipping initialization (version unchanged and config valid)'); } - + } catch (\Exception $e) { // Log error but don't fail the boot process $logger->error('SoftwareCatalog boot error during version check: ' . $e->getMessage(), [ diff --git a/lib/Service/OrganizationSyncService.php b/lib/Service/OrganizationSyncService.php index dc192e98..383288f7 100644 --- a/lib/Service/OrganizationSyncService.php +++ b/lib/Service/OrganizationSyncService.php @@ -18,18 +18,20 @@ namespace OCA\SoftwareCatalog\Service; +use OCA\OpenRegister\Service\ObjectService; use OCA\SoftwareCatalog\Service\OrganisatieService; use OCA\SoftwareCatalog\Service\ContactpersoonService; use OCA\SoftwareCatalog\Service\SymfonyEmailService; use OCP\IAppConfig; +use OCP\IDBConnection; use Psr\Log\LoggerInterface; /** * Service for synchronizing organizations and contact persons - * + * * This service provides comprehensive synchronization between SoftwareCatalog objects * and OpenRegister entities, ensuring data consistency and proper user management. - * + * * @category Service * @package OCA\SoftwareCatalog\Service * @author Conduction b.v. @@ -96,7 +98,8 @@ public function __construct( SymfonyEmailService $emailService, IAppConfig $config, LoggerInterface $logger, - SettingsService $settingsService + SettingsService $settingsService, + private IDBConnection $db, ) { $this->organisatieService = $organisatieService; $this->contactpersoonService = $contactpersoonService; @@ -106,6 +109,60 @@ public function __construct( $this->settingsService = $settingsService; } + public function performOrganizationsSync(): array + { + // Check configuration + $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); + $register = $voorzieningenConfig['register'] ?? ''; + $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? ''; +// $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? ''; + + $stats = [ + 'organizationsProcessed' => 0, + 'entitiesCreated' => 0, + 'entitiesUpdated' => 0, + 'contactPersonsProcessed' => 0, + 'usersCreated' => 0, + 'usersUpdated' => 0, + 'errors' => [], + 'startTime' => date('Y-m-d H:i:s'), + 'endTime' => null, + 'duration' => null + ]; + + $qb = $this->db->getQueryBuilder(); + + $qb->select('o.uuid', $qb->createFunction('json_unquote(json_extract(o.object, \'$.status\')) as status'), 'o2.uuid as oreg_uuid', 'o2.active as active') + ->from('openregister_objects', 'o') + ->leftJoin(fromAlias:'o', join: 'openregister_organisations', alias: 'o2', condition: 'o.uuid = o2.uuid') + ->where($qb->expr()->eq('o.schema', $qb->createNamedParameter($organizationSchema))) + ->andWhere($qb->expr()->eq('o.register', $qb->createNamedParameter($register))) + ->andWhere($qb->expr()->orX( + $qb->expr()->neq('o2.active', $qb->createFunction('(json_unquote(json_extract(o.object, \'$.status\')) = \'actief\')')), + $qb->expr()->isNull('o2.uuid') + )); + + $sql = $qb->getSQL(); + $objects = $qb->execute()->fetchAll(); + $orgs = []; + + foreach($objects as $object) { + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); + if($objectService instanceOf ObjectService === false) { + return []; + } + + $object = $objectService->find($object['uuid']); + + $org = $this->ensureOrganisationEntity($object,$stats); + + } + + return $stats; + } + + + /** * Performs comprehensive organization and contact person synchronization * @@ -113,7 +170,7 @@ public function __construct( * the specified time window with organisation entities. * * @param int $minutesBack Number of minutes to look back for changes (0 = all objects) - * + * * @return array Synchronization results and statistics */ public function performFullSync(int $minutesBack = 10): array @@ -122,7 +179,7 @@ public function performFullSync(int $minutesBack = 10): array 'minutesBack' => $minutesBack, 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental' ]); - + $stats = [ 'organizationsProcessed' => 0, 'entitiesCreated' => 0, @@ -179,7 +236,7 @@ public function performFullSync(int $minutesBack = 10): array $stats['duration'] = $endTime->diff($startTime)->format('%H:%I:%S'); $this->logger->info('OrganizationSyncService: Completed comprehensive synchronization', $stats); - + return $stats; } catch (\Exception $e) { @@ -199,14 +256,14 @@ public function performFullSync(int $minutesBack = 10): array * @param string $register The register ID * @param string $organizationSchema The organization schema ID * @param int $minutesBack Number of minutes to look back (0 = all objects) - * + * * @return array Array of organisatie objects */ private function getOrganisatieObjectsByTimeWindow(string $register, string $organizationSchema, int $minutesBack): array { try { $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); - + // Build base query for register and schema $query = [ '@self' => [ @@ -214,17 +271,17 @@ private function getOrganisatieObjectsByTimeWindow(string $register, string $org 'schema' => (int) $organizationSchema ] ]; - + // Add time-based filtering if minutesBack > 0 if ($minutesBack > 0) { $cutoffTime = new \DateTime(); $cutoffTime->sub(new \DateInterval('PT' . $minutesBack . 'M')); $cutoffTimeString = $cutoffTime->format('Y-m-d\TH:i:sP'); - + // Add time filtering to the query // Filter objects that were updated within the time window $query['@self']['updated'] = ['gte' => $cutoffTimeString]; - + $this->logger->debug('OrganizationSyncService: Using searchObjects with time-based filtering', [ 'register' => $register, 'schema' => $organizationSchema, @@ -241,10 +298,10 @@ private function getOrganisatieObjectsByTimeWindow(string $register, string $org 'query' => $query ]); } - + // Use searchObjects method for filtering $objects = $objectService->searchObjects($query); - + $this->logger->debug('OrganizationSyncService: Retrieved organisatie objects with searchObjects', [ 'register' => $register, 'schema' => $organizationSchema, @@ -261,7 +318,7 @@ private function getOrganisatieObjectsByTimeWindow(string $register, string $org 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); - + return []; } } @@ -328,24 +385,24 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta try { $objectData = $organisatieObject->getObject(); $organisatieId = $objectData['id'] ?? $organisatieObject->getId(); - + // Try to find existing organisation entity $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); - + try { $organisationEntity = $organisationMapper->findByUuid($organisatieId); - + // Entity exists - update it if needed - $beoordeling = strtolower($objectData['beoordeling'] ?? 'actief'); - $shouldBeActive = in_array($beoordeling, ['actief', 'active']); - + $status = strtolower($objectData['status'] ?? 'actief'); + $shouldBeActive = in_array($status, ['actief', 'active']); + if ($organisationEntity->getActive() !== $shouldBeActive) { $this->logger->info('OrganizationSyncService: Updating organisation entity status', [ 'organisatieId' => $organisatieId, 'oldActive' => $organisationEntity->getActive(), 'newActive' => $shouldBeActive ]); - + $organisationEntity->setActive($shouldBeActive); $organisationMapper->save($organisationEntity); $stats['entitiesUpdated']++; @@ -364,7 +421,7 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta } } } - + $this->logger->debug('OrganizationSyncService: Found existing organisation entity', [ 'organisatieId' => $organisatieId, 'entityId' => $organisationEntity->getId(), @@ -376,7 +433,7 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta $this->logger->info('OrganizationSyncService: Creating new organisation entity', [ 'organisatieId' => $organisatieId ]); - + $organisationEntity = $this->organisatieService->createOrganisationInOpenRegister($objectData); if ($organisationEntity) { $stats['entitiesCreated']++; @@ -400,7 +457,7 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta } return $organisationEntity; } - + } catch (\Exception $e) { $this->logger->error('OrganizationSyncService: Failed to ensure organisation entity', [ 'organisatieId' => $organisatieObject->getId(), @@ -409,7 +466,7 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta return null; } } - + /** * Safely sends organization registration email with error handling * @@ -429,7 +486,7 @@ private function sendOrganizationRegistrationEmail(array $organizationData): boo return false; } } - + /** * Safely sends organization activation email with error handling * @@ -468,7 +525,7 @@ private function getContactPersonsForOrganisation(string $organisatieId, string try { $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); - + // Use searchObjects for more efficient filtering on-demand $query = [ '@self' => [ @@ -477,7 +534,7 @@ private function getContactPersonsForOrganisation(string $organisatieId, string ], 'organisatie' => $organisatieId ]; - + $contactPersons = $objectService->searchObjects($query); $this->logger->debug('OrganizationSyncService: Retrieved contact persons on-demand', [ @@ -585,7 +642,7 @@ private function updateOrganisationEntityUsers(object $organisationEntity, array try { $organisationUuid = $organisationEntity->getUuid(); $currentUsers = $organisationEntity->getUsers() ?? []; - + // Add admin users to ensure they're always included $adminUsers = $this->getAdminUsers(); $allUsernames = array_unique(array_merge($usernames, $adminUsers)); @@ -605,12 +662,12 @@ private function updateOrganisationEntityUsers(object $organisationEntity, array ]); $organisationEntity->setUsers($allUsernames); - + $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); $organisationMapper->save($organisationEntity); - + $stats['entitiesUpdated']++; - + $this->logger->info('OrganizationSyncService: Successfully updated organisation entity users', [ 'organisationUuid' => $organisationUuid, 'totalUsers' => count($allUsernames) @@ -640,7 +697,7 @@ private function getAdminUsers(): array try { $groupManager = \OC::$server->get('OCP\IGroupManager'); $adminGroup = $groupManager->get('admin'); - + if ($adminGroup) { $adminUsers = $adminGroup->getUsers(); $adminUsernames = []; @@ -649,7 +706,7 @@ private function getAdminUsers(): array } return $adminUsernames; } - + return []; } catch (\Exception $e) { $this->logger->error('OrganizationSyncService: Failed to get admin users', [ @@ -663,7 +720,7 @@ private function getAdminUsers(): array * Performs a quick sync status check with prediction of objects to be processed * * @param int $minutesBack Number of minutes to look back for prediction (default: 10 for scheduled sync) - * + * * @return array Status information about sync requirements including processing predictions */ public function getSyncStatus(int $minutesBack = 10): array @@ -684,10 +741,10 @@ public function getSyncStatus(int $minutesBack = 10): array // Get total counts (all objects) $allOrganisatieObjects = $this->getOrganisatieObjectsByTimeWindow($register, $organizationSchema, 0); - + // Get incremental counts (objects to be processed in next sync) $incrementalOrganisatieObjects = $this->getOrganisatieObjectsByTimeWindow($register, $organizationSchema, $minutesBack); - + // Get organization entities count $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); $entitiesCount = 0; @@ -710,7 +767,7 @@ public function getSyncStatus(int $minutesBack = 10): array } // Calculate efficiency metrics - $efficiencyImprovement = count($allOrganisatieObjects) > 0 + $efficiencyImprovement = count($allOrganisatieObjects) > 0 ? round((1 - (count($incrementalOrganisatieObjects) / count($allOrganisatieObjects))) * 100, 1) : 0; @@ -718,31 +775,31 @@ public function getSyncStatus(int $minutesBack = 10): array 'configured' => true, 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental', 'timeWindow' => $minutesBack, - + // Total counts 'totalOrganizationObjects' => count($allOrganisatieObjects), 'totalOrganizationEntities' => $entitiesCount, - + // Processing predictions 'organizationsToProcess' => count($incrementalOrganisatieObjects), 'contactPersonsToProcess' => $predictedContactPersonsToProcess, - + // Efficiency metrics 'efficiencyImprovement' => $efficiencyImprovement . '%', 'processingReduction' => count($allOrganisatieObjects) - count($incrementalOrganisatieObjects), - + // Configuration 'contactSchemaConfigured' => !empty($contactSchema), 'lastSyncTime' => $this->config->getValueString('softwarecatalog', 'last_sync_time', 'Never'), - + // Email configuration status 'emailStatus' => $this->getEmailConfigurationStatus(), - + // Status messages - 'message' => count($incrementalOrganisatieObjects) > 0 + 'message' => count($incrementalOrganisatieObjects) > 0 ? "Ready to process {$this->formatNumber(count($incrementalOrganisatieObjects))} organizations and {$this->formatNumber($predictedContactPersonsToProcess)} contact persons" : 'No organizations to process in the current time window', - 'nextScheduledSync' => $minutesBack > 0 + 'nextScheduledSync' => $minutesBack > 0 ? "Will process organizations updated in the last {$minutesBack} minutes" : 'Will process all organizations (full sync)' ]; @@ -781,7 +838,7 @@ private function getEmailConfigurationStatus(): array * Format numbers for better readability * * @param int $number The number to format - * + * * @return string Formatted number */ private function formatNumber(int $number): string @@ -810,7 +867,7 @@ public function recordSyncTime(): void * Uses default 10-minute lookback for incremental sync. * * @param int $minutesBack Number of minutes to look back for changes (default: 10) - * + * * @return array Synchronization results with detailed logging information */ public function performScheduledSync(int $minutesBack = 10): array @@ -822,8 +879,8 @@ public function performScheduledSync(int $minutesBack = 10): array try { // Perform the core synchronization with time-based filtering - $syncResults = $this->performFullSync($minutesBack); - +// $syncResults = $this->performFullSync($minutesBack); + $syncResults = $this->performOrganizationsSync(); // Record the sync time $this->recordSyncTime(); @@ -855,7 +912,7 @@ public function performScheduledSync(int $minutesBack = 10): array 'line' => $e->getLine(), 'trace' => $e->getTraceAsString() ]); - + return [ 'organizationsProcessed' => 0, 'entitiesCreated' => 0, @@ -879,7 +936,7 @@ public function performScheduledSync(int $minutesBack = 10): array * Uses full sync (minutesBack = 0) for manual triggers by default. * * @param int $minutesBack Number of minutes to look back for changes (default: 0 for full sync) - * + * * @return array Synchronization results formatted for API response */ public function performManualSync(int $minutesBack = 0): array @@ -890,8 +947,11 @@ public function performManualSync(int $minutesBack = 0): array ]); try { + $syncResults = $this->performOrganizationsSync(); +// die; + // Perform the core synchronization with time-based filtering - $syncResults = $this->performFullSync($minutesBack); +// $syncResults = $this->performFullSync($minutesBack); // Record the sync time $this->recordSyncTime(); @@ -942,7 +1002,7 @@ public function getSyncStatusWithErrorHandling(int $minutesBack = 10): array 'minutesBack' => $minutesBack, 'exception' => $e->getMessage() ]); - + return [ 'configured' => false, 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental', @@ -951,4 +1011,4 @@ public function getSyncStatusWithErrorHandling(int $minutesBack = 10): array ]; } } -} \ No newline at end of file +} From 3c420a5b7bde0711c5c08ea9d83eeeaf32fb501d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 13 Aug 2025 12:28:05 +0000 Subject: [PATCH 03/83] Bump version to 0.1.38 [skip ci] --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 0c530e7f..1e145948 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Submit a [bug report](https://github.com/OpenCatalogi/.github/issues/new/choose) Submit a [feature request](https://github.com/OpenCatalogi/.github/issues/new/choose). ]]> - 0.1.37 + 0.1.38 agpl organization Conduction From 5c33f0b0284c33146a382aafd99ddecd45fc05b1 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 13 Aug 2025 14:51:28 +0200 Subject: [PATCH 04/83] Working on the detail pages --- lib/Settings/softwarecatalogus_register.json | 16 +- src/components/GenericObjectTable.vue | 332 ++++++++---- src/components/PaginationComponent.vue | 74 ++- src/navigation/MainMenu.vue | 317 +++++++++++- src/store/modules/object.js | 62 +-- src/views/Dashboard.vue | 509 +++++++++++++++++++ src/views/ObjectIndex.vue | 351 +++++++++++++ src/views/Views.vue | 136 ++++- src/views/organisaties/OrganisatieIndex.vue | 13 + 9 files changed, 1644 insertions(+), 166 deletions(-) create mode 100644 src/views/Dashboard.vue create mode 100644 src/views/ObjectIndex.vue diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index 1640e277..ebd6a4f7 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -159,7 +159,7 @@ "description": "Een generieke voorziening (software product)", "version": "0.0.80", "summary": "", - "icon": null, + "icon": "ApplicationCog", "required": [ "naam" ], @@ -1128,7 +1128,7 @@ "description": "Contactgegevens van een persoon", "version": "0.0.17", "summary": "", - "icon": null, + "icon": "AccountMultiple", "required": [ "organisatie", "e-mailadres" @@ -1353,7 +1353,7 @@ "description": "Een organisatie die voorzieningen aanbiedt", "version": "0.0.90", "summary": "", - "icon": null, + "icon": "OfficeBuildingOutline", "required": [ "naam", "type", @@ -2326,7 +2326,7 @@ "description": "Een formele overeenkomst voor het inzetten van een VoorzieningAanbod op een VoorzieningGebruik", "version": "0.0.4", "summary": "", - "icon": null, + "icon": "FileDocumentEdit", "required": [ "voorzieningAanbod", "voorzieningGebruik", @@ -2533,7 +2533,7 @@ "description": "Een specifieke standaard waaraan een voorziening kan voldoen", "version": "0.0.3", "summary": "", - "icon": null, + "icon": "FileDocumentCheck", "required": [ "naam" ], @@ -2626,7 +2626,7 @@ "description": "Beoordeling van een voorziening", "version": "0.0.3", "summary": "", - "icon": null, + "icon": "Star", "required": [ "voorziening", "score" @@ -5059,7 +5059,7 @@ "description": "Schema voor compliancy en standaard ondersteuning", "version": "0.0.1", "summary": "", - "icon": null, + "icon": "CheckCircle", "required": [], "properties": { "ondersteuntStandaardversie": { @@ -5184,7 +5184,7 @@ "description": "Schema voor module versies", "version": "0.0.1", "summary": "", - "icon": null, + "icon": "ViewModule", "required": [], "properties": { "module": { diff --git a/src/components/GenericObjectTable.vue b/src/components/GenericObjectTable.vue index 43c1b49b..cc1e4b4d 100644 --- a/src/components/GenericObjectTable.vue +++ b/src/components/GenericObjectTable.vue @@ -59,6 +59,20 @@ import { objectStore, navigationStore } from '../store/store.js' + +
+
+ + +
+
+
- + -
-
-

- - {{ getObjectTitle(item) }} -

- - - +
@@ -52,6 +53,7 @@ export default { name: 'OrganisatieIndex', components: { GenericObjectTable, + // eslint-disable-next-line vue/no-unused-components OrganisatieCard, }, data() { @@ -215,6 +217,17 @@ export default { }, }, ], + organisatieFilters: [ + { + key: 'status', + label: 'Status', + options: [ + { value: 'all', label: 'Alle statussen' }, + { value: 'Actief', label: 'Actief' }, + { value: 'concept', label: 'Concept' }, + ], + }, + ], addOrganisatieAction: { id: 'add', label: 'Add Organisatie', From cdefc43d3d99c87d1de67d839ac7a05e18a5e242 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 13 Aug 2025 17:10:18 +0200 Subject: [PATCH 05/83] Create users for contacts --- lib/AppInfo/Application.php | 4 +- lib/Service/OrganizationSyncService.php | 67 +++ .../ContactPersonHandler.php | 386 +++++++++--------- 3 files changed, 262 insertions(+), 195 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 787b180f..13fb046f 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -17,6 +17,7 @@ namespace OCA\SoftwareCatalog\AppInfo; +use OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -182,7 +183,8 @@ public function register(IRegistrationContext $context): void $container->get(IAppConfig::class), $container->get('Psr\Log\LoggerInterface'), $container->get(SettingsService::class), - $container->get(IDBConnection::class) + $container->get(IDBConnection::class), + $container->get(ContactPersonHandler::class), ); }); diff --git a/lib/Service/OrganizationSyncService.php b/lib/Service/OrganizationSyncService.php index 383288f7..73185af0 100644 --- a/lib/Service/OrganizationSyncService.php +++ b/lib/Service/OrganizationSyncService.php @@ -21,6 +21,7 @@ use OCA\OpenRegister\Service\ObjectService; use OCA\SoftwareCatalog\Service\OrganisatieService; use OCA\SoftwareCatalog\Service\ContactpersoonService; +use OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler; use OCA\SoftwareCatalog\Service\SymfonyEmailService; use OCP\IAppConfig; use OCP\IDBConnection; @@ -100,6 +101,7 @@ public function __construct( LoggerInterface $logger, SettingsService $settingsService, private IDBConnection $db, + private readonly ContactPersonHandler $contactpersonHandler, ) { $this->organisatieService = $organisatieService; $this->contactpersoonService = $contactpersoonService; @@ -161,6 +163,69 @@ public function performOrganizationsSync(): array return $stats; } + public function performContactSync() :array + { + // Check configuration + $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); + $register = $voorzieningenConfig['register'] ?? ''; +// $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? ''; + $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? ''; + + $stats = [ + 'organizationsProcessed' => 0, + 'entitiesCreated' => 0, + 'entitiesUpdated' => 0, + 'contactPersonsProcessed' => 0, + 'usersCreated' => 0, + 'usersUpdated' => 0, + 'errors' => [], + 'startTime' => date('Y-m-d H:i:s'), + 'endTime' => null, + 'duration' => null + ]; + + $qb = $this->db->getQueryBuilder(); + + $qb->select( + 'o.uuid', + 'a.uid', + $qb->createFunction('json_unquote(json_extract(o.object, \'$.e-mailadres\')) as email'), + $qb->createFunction('json_unquote(json_extract(o.object, \'$.username\')) as username') + ) + ->from('openregister_objects', 'o') + ->leftJoin( + fromAlias: 'o', + join: 'accounts_data', + alias: 'a', + condition: 'json_unquote(json_extract(o.object, \'$.e-mailadres\')) = a.value') + ->where($qb->expr()->eq('o.register', $qb->createNamedParameter($register))) + ->andWhere($qb->expr()->eq('o.schema', $qb->createNamedParameter($contactSchema))) + ->andWhere($qb->expr()->isNull($qb->createFunction('json_unquote(json_extract(o.object, \'$.username\'))'))); + +// var_dump($qb->getSQL()); + $contacts = $qb->execute()->fetchAll(); + + foreach ($contacts as $contact) { + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); + $contactEntity = $objectService->find($contact['uuid']); + $contactEntityObject = $contactEntity->getObject(); + + $contactEntityObject['username'] = $contact['uid']; + + if ($contact['uid'] === null) { + $user = $this->contactpersonHandler->createUserAccount($contactEntity); + $contactEntityObject['username'] = $user->getUID(); + } + + $contactEntity->setObject($contactEntityObject); + $objectService->saveObject(object: $contactEntity, register: $register, schema: $contactSchema); + + $stats['contactPersonsProcessed']++; + } + + return $stats; + } + /** @@ -948,6 +1013,8 @@ public function performManualSync(int $minutesBack = 0): array try { $syncResults = $this->performOrganizationsSync(); + + $syncResults = array_merge($this->performContactSync(), $syncResults); // die; // Perform the core synchronization with time-based filtering diff --git a/lib/Service/SoftwareCatalogue/ContactPersonHandler.php b/lib/Service/SoftwareCatalogue/ContactPersonHandler.php index 2a468bbd..96643067 100644 --- a/lib/Service/SoftwareCatalogue/ContactPersonHandler.php +++ b/lib/Service/SoftwareCatalogue/ContactPersonHandler.php @@ -86,7 +86,7 @@ private function _getObjectService(): ?\OCA\OpenRegister\Service\ObjectService * Generates a username from contact data with fallback strategies * * @param array $contactData The contact data array - * + * * @return string Generated username */ public function generateUsernameFromContactData(array $contactData): string @@ -97,7 +97,7 @@ public function generateUsernameFromContactData(array $contactData): string $tussenvoegsel = $contactData['tussenvoegsel'] ?? ''; $achternaam = $contactData['achternaam'] ?? ''; $email = $contactData['email'] ?? $contactData['e-mailadres'] ?? ''; - + // Strategy 1: full email address (PRIORITY) @@ -134,7 +134,7 @@ public function generateUsernameFromContactData(array $contactData): string $this->_logger->error('All username generation strategies failed', ['contactData' => $contactData]); return ''; } - + /** * Validates if a username meets Nextcloud requirements */ @@ -143,25 +143,25 @@ private function isValidUsername(string $username): bool if (empty($username)) { return false; } - + // Basic validation rules (adjust based on your Nextcloud configuration) if (strlen($username) < 3 || strlen($username) > 64) { return false; } - + // Must start with alphanumeric if (!preg_match('/^[a-z0-9]/', $username)) { return false; } - + // Only allow alphanumeric, dots, underscores, dashes, and @ symbol (for email addresses) if (!preg_match('/^[a-z0-9._@-]+$/', $username)) { return false; } - + return true; } - + /** * Ensures username is unique by adding counter if needed */ @@ -169,19 +169,19 @@ private function ensureUniqueUsername(string $username): string { $originalUsername = $username; $counter = 1; - + while ($this->_userManager->userExists($username)) { $username = $originalUsername . $counter; $counter++; - + // Safety check to prevent infinite loop if ($counter > 9999) { $username = $originalUsername . uniqid(); break; } } - + return $username; } @@ -189,7 +189,7 @@ private function ensureUniqueUsername(string $username): string * Creates a user account for a contact person * * @param object $contactpersoonObject The contact person object - * + * * @return \OCP\IUser|null The created user or null if failed */ public function createUserAccount(object $contactpersoonObject, bool $isFirstContact = false): ?\OCP\IUser @@ -197,7 +197,7 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon try { $objectData = $contactpersoonObject->getObject(); $email = $objectData['email'] ?? $objectData['e-mailadres'] ?? ''; - + if (empty($email)) { $this->_logger->warning( 'Cannot create user account: no email address provided', @@ -205,14 +205,14 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon ); return null; } - + // Generate username first to check both email and username existence $username = $objectData['username'] ?? ''; if (empty($username)) { $username = $this->generateUsernameFromContactData($objectData); } - + // Check if user already exists by email if ($this->_userManager->userExists($email)) { $this->_logger->info( @@ -226,13 +226,13 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon if (!empty($organizationUuid)) { $this->storeUserOrganizationUuid($existingUser, $organizationUuid); } - + // Update groups for existing user $this->assignUserGroups($existingUser, $objectData, $isFirstContact); return $existingUser; } } - + // Check if user already exists by username $existingUserByUsername = $this->_userManager->get($username); if ($existingUserByUsername) { @@ -240,45 +240,45 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'User already exists with username', ['username' => $username, 'contactpersoonId' => $contactpersoonObject->getId()] ); - + // Store organization UUID for existing user $organizationUuid = $objectData['organisation'] ?? $objectData['organisatie'] ?? ''; if (!empty($organizationUuid)) { $this->storeUserOrganizationUuid($existingUserByUsername, $organizationUuid); } - + // Update groups for existing user $this->assignUserGroups($existingUserByUsername, $objectData, $isFirstContact); return $existingUserByUsername; } - + // Username already generated above for existence checks - + // Create user account $user = $this->_userManager->createUser($username, $username); - + if ($user) { - + // Set user details $user->setEMailAddress($email); $user->setDisplayName($this->getDisplayNameFromContactData($objectData)); - + // Store organization UUID in user config for OpenConnector access $organizationUuid = $objectData['organisation'] ?? $objectData['organisatie'] ?? ''; if (!empty($organizationUuid)) { $this->storeUserOrganizationUuid($user, $organizationUuid); } - + // Set user groups based on roles and organization $this->assignUserGroups($user, $objectData, $isFirstContact); - + // Update contactpersoon with username $objectData['username'] = $username; $contactpersoonObject->setObject($objectData); - + // Send user creation email $this->sendUserCreationEmail($user, $objectData); - + $this->_logger->info( 'Created user account for contact person', [ @@ -287,7 +287,7 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'email' => $email ] ); - + return $user; } else { $this->_logger->error( @@ -299,9 +299,9 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon ] ); } - + return null; - + } catch (\Exception $e) { $this->_logger->error( 'Failed to create user account: ' . $e->getMessage(), @@ -322,7 +322,7 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon * * @param \OCP\IUser $user The user to assign groups to * @param array $objectData The contact person data - * + * * @return void */ private function assignUserGroups(\OCP\IUser $user, array $objectData, bool $isFirstContact = false): void @@ -330,23 +330,21 @@ private function assignUserGroups(\OCP\IUser $user, array $objectData, bool $isF try { $roles = $objectData['roles'] ?? []; $organizationId = $objectData['organisation'] ?? $objectData['organisatie'] ?? ''; - - // Ensure roles is an array if (!is_array($roles)) { $roles = [$roles]; } - + // Get the settings service to access group configurations $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService'); - + // Add user to ALL generic user groups (as requested) $genericGroups = $settingsService->getGenericUserGroups(); foreach ($genericGroups as $groupName) { $this->addUserToGroup($user, $groupName, 'generic-user-group'); } - + // Add user to organization admin groups if this is the first contact if ($isFirstContact) { $organizationAdminGroups = $settingsService->getOrganizationAdminGroups(); @@ -354,14 +352,14 @@ private function assignUserGroups(\OCP\IUser $user, array $objectData, bool $isF $this->addUserToGroup($user, $groupName, 'organization-admin'); } } - + // Add user to organization group if available if (!empty($organizationId)) { $organizationGroup = $this->getOrganizationGroup((string)$organizationId); - + if ($organizationGroup && !$organizationGroup->inGroup($user)) { $organizationGroup->addUser($user); - + // If this is the first contact, make them a subadmin of the organization group if ($isFirstContact) { try { @@ -397,7 +395,7 @@ private function assignUserGroups(\OCP\IUser $user, array $objectData, bool $isF ); } } - + // Check if organization is of type "Gemeente" and add to "ambtenaar" group $organizationType = $this->getOrganizationType((string)$organizationId); if (strtolower($organizationType) === 'gemeente') { @@ -412,7 +410,7 @@ private function assignUserGroups(\OCP\IUser $user, array $objectData, bool $isF ); } } - + $this->_logger->info( 'Successfully assigned user groups', [ @@ -422,7 +420,7 @@ private function assignUserGroups(\OCP\IUser $user, array $objectData, bool $isF 'organizationAdminGroups' => $isFirstContact ? ($organizationAdminGroups ?? []) : [] ] ); - + } catch (\Exception $e) { $this->_logger->error( 'Failed to assign user groups: ' . $e->getMessage(), @@ -458,7 +456,7 @@ private function getAllowedRoleGroups(): array * @param \OCP\IUser $user The user to add * @param string $groupName The group name * @param string $type The type of group assignment (for logging) - * + * * @return void */ private function addUserToGroup(\OCP\IUser $user, string $groupName, string $type): void @@ -474,7 +472,7 @@ private function addUserToGroup(\OCP\IUser $user, string $groupName, string $typ ); } } - + if ($group && !$group->inGroup($user)) { $group->addUser($user); $this->_logger->info( @@ -505,14 +503,14 @@ private function addUserToGroup(\OCP\IUser $user, string $groupName, string $typ * @param \OCP\IUser $user The user to update * @param array $newRoles The new roles * @param array $oldRoles The old roles (optional) - * + * * @return void */ public function updateUserGroupsFromRoles(\OCP\IUser $user, array $newRoles, array $oldRoles = []): void { try { $allowedGroups = $this->getAllowedRoleGroups(); - + // Remove user from groups for roles they no longer have if (!empty($oldRoles)) { $removedRoles = array_diff($oldRoles, $newRoles); @@ -534,7 +532,7 @@ public function updateUserGroupsFromRoles(\OCP\IUser $user, array $newRoles, arr } } } - + // Add user to groups for new roles foreach ($newRoles as $role) { if (in_array($role, array_keys($allowedGroups))) { @@ -542,11 +540,11 @@ public function updateUserGroupsFromRoles(\OCP\IUser $user, array $newRoles, arr $this->addUserToGroup($user, $groupName, 'role-update'); } } - + // Ensure organization type-based groups are preserved // (e.g., "ambtenaar" for Gemeente organizations) $this->ensureOrganizationTypeGroups($user); - + } catch (\Exception $e) { $this->_logger->error( 'Failed to update user groups from roles: ' . $e->getMessage(), @@ -562,7 +560,7 @@ public function updateUserGroupsFromRoles(\OCP\IUser $user, array $newRoles, arr * Ensures organization type-based groups are assigned to the user * * @param \OCP\IUser $user The user to check and update - * + * * @return void */ private function ensureOrganizationTypeGroups(\OCP\IUser $user): void @@ -571,21 +569,21 @@ private function ensureOrganizationTypeGroups(\OCP\IUser $user): void // Find the user's organization by looking for their contactpersoon $objectService = $this->_getObjectService(); $contactpersoon = $this->findContactpersoonByUsername($user->getUID()); - + if ($contactpersoon) { $contactData = $contactpersoon->getObject(); $organizationId = $contactData['organisation'] ?? ''; - + if (!empty($organizationId)) { $organizationType = $this->getOrganizationType($organizationId); - + // If organization is Gemeente, ensure user is in ambtenaar group if (strtolower($organizationType) === 'gemeente') { $this->addUserToGroup($user, 'ambtenaar', 'gemeente-organization-preserve'); } } } - + } catch (\Exception $e) { $this->_logger->error( 'Failed to ensure organization type groups: ' . $e->getMessage(), @@ -601,7 +599,7 @@ private function ensureOrganizationTypeGroups(\OCP\IUser $user): void * Finds contactpersoon object by username * * @param string $username The username to search for - * + * * @return object|null The contactpersoon object or null if not found */ private function findContactpersoonByUsername(string $username): ?object @@ -609,28 +607,28 @@ private function findContactpersoonByUsername(string $username): ?object try { $objectService = $this->_getObjectService(); $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService'); - + // Get configuration values $registerId = $settingsService->getVoorzieningenRegisterId(); $contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType('contactpersoon'); - + if (!$registerId || !$contactpersoonSchemaId) { throw new \Exception('Register or schema ID not configured for contactpersoon'); } - + // Search for contactpersoon with the given username $searchFilters = [ 'username' => $username ]; - + $results = $objectService->findAll($searchFilters, $registerId, $contactpersoonSchemaId); - + if (!empty($results)) { return $results[0]; // Return the first match } - + return null; - + } catch (\Exception $e) { $this->_logger->error( 'Failed to find contactpersoon by username: ' . $e->getMessage(), @@ -647,7 +645,7 @@ private function findContactpersoonByUsername(string $username): ?object * Gets the organization group for a given organization ID * * @param string $organizationId The organization ID - * + * * @return \OCP\IGroup|null The organization group or null if not found */ private function getOrganizationGroup(string $organizationId): ?\OCP\IGroup @@ -658,32 +656,32 @@ private function getOrganizationGroup(string $organizationId): ?\OCP\IGroup if (!$objectService) { return null; } - + // Get register and schema IDs dynamically from configuration $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService'); $registerId = $settingsService->getVoorzieningenRegisterId(); $organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie'); - + if (!$registerId || !$organisatieSchemaId) { $this->_logger->warning('Register or schema ID not configured for organisatie'); return null; } - + // Use find() method with proper register/schema context $organizationObject = $objectService->find($organizationId, [], false, $registerId, $organisatieSchemaId); - + if ($organizationObject) { $organizationData = $organizationObject->getObject(); $groupId = $organizationData['group'] ?? ''; - + if (!empty($groupId)) { $group = $this->_groupManager->get($groupId); return $group; } } - + return null; - + } catch (\Exception $e) { $this->_logger->error( 'Failed to get organization group: ' . $e->getMessage(), @@ -701,7 +699,7 @@ private function getOrganizationGroup(string $organizationId): ?\OCP\IGroup * * @param object $contactObject The contact object being processed (contactpersoon) * @param array $objectData The contact data - * + * * @return bool True if this is the first contact for the organization */ private function isFirstContactForOrganization(object $contactObject, array $objectData): bool @@ -709,12 +707,12 @@ private function isFirstContactForOrganization(object $contactObject, array $obj try { $organizationId = $objectData['organisation'] ?? $objectData['organisatie'] ?? ''; $currentContactId = $contactObject->getId(); - + if (empty($organizationId)) { $this->_logger->warning('No organization ID found for contact object'); return false; } - + $this->_logger->info( 'Checking if contact is first for organization', [ @@ -722,18 +720,18 @@ private function isFirstContactForOrganization(object $contactObject, array $obj 'organizationId' => $organizationId ] ); - + // Simple approach: Check if any OTHER users exist with this organization UUID $objectService = $this->_getObjectService(); if (!$objectService) { $this->_logger->error('ObjectService not available for first contact check'); return false; } - + // Get settings for schema IDs $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService'); $registerId = $settingsService->getVoorzieningenRegisterId(); - + // Check contactpersoon schema $contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType('contactpersoon'); if ($contactpersoonSchemaId) { @@ -742,12 +740,12 @@ private function isFirstContactForOrganization(object $contactObject, array $obj $registerId, $contactpersoonSchemaId ); - + // Filter out the current contact being processed $otherContacts = array_filter($existingContacts, function($contact) use ($currentContactId) { return $contact->getId() !== $currentContactId; }); - + $this->_logger->info( 'Found existing contacts for organization', [ @@ -758,15 +756,15 @@ private function isFirstContactForOrganization(object $contactObject, array $obj 'isFirstContact' => empty($otherContacts) ] ); - + // If there are any OTHER existing contacts, this is not the first if (!empty($otherContacts)) { return false; } } - + return true; - + } catch (\Exception $e) { $this->_logger->error( 'Failed to determine if first contact: ' . $e->getMessage(), @@ -788,7 +786,7 @@ private function isFirstContactForOrganization(object $contactObject, array $obj * * @param IUser $user The user object * @param string|int $organizationUuid The organization UUID (can be string or int) - * + * * @return void */ private function storeUserOrganizationUuid(IUser $user, string|int $organizationUuid): void @@ -797,14 +795,14 @@ private function storeUserOrganizationUuid(IUser $user, string|int $organization if (!empty($organizationUuid)) { // Convert to string to ensure consistent storage $organizationUuidStr = (string)$organizationUuid; - + $this->_config->setUserValue( $user->getUID(), 'core', 'organisation', $organizationUuidStr ); - + $this->_logger->info( 'Stored organization UUID in user config', [ @@ -831,7 +829,7 @@ private function storeUserOrganizationUuid(IUser $user, string|int $organization * Gets a display name from contact data * * @param array $contactData The contact data - * + * * @return string The display name */ private function getDisplayNameFromContactData(array $contactData): string @@ -841,7 +839,7 @@ private function getDisplayNameFromContactData(array $contactData): string $contactData['tussenvoegsel'] ?? '', $contactData['achternaam'] ?? '' ]); - + return implode(' ', $parts) ?: ($contactData['email'] ?? $contactData['e-mailadres'] ?? 'Unknown User'); } @@ -849,7 +847,7 @@ private function getDisplayNameFromContactData(array $contactData): string * Handles new contact creation * * @param object $contactObject The contact object - * + * * @return void */ public function handleNewContact(object $contactObject): void @@ -877,7 +875,7 @@ public function handleNewContact(object $contactObject): void * Handles contact update * * @param object $contactObject The contact object - * + * * @return void */ public function handleContactUpdate(object $contactObject): void @@ -905,7 +903,7 @@ public function handleContactUpdate(object $contactObject): void * Handles contact deletion * * @param object $contactObject The contact object - * + * * @return void */ public function handleContactDeletion(object $contactObject): void @@ -924,10 +922,10 @@ public function handleContactDeletion(object $contactObject): void if ($user) { // Option 1: Delete the user account // $user->delete(); - + // Option 2: Just disable the user $user->setEnabled(false); - + $this->_logger->info( 'User account disabled due to contact deletion', [ @@ -935,7 +933,7 @@ public function handleContactDeletion(object $contactObject): void 'contactId' => $contactObject->getId() ] ); - + // Send account suspension notification email $this->sendAccountSuspensionEmail($user, $objectData); } @@ -958,7 +956,7 @@ public function handleContactDeletion(object $contactObject): void * @param object $contactpersoonObject The contactpersoon object * @param string $username The username * @param string $organizationUuid The organization UUID - * + * * @return void */ public function assignBeheerderRole(object $contactpersoonObject, string $username, string $organizationUuid): void @@ -966,19 +964,19 @@ public function assignBeheerderRole(object $contactpersoonObject, string $userna try { $objectData = $contactpersoonObject->getObject(); $currentRoles = $objectData['roles'] ?? []; - + if (!is_array($currentRoles)) { $currentRoles = []; } - + // Add beheerder role if not already present if (!in_array('beheerder', array_map('strtolower', $currentRoles))) { $currentRoles[] = 'beheerder'; - + // Update the contactpersoon object (but don't save to prevent event loops) $objectData['roles'] = $currentRoles; $contactpersoonObject->setObject($objectData); - + // Note: NOT saving the object here to prevent infinite event loops // The original API call/operation will handle persistence $this->_logger->info('Beheerder role added to contactpersoon object, but not saved to prevent event loops', [ @@ -987,20 +985,20 @@ public function assignBeheerderRole(object $contactpersoonObject, string $userna 'updatedRoles' => $currentRoles, 'objectId' => $contactpersoonObject->getId() ]); - + // Add user to beheerder group $beheerderGroup = $this->_groupManager->get('beheerder'); if (!$beheerderGroup) { $beheerderGroup = $this->_groupManager->createGroup('beheerder'); } - + if ($beheerderGroup) { $user = $this->_userManager->get($username); if ($user && !$beheerderGroup->inGroup($user)) { $beheerderGroup->addUser($user); } } - + $this->_logger->info( 'Assigned beheerder role to first user in organization', [ @@ -1010,7 +1008,7 @@ public function assignBeheerderRole(object $contactpersoonObject, string $userna ] ); } - + } catch (\Exception $e) { $this->_logger->error( 'Failed to assign beheerder role: ' . $e->getMessage(), @@ -1028,7 +1026,7 @@ public function assignBeheerderRole(object $contactpersoonObject, string $userna * * @param string $username The username * @param string $managerUsername The manager's username - * + * * @return void */ public function setUserManager(string $username, string $managerUsername): void @@ -1036,7 +1034,7 @@ public function setUserManager(string $username, string $managerUsername): void try { $user = $this->_userManager->get($username); $manager = $this->_userManager->get($managerUsername); - + if (!$user || !$manager) { $this->_logger->warning( 'Cannot set manager - user or manager not found', @@ -1049,7 +1047,7 @@ public function setUserManager(string $username, string $managerUsername): void ); return; } - + // In Nextcloud, we can set this as a user preference or custom attribute // Since there's no built-in manager field, we'll use preferences \OC::$server->getConfig()->setUserValue( @@ -1058,7 +1056,7 @@ public function setUserManager(string $username, string $managerUsername): void 'manager', $managerUsername ); - + $this->_logger->info( 'Set user manager', [ @@ -1066,7 +1064,7 @@ public function setUserManager(string $username, string $managerUsername): void 'manager' => $managerUsername ] ); - + } catch (\Exception $e) { $this->_logger->error( 'Failed to set user manager: ' . $e->getMessage(), @@ -1083,7 +1081,7 @@ public function setUserManager(string $username, string $managerUsername): void * Gets a user's manager * * @param string $username The username - * + * * @return string|null The manager's username or null if not set */ public function getUserManager(string $username): ?string @@ -1095,9 +1093,9 @@ public function getUserManager(string $username): ?string 'manager', '' ); - + return !empty($manager) ? $manager : null; - + } catch (\Exception $e) { $this->_logger->error( 'Failed to get user manager: ' . $e->getMessage(), @@ -1114,7 +1112,7 @@ public function getUserManager(string $username): ?string * Gets the organization type for a given organization ID * * @param string $organizationId The organization ID - * + * * @return string The organization type or empty string if not found */ private function getOrganizationType(string $organizationId): string @@ -1122,27 +1120,27 @@ private function getOrganizationType(string $organizationId): string try { // Get the organization object to find its type $objectService = $this->_getObjectService(); - + // Get register and schema IDs dynamically from configuration $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService'); $registerId = $settingsService->getVoorzieningenRegisterId(); $organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie'); - + if (!$registerId || !$organisatieSchemaId) { $this->_logger->warning('Register or schema ID not configured for organisatie'); return ''; } - + // Try to find by UUID first, then by database ID if needed $organizationObject = $objectService->find($organizationId, [], false, $registerId, $organisatieSchemaId); - + if ($organizationObject) { $organizationData = $organizationObject->getObject(); return $organizationData['type'] ?? ''; } - + return ''; - + } catch (\Exception $e) { $this->_logger->error( 'Failed to get organization type: ' . $e->getMessage(), @@ -1160,7 +1158,7 @@ private function getOrganizationType(string $organizationId): string * * @param \OCP\IUser $user The created user * @param array $objectData The contact person data - * + * * @return void */ private function sendUserCreationEmail(\OCP\IUser $user, array $objectData): void @@ -1170,7 +1168,7 @@ private function sendUserCreationEmail(\OCP\IUser $user, array $objectData): voi 'username' => $user->getUID(), 'email' => $user->getEMailAddress() ]); - + // Prepare user data for email $userData = [ 'username' => $user->getUID(), @@ -1180,7 +1178,7 @@ private function sendUserCreationEmail(\OCP\IUser $user, array $objectData): voi 'achternaam' => $objectData['achternaam'] ?? '', 'roles' => $objectData['roles'] ?? [] ]; - + // Get organization data if available $organizationData = []; $organizationId = $objectData['organisation'] ?? $objectData['organisatie'] ?? ''; @@ -1191,12 +1189,12 @@ private function sendUserCreationEmail(\OCP\IUser $user, array $objectData): voi $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService'); $registerId = $settingsService->getVoorzieningenRegisterId(); $organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie'); - + if (!$registerId || !$organisatieSchemaId) { $this->_logger->warning('Register or schema ID not configured for organisatie'); return; } - + $organizationObject = $objectService->find($organizationId, [], false, $registerId, $organisatieSchemaId); if ($organizationObject) { $organizationData = $organizationObject->getObject(); @@ -1212,10 +1210,10 @@ private function sendUserCreationEmail(\OCP\IUser $user, array $objectData): voi ]); } } - + // Send user creation email $success = $this->_emailService->sendUserCreationEmail($userData, $organizationData); - + if ($success) { $this->_logger->info('User creation email sent successfully', [ 'username' => $user->getUID(), @@ -1227,7 +1225,7 @@ private function sendUserCreationEmail(\OCP\IUser $user, array $objectData): voi 'email' => $user->getEMailAddress() ]); } - + } catch (\Exception $e) { $this->_logger->error('Exception sending user creation email: ' . $e->getMessage(), [ 'username' => $user->getUID(), @@ -1245,7 +1243,7 @@ private function sendUserCreationEmail(\OCP\IUser $user, array $objectData): voi * * @param object $contactpersoonObject The contactpersoon object to process * @param bool $isUpdate Whether this is an update operation (defaults to false) - * + * * @return bool True if processing was successful * @throws \Exception If processing fails */ @@ -1259,99 +1257,99 @@ public function processContactpersoon(object $contactpersoonObject, bool $isUpda // Get object data $objectData = $contactpersoonObject->getObject(); - + // Check if username exists and is filled $username = $objectData['username'] ?? ''; - + if (empty($username)) { $this->_logger->info('Username not found or empty, creating inactive user account'); - + // Generate username from name fields $username = $this->generateUsernameFromContactData($objectData); - + // For updates, try to find existing user first to avoid expensive isFirstContactForOrganization check if ($isUpdate) { $existingUser = $this->_userManager->get($username); - + if ($existingUser) { $this->_logger->info('Found existing user during update, skipping expensive first contact check', [ 'username' => $username, 'objectId' => $contactpersoonObject->getId() ]); - + // Update the contactpersoon object with the username (but don't save to prevent event loops) $objectData['username'] = $username; $contactpersoonObject->setObject($objectData); - + $this->_logger->info('Username added to contactpersoon object during update, but not saved to prevent event loops', [ 'username' => $username, 'objectId' => $contactpersoonObject->getId() ]); - + // Ensure contactpersoon is added to organization $this->ensureContactpersoonInOrganization($contactpersoonObject); - + return true; } } - + // Determine if this is the first contact for the organization (expensive operation) $isFirstContact = $this->isFirstContactForOrganization($contactpersoonObject, $objectData); - + // Create the user account $user = $this->createUserAccount($contactpersoonObject, $isFirstContact); - + if ($user === null) { throw new \Exception('Failed to create user account'); } - + // Set user to inactive initially $this->setUserInactive($user->getUID()); - + // Update the contactpersoon object with the username (but don't save to prevent event loops) $objectData['username'] = $username; $contactpersoonObject->setObject($objectData); - + // Note: NOT saving the object here to prevent infinite event loops // The original API call/operation will handle persistence $this->_logger->info('Username added to contactpersoon object, but not saved to prevent event loops', [ 'username' => $username, 'objectId' => $contactpersoonObject->getId() ]); - + // Ensure contactpersoon is added to organization $this->ensureContactpersoonInOrganization($contactpersoonObject); - + // Also add user to organization entity (OpenRegister entity, not object) $this->addUserToOrganizationEntity($contactpersoonObject, $username); - + $this->_logger->info( - 'Successfully created inactive user and updated contactpersoon', + 'Successfully created inactive user and updated contactpersoon', [ 'username' => $username, 'objectId' => $contactpersoonObject->getId() ] ); - + return true; } - + $this->_logger->info( - 'Username already exists, contactpersoon processed', + 'Username already exists, contactpersoon processed', [ 'username' => $username, 'objectId' => $contactpersoonObject->getId() ] ); - + // Ensure contactpersoon is added to organization (even for existing users) $this->ensureContactpersoonInOrganization($contactpersoonObject); - + return true; - + } catch (\Exception $e) { $this->_logger->error( - 'Failed to process contactpersoon object: ' . $e->getMessage(), + 'Failed to process contactpersoon object: ' . $e->getMessage(), [ 'exception' => $e, 'objectId' => $contactpersoonObject->getId() ?? 'unknown' @@ -1365,24 +1363,24 @@ public function processContactpersoon(object $contactpersoonObject, bool $isUpda * Sets a user account to inactive * * @param string $username The username to set as inactive - * + * * @return bool True if successful */ public function setUserInactive(string $username): bool { try { $user = $this->_userManager->get($username); - + if ($user) { $user->setEnabled(false); - + $this->_logger->info( 'Set user account to inactive', [ 'username' => $username ] ); - + return true; } else { $this->_logger->warning( @@ -1391,10 +1389,10 @@ public function setUserInactive(string $username): bool 'username' => $username ] ); - + return false; } - + } catch (\Exception $e) { $this->_logger->error( 'Failed to set user inactive: ' . $e->getMessage(), @@ -1403,7 +1401,7 @@ public function setUserInactive(string $username): bool 'exception' => $e ] ); - + return false; } } @@ -1412,24 +1410,24 @@ public function setUserInactive(string $username): bool * Sets a user account to active * * @param string $username The username to set as active - * + * * @return bool True if successful */ public function setUserActive(string $username): bool { try { $user = $this->_userManager->get($username); - + if ($user) { $user->setEnabled(true); - + $this->_logger->info( 'Set user account to active', [ 'username' => $username ] ); - + return true; } else { $this->_logger->warning( @@ -1438,10 +1436,10 @@ public function setUserActive(string $username): bool 'username' => $username ] ); - + return false; } - + } catch (\Exception $e) { $this->_logger->error( 'Failed to set user active: ' . $e->getMessage(), @@ -1450,7 +1448,7 @@ public function setUserActive(string $username): bool 'exception' => $e ] ); - + return false; } } @@ -1460,7 +1458,7 @@ public function setUserActive(string $username): bool * * @param object $contactpersoonObject The updated contactpersoon object * @param object $oldContactpersoonObject The previous contactpersoon object - * + * * @return void */ public function handleContactpersoonUpdate(object $contactpersoonObject, object $oldContactpersoonObject): void @@ -1472,14 +1470,14 @@ public function handleContactpersoonUpdate(object $contactpersoonObject, object // Process the updated contactpersoon $this->processContactpersoon($contactpersoonObject); - + // Check for role changes and update groups accordingly $newData = $contactpersoonObject->getObject(); $oldData = $oldContactpersoonObject->getObject(); - + $newRoles = $newData['roles'] ?? []; $oldRoles = $oldData['roles'] ?? []; - + // Ensure both are arrays if (!is_array($newRoles)) { $newRoles = [$newRoles]; @@ -1487,7 +1485,7 @@ public function handleContactpersoonUpdate(object $contactpersoonObject, object if (!is_array($oldRoles)) { $oldRoles = [$oldRoles]; } - + // Check if roles have changed if ($newRoles !== $oldRoles) { $username = $newData['username'] ?? ''; @@ -1503,7 +1501,7 @@ public function handleContactpersoonUpdate(object $contactpersoonObject, object 'newRoles' => $newRoles ] ); - + // Update user groups based on role changes $this->updateUserGroupsFromRoles($user, $newRoles, $oldRoles); } @@ -1526,7 +1524,7 @@ public function handleContactpersoonUpdate(object $contactpersoonObject, object * * @param \OCP\IUser $user The suspended user * @param array $objectData The contact person data - * + * * @return void */ private function sendAccountSuspensionEmail(\OCP\IUser $user, array $objectData): void @@ -1536,17 +1534,17 @@ private function sendAccountSuspensionEmail(\OCP\IUser $user, array $objectData) 'username' => $user->getUID(), 'email' => $user->getEMailAddress() ]); - - // For now, we'll use a simple log message as the PhpEmailService + + // For now, we'll use a simple log message as the PhpEmailService // doesn't have a specific suspension email method yet // This can be extended later if needed - + $this->_logger->info('Account suspension email would be sent here', [ 'username' => $user->getUID(), 'email' => $user->getEMailAddress(), 'displayName' => $user->getDisplayName() ]); - + } catch (\Exception $e) { $this->_logger->error('Exception sending account suspension email: ' . $e->getMessage(), [ 'username' => $user->getUID(), @@ -1560,7 +1558,7 @@ private function sendAccountSuspensionEmail(\OCP\IUser $user, array $objectData) * Checks if a contactpersoon username is in the organization's users list * * @param object $contactpersoonObject The contactpersoon object - * + * * @return bool True if the user should be added to the organization */ public function shouldAddContactpersoonToOrganization(object $contactpersoonObject): bool @@ -1591,10 +1589,10 @@ public function shouldAddContactpersoonToOrganization(object $contactpersoonObje try { $organizationObject = $objectService->find($organizationUuid, [], false, $registerId, $organisatieSchemaId); $organizationData = $organizationObject->getObject(); - + // Check if the username is already in the organization's users $organizationUsers = $organizationData['users'] ?? []; - + if (is_array($organizationUsers) && !in_array($username, $organizationUsers)) { $this->_logger->info('ContactPersonHandler: Contactpersoon should be added to organization', [ 'username' => $username, @@ -1631,7 +1629,7 @@ public function shouldAddContactpersoonToOrganization(object $contactpersoonObje * Adds a contactpersoon username to the organization's users list * * @param object $contactpersoonObject The contactpersoon object - * + * * @return bool True if the user was successfully added */ public function addContactpersoonToOrganization(object $contactpersoonObject): bool @@ -1668,17 +1666,17 @@ public function addContactpersoonToOrganization(object $contactpersoonObject): b try { $organizationObject = $objectService->find($organizationUuid, [], false, $registerId, $organisatieSchemaId); $organizationData = $organizationObject->getObject(); - + // Add the username to the organization's users list $organizationUsers = $organizationData['users'] ?? []; if (!is_array($organizationUsers)) { $organizationUsers = []; } - + if (!in_array($username, $organizationUsers)) { $organizationUsers[] = $username; $organizationData['users'] = $organizationUsers; - + // Update the organization object $updatedOrganization = $objectService->saveObject( $organizationData, @@ -1730,7 +1728,7 @@ public function addContactpersoonToOrganization(object $contactpersoonObject): b * Ensures contactpersoon is added to organization after user creation/update * * @param object $contactpersoonObject The contactpersoon object - * + * * @return void */ public function ensureContactpersoonInOrganization(object $contactpersoonObject): void @@ -1744,7 +1742,7 @@ public function ensureContactpersoonInOrganization(object $contactpersoonObject) if ($this->shouldAddContactpersoonToOrganization($contactpersoonObject)) { // Add user to organization $result = $this->addContactpersoonToOrganization($contactpersoonObject); - + if ($result) { $this->_logger->info('ContactPersonHandler: Successfully ensured contactpersoon in organization', [ 'objectId' => $contactpersoonObject->getId() @@ -1773,13 +1771,13 @@ public function ensureContactpersoonInOrganization(object $contactpersoonObject) ); } } - + /** * Adds a user to the organization entity (OpenRegister entity, not object) * * @param object $contactpersoonObject The contactpersoon object * @param string $username The username to add - * + * * @return void */ private function addUserToOrganizationEntity(object $contactpersoonObject, string $username): void @@ -1787,7 +1785,7 @@ private function addUserToOrganizationEntity(object $contactpersoonObject, strin try { $objectData = $contactpersoonObject->getObject(); $organizationUuid = $objectData['organisation'] ?? $objectData['organisatie'] ?? ''; - + if (empty($organizationUuid)) { $this->_logger->warning('ContactPersonHandler: No organization reference found for contact person', [ 'objectId' => $contactpersoonObject->getId(), @@ -1795,24 +1793,24 @@ private function addUserToOrganizationEntity(object $contactpersoonObject, strin ]); return; } - + $this->_logger->info('ContactPersonHandler: Adding user to organization entity', [ 'objectId' => $contactpersoonObject->getId(), 'username' => $username, 'organizationUuid' => $organizationUuid ]); - + try { $organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper'); $organisation = $organisationMapper->findByUuid($organizationUuid); - + if ($organisation) { $currentUsers = $organisation->getUsers() ?? []; if (!in_array($username, $currentUsers)) { $currentUsers[] = $username; $organisation->setUsers($currentUsers); $organisationMapper->save($organisation); - + $this->_logger->info('ContactPersonHandler: Successfully added user to organization entity', [ 'objectId' => $contactpersoonObject->getId(), 'username' => $username, @@ -1850,4 +1848,4 @@ private function addUserToOrganizationEntity(object $contactpersoonObject, strin } } -} \ No newline at end of file +} From 7f5385276e2a301e90a056765fbd7d30cfe8624d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 14 Aug 2025 06:10:57 +0000 Subject: [PATCH 06/83] Bump version to 0.1.39 [skip ci] --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 1e145948..b91ffc8c 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Submit a [bug report](https://github.com/OpenCatalogi/.github/issues/new/choose) Submit a [feature request](https://github.com/OpenCatalogi/.github/issues/new/choose). ]]> - 0.1.38 + 0.1.39 agpl organization Conduction From 746aabfbeccbb7f842a9290d7aa60cffd1e22b79 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 14 Aug 2025 08:12:27 +0200 Subject: [PATCH 07/83] Organisation activation --- appinfo/routes.php | 3 + lib/Controller/SettingsController.php | 33 ++ lib/Service/ArchiMateService.php | 172 ++++++++-- lib/Service/ArchiMateService_backup.php | 15 +- lib/Service/SettingsService.php | 24 ++ src/components/GenericObjectTable.vue | 6 +- src/components/cards/OrganisatieCard.vue | 6 + src/dialogs/Dialogs.vue | 3 + .../object/ChangeOrganisatieStatusDialog.vue | 164 ++++++++++ src/views/Dashboard.vue | 174 ++++++++-- src/views/organisaties/OrganisatieIndex.vue | 296 ++++++++++++++++-- src/views/settings/Settings.vue | 132 ++++++++ 12 files changed, 948 insertions(+), 80 deletions(-) create mode 100644 src/modals/object/ChangeOrganisatieStatusDialog.vue diff --git a/appinfo/routes.php b/appinfo/routes.php index c88b7450..b04f450d 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -116,5 +116,8 @@ // User Groups focused endpoints ['name' => 'settings#getUserGroupsConfig', 'url' => '/api/user-groups/config', 'verb' => 'GET'], ['name' => 'settings#updateUserGroupsConfig', 'url' => '/api/user-groups/config', 'verb' => 'POST'], + + // Catalog location endpoint + ['name' => 'settings#updateCatalogLocation', 'url' => '/api/settings/catalog-location', 'verb' => 'POST'], ], ]; diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index a1e03d2c..9168d35f 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -2129,6 +2129,39 @@ public function updateUserGroupsConfig(): JSONResponse } } + /** + * Update catalog location + * + * @NoCSRFRequired + * + * @return JSONResponse Update result + */ + public function updateCatalogLocation(): JSONResponse + { + try { + $data = $this->request->getParams(); + $catalogLocation = $data['catalogLocation'] ?? ''; + + $this->settingsService->setCatalogLocation($catalogLocation); + + return new JSONResponse([ + 'success' => true, + 'message' => 'Catalog location updated successfully', + 'catalogLocation' => $catalogLocation + ]); + + } catch (\Exception $e) { + $this->logger->error('Failed to update catalog location', [ + 'exception' => $e->getMessage(), + 'requestData' => $this->request->getParams() + ]); + return new JSONResponse([ + 'success' => false, + 'message' => 'Failed to update catalog location: ' . $e->getMessage() + ], 500); + } + } + }//end class diff --git a/lib/Service/ArchiMateService.php b/lib/Service/ArchiMateService.php index 871b79bd..a9f099ad 100644 --- a/lib/Service/ArchiMateService.php +++ b/lib/Service/ArchiMateService.php @@ -66,6 +66,12 @@ class ArchiMateService 'property_definition' => 105 ]; + /** + * Storage for the last save operation results + * Contains the structured return from ObjectService::saveObjects + */ + private ?array $lastSaveResult = null; + /** * Constructor for ArchiMateService * @@ -510,15 +516,38 @@ private function createOrUpdateModelObject(array $modelMetadata): array ]; // Save the model object using ObjectService::saveObjects - $savedObjects = $objectService->saveObjects([$modelData]); + $saveResult = $objectService->saveObjects([$modelData]); + + // Extract the saved/updated objects from the new structured return format + $savedObjects = array_merge( + $saveResult['saved'] ?? [], + $saveResult['updated'] ?? [] + ); if (empty($savedObjects)) { + // Check if there are validation errors + if (!empty($saveResult['invalid'])) { + $errorMsg = $saveResult['invalid'][0]['error'] ?? 'Model object failed validation'; + $this->logger->error('ArchiMateService: Model object failed validation', [ + 'error' => $errorMsg, + 'model_id' => $modelIdentifier + ]); + return ['success' => false, 'error' => "Validation failed: $errorMsg"]; + } + $this->logger->error('ArchiMateService: Failed to save model object'); return ['success' => false, 'error' => 'Failed to save model object']; } $savedModelObject = $savedObjects[0]; - $modelAction = $this->determineObjectAction($savedModelObject); + + // Determine if this was a create or update operation + $modelAction = 'unknown'; + if (!empty($saveResult['saved'])) { + $modelAction = 'created'; + } elseif (!empty($saveResult['updated'])) { + $modelAction = 'updated'; + } $this->logger->info('ArchiMateService: Saved model object', [ 'model_id' => $modelIdentifier, @@ -536,24 +565,7 @@ private function createOrUpdateModelObject(array $modelMetadata): array } } - /** - * Determine the action taken on an object during save operation - * - * @param array $savedObject The saved object returned from ObjectService::saveObjects - * @return string The action taken: 'created', 'updated', or 'unknown' - */ - private function determineObjectAction(array $savedObject): string - { - // Check if the object was created or updated based on the response - if (isset($savedObject['@self']['id'])) { - // If we have an ID, the object was saved successfully - // We can't easily determine if it was created or updated from the response - // For now, assume it was updated if it has an ID - return 'updated'; - } - - return 'unknown'; - } + // Method removed - action is now determined directly from ObjectService::saveObjects structured return /** * Normalize ArchiMate data structure for storage as JSON blob @@ -1288,15 +1300,42 @@ private function saveObjectsToDatabase(array $objects): array } // Save objects using ObjectService::saveObjects with proper @self structure - $savedObjects = $objectService->saveObjects( + $saveResult = $objectService->saveObjects( objects: $objects, register: $registerId ); + // Store the save result for later access to statistics + $this->lastSaveResult = $saveResult; + + // Extract saved objects from the new structured return format + $savedObjects = array_merge( + $saveResult['saved'] ?? [], + $saveResult['updated'] ?? [] + ); + + // Log detailed results including validation errors $this->logger->info('Objects saved successfully', [ - 'saved_count' => count($savedObjects) + 'saved_count' => count($saveResult['saved'] ?? []), + 'updated_count' => count($saveResult['updated'] ?? []), + 'skipped_count' => count($saveResult['skipped'] ?? []), + 'invalid_count' => count($saveResult['invalid'] ?? []), + 'error_count' => count($saveResult['errors'] ?? []), + 'total_processed' => $saveResult['statistics']['totalProcessed'] ?? 0 ]); + // Log any validation errors for debugging + if (!empty($saveResult['invalid'])) { + foreach ($saveResult['invalid'] as $invalidItem) { + $this->logger->warning('Object failed validation during import', [ + 'object_id' => $invalidItem['object']['@self']['id'] ?? 'unknown', + 'error' => $invalidItem['error'] ?? 'Unknown validation error', + 'type' => $invalidItem['type'] ?? 'ValidationException' + ]); + } + } + + // Return the combined saved and updated objects (maintaining backward compatibility) return $savedObjects; } @@ -1914,17 +1953,80 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb 'property_definitions' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []] ]; - // Count objects by type from normalized data - $sections = ['elements', 'relationships', 'organizations', 'views', 'property_definitions']; - foreach ($sections as $section) { - if (isset($normalizedData[$section])) { - $count = count($normalizedData[$section]); - // Assume all objects were created (we can refine this later with actual save results) - $statistics[$section]['created'] = $count; + // If we have access to the actual save results from ObjectService, use those + if ($this->lastSaveResult !== null) { + $saveResult = $this->lastSaveResult; + + // Count objects by section type from the actual saved objects + $allProcessedObjects = array_merge( + $saveResult['saved'] ?? [], + $saveResult['updated'] ?? [], + $saveResult['skipped'] ?? [], + // For invalid objects, extract the original object from the error structure + array_map(fn($item) => $item['object'] ?? [], $saveResult['invalid'] ?? []) + ); + + foreach ($allProcessedObjects as $object) { + $sectionType = $object['section'] ?? 'elements'; // Default to elements if section not found + + // Map section types to statistics keys + $sectionKey = match($sectionType) { + 'elements' => 'elements', + 'relationships' => 'relationships', + 'organizations' => 'organizations', + 'views' => 'views', + 'property_definitions' => 'property_definitions', + default => 'elements' // Default fallback + }; + + if (!isset($statistics[$sectionKey])) { + continue; // Skip unknown section types + } + + // Determine if this object was created, updated, or had errors + $objectId = $object['@self']['id'] ?? $object['identifier'] ?? null; + + // Check if this object is in the saved (created) list + $wasCreated = !empty(array_filter($saveResult['saved'] ?? [], + fn($saved) => ($saved->getUuid() === $objectId))); + + // Check if this object is in the updated list + $wasUpdated = !empty(array_filter($saveResult['updated'] ?? [], + fn($updated) => ($updated->getUuid() === $objectId))); + + // Check if this object had validation errors + $hasErrors = !empty(array_filter($saveResult['invalid'] ?? [], + fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId))); + + if ($wasCreated) { + $statistics[$sectionKey]['created']++; + } elseif ($wasUpdated) { + $statistics[$sectionKey]['updated']++; + } elseif ($hasErrors) { + // Add to errors array for this section + $errorInfo = array_filter($saveResult['invalid'] ?? [], + fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId)); + + if (!empty($errorInfo)) { + $statistics[$sectionKey]['errors'][] = array_values($errorInfo)[0]['error'] ?? 'Unknown validation error'; + } + } else { + $statistics[$sectionKey]['skipped']++; + } + } + } else { + // Fallback to old method if no save result is available + $sections = ['elements', 'relationships', 'organizations', 'views', 'property_definitions']; + foreach ($sections as $section) { + if (isset($normalizedData[$section])) { + $count = count($normalizedData[$section]); + // Assume all objects were created (legacy behavior) + $statistics[$section]['created'] = $count; + } } } - // Calculate summary totals + // Calculate summary totals from actual statistics $summary = [ 'total_objects_created' => 0, 'total_objects_updated' => 0, @@ -1934,10 +2036,12 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb ]; foreach ($statistics as $section => $sectionStats) { - $summary['total_objects_created'] += $sectionStats['created']; - $summary['total_objects_updated'] += $sectionStats['updated']; - $summary['total_objects_skipped'] += $sectionStats['skipped']; - $summary['total_errors'] += count($sectionStats['errors']); + if ($section !== 'summary') { // Skip summary section itself + $summary['total_objects_created'] += $sectionStats['created']; + $summary['total_objects_updated'] += $sectionStats['updated']; + $summary['total_objects_skipped'] += $sectionStats['skipped']; + $summary['total_errors'] += count($sectionStats['errors']); + } } $statistics['summary'] = $summary; diff --git a/lib/Service/ArchiMateService_backup.php b/lib/Service/ArchiMateService_backup.php index 2d00a764..528a8e40 100644 --- a/lib/Service/ArchiMateService_backup.php +++ b/lib/Service/ArchiMateService_backup.php @@ -1824,14 +1824,23 @@ private function processSchemaTypeSynchronous(array $items, string $schemaType, 'schema_id' => $schemaId ]); - $savedObjects = $objectService->saveObjects( + $saveResult = $objectService->saveObjects( objects: $allProcessedObjects, register: $registerId, schema: $schemaId ); - // Analyze the saved objects to determine created vs updated counts - $actionCounts = $this->analyzeBatchObjectActions($savedObjects); + // Extract saved objects from the new structured return format + $savedObjects = array_merge( + $saveResult['saved'] ?? [], + $saveResult['updated'] ?? [] + ); + + // Use actual counts from the structured return instead of analyzing objects + $actionCounts = [ + 'created' => count($saveResult['saved'] ?? []), + 'updated' => count($saveResult['updated'] ?? []) + ]; $totalSaved = count($savedObjects); // Store the action counts for return value diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php index c2eb8bf3..c70a21cd 100644 --- a/lib/Service/SettingsService.php +++ b/lib/Service/SettingsService.php @@ -237,6 +237,9 @@ public function getSettings(): array $data['configuration'][$key] = $this->config->getValueString($this->_appName, $key, $defaultValue); } + // Add catalog location + $data['catalogLocation'] = $this->getCatalogLocation(); + return $data; } catch (\Exception $e) { throw new \RuntimeException('Failed to retrieve settings: ' . $e->getMessage()); @@ -4393,4 +4396,25 @@ public function updateUserGroupsConfig(array $config): array } } + /** + * Get catalog location + * + * @return string The catalog location URL + */ + public function getCatalogLocation(): string + { + return $this->config->getValueString($this->_appName, 'catalog_location', ''); + } + + /** + * Set catalog location + * + * @param string $location The catalog location URL + * @return void + */ + public function setCatalogLocation(string $location): void + { + $this->config->setValueString($this->_appName, 'catalog_location', $location); + } + } \ No newline at end of file diff --git a/src/components/GenericObjectTable.vue b/src/components/GenericObjectTable.vue index cc1e4b4d..1ea397f8 100644 --- a/src/components/GenericObjectTable.vue +++ b/src/components/GenericObjectTable.vue @@ -196,9 +196,9 @@ import { objectStore, navigationStore } from '../store/store.js' Refresh Status - - - - {{ selectedTimeWindow && selectedTimeWindow.value === 0 ? 'Full Sync Now' : 'Incremental Sync Now' }} -
From 00be54c56f76ab1e897abd898a4c5a58e42dff1e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 25 Aug 2025 12:08:50 +0000 Subject: [PATCH 44/83] Bump version to 0.1.54 [skip ci] --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 27235714..fed2d8e0 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Submit a [bug report](https://github.com/OpenCatalogi/.github/issues/new/choose) Submit a [feature request](https://github.com/OpenCatalogi/.github/issues/new/choose). ]]> - 0.1.53 + 0.1.54 agpl organization Conduction From a49b43235d3e79144a62baa112a38e2958d0529f Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 25 Aug 2025 15:01:11 +0200 Subject: [PATCH 45/83] Introducing a heartbeat script --- appinfo/routes.php | 3 + lib/Controller/SettingsController.php | 40 ++++- src/utils/heartbeat.js | 160 ++++++++++++++++++ .../sections/ArchiMateImportExport.vue | 38 +++-- .../sections/OrganizationSynchronization.vue | 30 ++-- 5 files changed, 244 insertions(+), 27 deletions(-) create mode 100644 src/utils/heartbeat.js diff --git a/appinfo/routes.php b/appinfo/routes.php index 5018c65d..f4dbfbd0 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -36,6 +36,9 @@ // Organization synchronization routes ['name' => 'settings#getSyncStatus', 'url' => '/api/settings/sync-status', 'verb' => 'GET'], ['name' => 'settings#performSync', 'url' => '/api/settings/sync', 'verb' => 'POST'], + + // Heartbeat route for keeping connections alive during long operations + ['name' => 'settings#heartbeat', 'url' => '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/api/heartbeat', 'verb' => 'POST'], // Version and import management routes ['name' => 'settings#getVersionInfo', 'url' => '/api/settings/version', 'verb' => 'GET'], diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index a932a330..f732e65f 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -598,7 +598,45 @@ public function performSync(int $minutesBack = 0): JSONResponse } } - + /** + * Heartbeat endpoint to keep connections alive during long-running operations + * + * This endpoint prevents 504 gateway timeouts by responding to periodic + * keep-alive requests sent during lengthy operations like sync or export. + * + * @return JSONResponse JSON response confirming heartbeat received + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function heartbeat(): JSONResponse + { + try { + $timestamp = $this->request->getParam('timestamp', time() * 1000); + + $this->logger->debug('Heartbeat received', [ + 'timestamp' => $timestamp, + 'server_time' => time() * 1000 + ]); + + return new JSONResponse([ + 'success' => true, + 'message' => 'Heartbeat received', + 'timestamp' => $timestamp, + 'server_time' => time() * 1000 + ]); + } catch (\Exception $e) { + $this->logger->error('Heartbeat error: ' . $e->getMessage(), [ + 'exception' => $e + ]); + + return new JSONResponse([ + 'success' => false, + 'message' => 'Heartbeat failed', + 'error' => $e->getMessage() + ], 500); + } + } /** * Get version information for the app and configuration. diff --git a/src/utils/heartbeat.js b/src/utils/heartbeat.js new file mode 100644 index 00000000..45c85638 --- /dev/null +++ b/src/utils/heartbeat.js @@ -0,0 +1,160 @@ +/** + * Heartbeat utility for keeping long-running connections alive + * + * This utility prevents 504 gateway timeouts by sending periodic + * keep-alive requests during long operations. + * + * @author Conduction B.V. + * @license AGPL-3.0-or-later + * @version 1.0.0 + */ + +/** + * Heartbeat class for managing keep-alive requests + */ +class Heartbeat { + /** + * Create a new heartbeat instance + * + * @param {number} interval - Heartbeat interval in milliseconds (default: 30000 = 30s) + */ + constructor(interval = 30000) { + this.interval = interval + this.timer = null + this.isRunning = false + this.endpoint = '/index.php/apps/softwarecatalog/api/heartbeat' + } + + /** + * Start sending heartbeat requests + * + * @return {void} + */ + start() { + if (this.isRunning) { + return + } + + this.isRunning = true + console.debug('Starting heartbeat with interval:', this.interval + 'ms') + + // Send first heartbeat immediately + this.sendHeartbeat() + + // Set up periodic heartbeats + this.timer = setInterval(() => { + this.sendHeartbeat() + }, this.interval) + } + + /** + * Stop sending heartbeat requests + * + * @return {void} + */ + stop() { + if (!this.isRunning) { + return + } + + this.isRunning = false + + if (this.timer) { + clearInterval(this.timer) + this.timer = null + } + + console.debug('Heartbeat stopped') + } + + /** + * Send a single heartbeat request + * + * @private + * @return {void} + */ + async sendHeartbeat() { + try { + const response = await fetch(this.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + requesttoken: OC.requestToken, + }, + body: JSON.stringify({ + timestamp: Date.now(), + }), + }) + + if (!response.ok) { + console.warn('Heartbeat request failed:', response.status, response.statusText) + } else { + console.debug('Heartbeat sent successfully') + } + } catch (error) { + console.warn('Heartbeat request error:', error.message) + } + } + + /** + * Check if heartbeat is currently running + * + * @return {boolean} True if heartbeat is running + */ + get running() { + return this.isRunning + } +} + +// Export singleton instance for global use +const heartbeat = new Heartbeat() + +/** + * Start heartbeat for long operations + * + * @param {number} interval - Optional custom interval in milliseconds + * @return {void} + */ +export function startHeartbeat(interval) { + if (interval) { + heartbeat.interval = interval + } + heartbeat.start() +} + +/** + * Stop heartbeat + * + * @return {void} + */ +export function stopHeartbeat() { + heartbeat.stop() +} + +/** + * Check if heartbeat is running + * + * @return {boolean} True if running + */ +export function isHeartbeatRunning() { + return heartbeat.running +} + +/** + * Convenience function to wrap a long-running operation with heartbeat + * + * @param {Function} operation - Async function to execute + * @param {number} interval - Optional heartbeat interval in milliseconds + * @return {Promise} Promise that resolves with the operation result + */ +export async function withHeartbeat(operation, interval = 30000) { + try { + startHeartbeat(interval) + return await operation() + } finally { + stopHeartbeat() + } +} + +export default heartbeat diff --git a/src/views/settings/sections/ArchiMateImportExport.vue b/src/views/settings/sections/ArchiMateImportExport.vue index 25ff2b3d..f4cbb594 100644 --- a/src/views/settings/sections/ArchiMateImportExport.vue +++ b/src/views/settings/sections/ArchiMateImportExport.vue @@ -269,6 +269,7 @@ */ import { settingsStore } from '../../../store/store.js' +import { withHeartbeat } from '../../../utils/heartbeat.js' // Components import AlwaysVisibleSection from '../../../components/AlwaysVisibleSection.vue' @@ -406,10 +407,12 @@ export default { this.importError = null try { - // Create FormData for file upload - const formData = new FormData() - formData.append('archiMateFile', this.selectedFile) + // Create FormData for file upload + const formData = new FormData() + formData.append('archiMateFile', this.selectedFile) + // Wrap the import operation with heartbeat to prevent 504 timeouts + const result = await withHeartbeat(async () => { // Make the API call const response = await fetch('/index.php/apps/softwarecatalog/api/archimate/import', { method: 'POST', @@ -422,17 +425,21 @@ export default { const result = await response.json() - if (result.success) { - this.importResult = result - // Show success notification - OC.Notification.showTemporary( - `Successfully imported ${result.performance_metrics.objects_processed} objects in ${this.formatTime(result.performance_metrics.total_time_seconds)}`, - { type: 'success' }, - ) - } else { + if (!result.success) { throw new Error(result.message || 'Import failed') } + return result + }, 30000) // 30-second heartbeat interval + + // Handle successful result + this.importResult = result + // Show success notification + OC.Notification.showTemporary( + `Successfully imported ${result.performance_metrics.objects_processed} objects in ${this.formatTime(result.performance_metrics.total_time_seconds)}`, + { type: 'success' }, + ) + } catch (error) { console.error('Error importing ArchiMate file:', error) this.importError = { @@ -465,8 +472,10 @@ export default { organization: this.selectedOrganization?.value ?? null, } - // Make the API call - const response = await fetch('/index.php/apps/softwarecatalog/api/archimate/export', { + // Wrap the export operation with heartbeat to prevent 504 timeouts + await withHeartbeat(async () => { + // Make the API call + const response = await fetch('/index.php/apps/softwarecatalog/api/archimate/export', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -512,8 +521,9 @@ export default { const errorData = await response.json() throw new Error(errorData.message || 'Export failed') } + }, 30000) // 30-second heartbeat interval - } catch (error) { + } catch (error) { console.error('Error exporting ArchiMate file:', error) // Show error notification diff --git a/src/views/settings/sections/OrganizationSynchronization.vue b/src/views/settings/sections/OrganizationSynchronization.vue index 409a3efd..597830ca 100644 --- a/src/views/settings/sections/OrganizationSynchronization.vue +++ b/src/views/settings/sections/OrganizationSynchronization.vue @@ -256,6 +256,7 @@ import { settingsStore } from '../../../store/store.js' import { showError, showSuccess } from '@nextcloud/dialogs' +import { withHeartbeat } from '../../../utils/heartbeat.js' // Components import AlwaysVisibleSection from '../../../components/AlwaysVisibleSection.vue' @@ -440,20 +441,25 @@ export default { try { const timeWindow = this.selectedTimeWindow?.value || 10 - const response = await fetch('/index.php/apps/softwarecatalog/api/settings/sync', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest', - }, - body: JSON.stringify({ timeWindow }), - }) + + // Wrap the sync operation with heartbeat to prevent 504 timeouts + const result = await withHeartbeat(async () => { + const response = await fetch('/index.php/apps/softwarecatalog/api/settings/sync', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + body: JSON.stringify({ timeWindow }), + }) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } + return await response.json() + }, 30000) // 30-second heartbeat interval - const result = await response.json() this.syncResult = result if (result.success) { From fe49d6712d88f99263633b58697389661d04c806 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 25 Aug 2025 13:01:59 +0000 Subject: [PATCH 46/83] Bump version to 0.1.55 [skip ci] --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index fed2d8e0..40aec8bb 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Submit a [bug report](https://github.com/OpenCatalogi/.github/issues/new/choose) Submit a [feature request](https://github.com/OpenCatalogi/.github/issues/new/choose). ]]> - 0.1.54 + 0.1.55 agpl organization Conduction From ce1ea9e92de2112cc2f75929aa73f85930fd9a33 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 26 Aug 2025 23:42:37 +0200 Subject: [PATCH 47/83] Hotfix: cron on objects --- .../OrganizationContactSyncJob.php | 3 +- lib/Service/ArchiMateImportService.php | 351 +++++++++++++++++- lib/Service/ArchiMateService.php | 115 +++++- lib/Service/OrganizationSyncService.php | 8 +- lib/Settings/softwarecatalogus_register.json | 6 +- src/utils/heartbeat.js | 3 + .../sections/ArchiMateImportExport.vue | 204 +++++----- 7 files changed, 562 insertions(+), 128 deletions(-) diff --git a/lib/BackgroundJob/OrganizationContactSyncJob.php b/lib/BackgroundJob/OrganizationContactSyncJob.php index 6e28abda..67c7ebe2 100644 --- a/lib/BackgroundJob/OrganizationContactSyncJob.php +++ b/lib/BackgroundJob/OrganizationContactSyncJob.php @@ -26,7 +26,8 @@ * Background job for comprehensive organization and contact person synchronization * * This job runs every 5 minutes to ensure data consistency between SoftwareCatalog objects - * and OpenRegister entities. All business logic is delegated to the OrganizationSyncService. + * and OpenRegister entities using full sync (all organizations). All business logic is + * delegated to the OrganizationSyncService. * * @category BackgroundJob * @package OCA\SoftwareCatalog\BackgroundJob diff --git a/lib/Service/ArchiMateImportService.php b/lib/Service/ArchiMateImportService.php index 705c05b5..af5047d2 100644 --- a/lib/Service/ArchiMateImportService.php +++ b/lib/Service/ArchiMateImportService.php @@ -103,6 +103,13 @@ class ArchiMateImportService */ private array $identifierPatternCache = []; + /** + * Flag to track if we've already logged finding a GEMMA type property + * + * @var bool + */ + private bool $gemmaTypePropertyFound = false; + /** * Cache for property definition maps to avoid rebuilding during import * @@ -1056,6 +1063,28 @@ private function saveObjectsToDatabase(array $objects): array // OPTIMIZATION: Use cached register ID $registerId = $this->cachedConfig['registerId'] ?? 15; + // VAR_DUMP DEBUG: Check objects before save + if (count($objects) > 0) { + $sampleObject = $objects[0]; + echo "\n=== VAR_DUMP DEBUG: Sample object BEFORE ObjectService save ===\n"; + echo "Sample object ID: " . ($sampleObject['identifier'] ?? 'unknown') . "\n"; + echo "Sample object keys: " . implode(', ', array_keys($sampleObject)) . "\n"; + echo "Has xml property: " . (isset($sampleObject['xml']) ? 'YES' : 'NO') . "\n"; + if (isset($sampleObject['xml'])) { + echo "XML keys: " . implode(', ', array_keys($sampleObject['xml'])) . "\n"; + } + echo "Has property mapping: " . (isset($sampleObject['_propertyMapping']) ? 'YES (' . count($sampleObject['_propertyMapping']) . ')' : 'NO') . "\n"; + if (isset($sampleObject['_propertyMapping'])) { + echo "Property mapping: " . implode(', ', array_keys($sampleObject['_propertyMapping'])) . "\n"; + } + $nonStandardKeys = array_diff(array_keys($sampleObject), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary']); + if (!empty($nonStandardKeys)) { + echo "Flattened properties: " . implode(', ', array_slice($nonStandardKeys, 0, 10)) . "\n"; + } else { + echo "NO flattened properties found!\n"; + } + } + // PERFORMANCE OPTIMIZATION: Use parallel batch processing for large datasets $batchProcessingStartTime = microtime(true); if (self::PERFORMANCE_OPTIMIZATIONS['parallel_processing'] && count($objects) > self::PERFORMANCE_OPTIMIZATIONS['batch_size']) { @@ -1068,6 +1097,23 @@ private function saveObjectsToDatabase(array $objects): array $totalSaveTime = microtime(true) - $saveStartTime; + // VAR_DUMP DEBUG: Check what ObjectService returned + if (count($result) > 0) { + $savedSampleObject = $result[0]; + echo "\n=== VAR_DUMP DEBUG: Sample object AFTER ObjectService save ===\n"; + echo "Saved object ID: " . ($savedSampleObject['identifier'] ?? $savedSampleObject['id'] ?? 'unknown') . "\n"; + echo "Saved object keys: " . implode(', ', array_keys($savedSampleObject)) . "\n"; + echo "Has xml property: " . (isset($savedSampleObject['xml']) ? 'YES' : 'NO') . "\n"; + echo "Has property mapping: " . (isset($savedSampleObject['_propertyMapping']) ? 'YES' : 'NO') . "\n"; + + $nonStandardKeys = array_diff(array_keys($savedSampleObject), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary', 'id']); + if (!empty($nonStandardKeys)) { + echo "Flattened properties STILL EXIST: " . implode(', ', array_slice($nonStandardKeys, 0, 10)) . "\n"; + } else { + echo "Flattened properties LOST during save!\n"; + } + } + $this->logger->info('Database save operation completed', [ 'total_save_time' => round($totalSaveTime, 3), 'service_init_time' => round($serviceInitTime, 3), @@ -1182,6 +1228,41 @@ private function saveObjectsInSingleBatch(array $objects, ObjectService $objectS 'count' => count($objects) ]); + // VAR_DUMP DEBUG: Check objects right before ObjectService call + if (count($objects) > 0) { + echo "\n=== VAR_DUMP DEBUG: Objects RIGHT BEFORE ObjectService::saveObjects ===\n"; + echo "Total objects: " . count($objects) . "\n"; + + // Look for objects with flattened properties vs metadata objects + $objectsWithFlattenedProps = []; + $objectsWithoutFlattenedProps = []; + + foreach (array_slice($objects, 0, 10) as $index => $object) { + $flattenedProps = array_diff(array_keys($object), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary']); + $hasGemmaProps = isset($object['gemmaThema']) || isset($object['objectId']) || isset($object['architectuurlaag']); + + if ($hasGemmaProps || isset($object['xml']) || isset($object['_propertyMapping'])) { + $objectsWithFlattenedProps[] = $index; + } else { + $objectsWithoutFlattenedProps[] = $index; + } + } + + echo "Objects WITH flattened/xml properties (first 10 checked): " . implode(', ', $objectsWithFlattenedProps) . "\n"; + echo "Objects WITHOUT flattened/xml properties (first 10 checked): " . implode(', ', $objectsWithoutFlattenedProps) . "\n"; + + // Show first object with flattened properties if exists + foreach ($objects as $object) { + if (isset($object['gemmaThema']) || isset($object['objectId']) || isset($object['xml'])) { + echo "\n=== FOUND OBJECT WITH FLATTENED PROPS ===\n"; + echo "ID: " . ($object['identifier'] ?? 'unknown') . "\n"; + echo "Keys: " . implode(', ', array_keys($object)) . "\n"; + echo "Full object JSON: " . json_encode($object, JSON_PRETTY_PRINT) . "\n"; + break; + } + } + } + $saveResult = $objectService->saveObjects( objects: $objects, register: $registerId, @@ -1966,6 +2047,69 @@ private function extractEssentialXmlData(array $item): array return $essential; } + /** + * Extract GEMMA type from an object using multiple possible property names + * + * This method tries different variations of GEMMA type property names to ensure + * compatibility with different ArchiMate model variations. + * + * @param array $object The object to extract GEMMA type from + * @return string|null The GEMMA type value or null if not found + */ + private function extractGemmaType(array $object): ?string + { + // Try various possible property names for GEMMA type + $possiblePropertyNames = [ + 'gemmaType', // Standard camelCase conversion of "GEMMA Type" + 'gemmatype', // Lowercase version + 'GemmaType', // PascalCase version + 'GEMMA_Type', // Underscore version + 'gemma_type', // Lowercase underscore version + 'GEMMAType', // All caps first word + 'type', // Sometimes just "Type" in models + 'elementType', // Alternative naming + 'componentType' // Another alternative + ]; + + foreach ($possiblePropertyNames as $propertyName) { + if (isset($object[$propertyName]) && !empty($object[$propertyName])) { + $value = (string) $object[$propertyName]; + + // Log the first successful match for debugging + if (!isset($this->gemmaTypePropertyFound)) { + $this->logger->debug('GEMMA Type property found', [ + 'property_name' => $propertyName, + 'value' => $value, + 'object_id' => $object['identifier'] ?? 'unknown' + ]); + $this->gemmaTypePropertyFound = true; + } + + return $value; + } + } + + // If no direct property found, check _propertyMapping for original property names + if (isset($object['_propertyMapping'])) { + foreach ($object['_propertyMapping'] as $camelCase => $original) { + // Check if the original property name contains "gemma" or "type" + if (stripos($original, 'gemma') !== false && stripos($original, 'type') !== false) { + if (isset($object[$camelCase]) && !empty($object[$camelCase])) { + $this->logger->debug('GEMMA Type found via property mapping', [ + 'camel_case_name' => $camelCase, + 'original_name' => $original, + 'value' => $object[$camelCase], + 'object_id' => $object['identifier'] ?? 'unknown' + ]); + return (string) $object[$camelCase]; + } + } + } + } + + return null; + } + /** * Process GEMMA Referentiecomponent-Standaard relationships with Verbindingsrol support * @@ -1987,14 +2131,33 @@ private function processGemmaReferenceComponentStandards(array $objects): array $standaarden = []; $gemmaRelationshipMap = []; + // Debug: Count objects and property variations + $elementCount = 0; + $elementsWithGemmaType = 0; + $gemmaTypeVariations = []; + // PASS 1: Collect Referentiecomponenten and Standaarden, process relationships immediately foreach ($objects as $index => $object) { - // Check if this is an element with GEMMA type property - if (isset($object['section']) && $object['section'] === 'element' && isset($object['gemmaType'])) { - if ($object['gemmaType'] === 'Referentiecomponent') { - $referentieComponenten[$object['identifier']] = $index; - } elseif ($object['gemmaType'] === 'Standaard') { - $standaarden[$object['identifier']] = $index; + // Debug: Count elements and GEMMA types + if (isset($object['section']) && $object['section'] === 'element') { + $elementCount++; + + // Check for various possible GEMMA type property names + $gemmaTypeValue = $this->extractGemmaType($object); + if ($gemmaTypeValue !== null) { + $elementsWithGemmaType++; + + // Track GEMMA type variations for debugging + if (!isset($gemmaTypeVariations[$gemmaTypeValue])) { + $gemmaTypeVariations[$gemmaTypeValue] = 0; + } + $gemmaTypeVariations[$gemmaTypeValue]++; + + if ($gemmaTypeValue === 'Referentiecomponent') { + $referentieComponenten[$object['identifier']] = $index; + } elseif ($gemmaTypeValue === 'Standaard') { + $standaarden[$object['identifier']] = $index; + } } } @@ -2004,7 +2167,11 @@ private function processGemmaReferenceComponentStandards(array $objects): array } } - $this->logger->info('GEMMA objects found', [ + // Enhanced debug logging + $this->logger->info('GEMMA objects processing complete', [ + 'total_elements' => $elementCount, + 'elements_with_gemma_type' => $elementsWithGemmaType, + 'gemma_type_variations' => $gemmaTypeVariations, 'referentiecomponenten_count' => count($referentieComponenten), 'standaarden_count' => count($standaarden), 'processed_relationships' => count($gemmaRelationshipMap) @@ -2172,6 +2339,14 @@ private function transformArchiMateXmlToObjectsBatch(array $xmlData, string $mod // Extract propertyDefinitionMap once for all objects $propertyDefinitionMap = $this->extractPropertyDefinitionMap($xmlData); + // Debug: Log property definition map extraction + $this->logger->info('Property definition map extracted', [ + 'total_definitions' => count($propertyDefinitionMap), + 'sample_definitions' => array_slice($propertyDefinitionMap, 0, 10, true), + 'has_gemma_type' => isset($propertyDefinitionMap['propid-3']) || in_array('GEMMA type', $propertyDefinitionMap) || in_array('GEMMA Type', $propertyDefinitionMap), + 'gemma_type_ref' => $propertyDefinitionMap['propid-3'] ?? 'not found' + ]); + // Create model object first if (isset($xmlData['_attributes']) || isset($xmlData['name'])) { $modelMetadata = [ @@ -2302,6 +2477,8 @@ private function transformSectionObjectsBatch( } // Create object directly (minimal processing) + $essentialXmlData = $this->extractEssentialXmlData($item); + $object = [ '@self' => [ 'register' => $this->cachedConfig['registerId'] ?? 15, @@ -2315,9 +2492,20 @@ private function transformSectionObjectsBatch( 'identifier' => $identifier, 'section' => $schemaType, 'model_identifier' => $modelIdentifier, - 'xml' => $this->extractEssentialXmlData($item) // OPTIMIZATION: Store only essential XML data + 'xml' => $essentialXmlData ]; + // Debug: Log XML data extraction + $this->logger->debug('XML data extracted for object', [ + 'object_id' => $identifier, + 'section' => $schemaType, + 'original_item_keys' => array_keys($item), + 'essential_xml_keys' => array_keys($essentialXmlData), + 'essential_xml_size' => strlen(json_encode($essentialXmlData)), + 'has_properties' => isset($item['properties']), + 'properties_structure' => isset($item['properties']) ? array_keys($item['properties']) : null + ]); + // Extract name from XML if it exists if (isset($item['name'])) { if (is_array($item['name']) && isset($item['name']['_value'])) { @@ -2336,14 +2524,107 @@ private function transformSectionObjectsBatch( } } + // VAR_DUMP DEBUG: Check property structure - limit to first element only + static $debugCount = 0; + if ($debugCount === 0 && isset($item['properties'])) { + echo "\n=== VAR_DUMP DEBUG: Item with properties structure ===\n"; + echo "Object ID: " . $identifier . "\n"; + echo "Item keys: " . implode(', ', array_keys($item)) . "\n"; + if (isset($item['properties'])) { + echo "Properties keys: " . implode(', ', array_keys($item['properties'])) . "\n"; + if (isset($item['properties']['property'])) { + echo "Properties.property structure:\n"; + $props = is_array($item['properties']['property']) && isset($item['properties']['property'][0]) ? + $item['properties']['property'] : [$item['properties']['property']]; + foreach (array_slice($props, 0, 3) as $i => $prop) { + echo " Property $i keys: " . implode(', ', array_keys($prop ?? [])) . "\n"; + if (isset($prop['_attributes']['propertyDefinitionRef'])) { + echo " DefRef: " . $prop['_attributes']['propertyDefinitionRef'] . "\n"; + } + if (isset($prop['value'])) { + $value = is_array($prop['value']) && isset($prop['value']['_value']) ? $prop['value']['_value'] : $prop['value']; + echo " Value: " . (is_string($value) ? substr($value, 0, 50) : gettype($value)) . "\n"; + } + } + } + } + echo "Property definition map size: " . count($propertyDefinitionMap) . "\n"; + echo "Sample prop defs: " . implode(', ', array_slice($propertyDefinitionMap, 0, 5, true)) . "\n"; + $debugCount++; + } + // Flatten properties efficiently (if present) if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) { $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap); + + // VAR_DUMP DEBUG: Show object after flattening - limit to first element only + if ($debugCount === 1) { + echo "\n=== VAR_DUMP DEBUG: Object after property flattening ===\n"; + echo "Object keys: " . implode(', ', array_keys($object)) . "\n"; + if (isset($object['_propertyMapping'])) { + echo "Property mapping: " . implode(', ', array_keys($object['_propertyMapping'])) . "\n"; + } + $nonStandardKeys = array_diff(array_keys($object), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary']); + if (!empty($nonStandardKeys)) { + echo "Flattened properties: " . implode(', ', $nonStandardKeys) . "\n"; + } + } + } else { + // Only show this for first few objects to avoid spam + if ($debugCount < 3) { + echo "\n=== VAR_DUMP DEBUG: SKIPPING property flattening for " . $identifier . " ===\n"; + echo "Has properties.property: " . (isset($item['properties']['property']) ? 'YES' : 'NO') . "\n"; + echo "Property definition map size: " . count($propertyDefinitionMap) . "\n"; + } } + // DEBUG: Log final object structure before adding to array + $this->logger->debug('Final object structure before save', [ + 'object_id' => $identifier, + 'section' => $schemaType, + 'object_keys' => array_keys($object), + 'has_xml_property' => isset($object['xml']), + 'xml_keys' => isset($object['xml']) ? array_keys($object['xml']) : null, + 'has_property_mapping' => isset($object['_propertyMapping']), + 'property_mapping_count' => isset($object['_propertyMapping']) ? count($object['_propertyMapping']) : 0, + 'sample_properties' => array_slice(array_diff(array_keys($object), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary']), 0, 5) + ]); + $objects[] = $object; } + // VAR_DUMP DEBUG: Check objects right after transformation + echo "\n=== VAR_DUMP DEBUG: Objects RIGHT AFTER transformation (before return) ===\n"; + echo "Total objects created: " . count($objects) . "\n"; + + // Count objects with different characteristics + $withXml = 0; + $withPropertyMapping = 0; + $withGemmaProps = 0; + $metadataObjects = 0; + + foreach ($objects as $object) { + if (isset($object['xml'])) $withXml++; + if (isset($object['_propertyMapping'])) $withPropertyMapping++; + if (isset($object['gemmaThema']) || isset($object['objectId']) || isset($object['architectuurlaag'])) $withGemmaProps++; + if (isset($object['documentation']) || isset($object['propertyDefinitionMap'])) $metadataObjects++; + } + + echo "Objects with xml: $withXml\n"; + echo "Objects with _propertyMapping: $withPropertyMapping\n"; + echo "Objects with GEMMA properties: $withGemmaProps\n"; + echo "Metadata objects: $metadataObjects\n"; + + // Show a sample object with GEMMA properties if any exist + foreach ($objects as $object) { + if (isset($object['gemmaThema']) || isset($object['objectId'])) { + echo "\n=== SAMPLE OBJECT WITH GEMMA PROPS AFTER TRANSFORMATION ===\n"; + echo "ID: " . ($object['identifier'] ?? 'unknown') . "\n"; + echo "Keys: " . implode(', ', array_keys($object)) . "\n"; + break; + } + } + return $objects; } @@ -2394,14 +2675,37 @@ private function flattenPropertiesBatch(array &$object, array $properties, array $props = isset($properties[0]) ? $properties : [$properties]; $processedProperties = []; - foreach ($props as $prop) { + // Debug: Log property flattening process + $this->logger->debug('Flattening properties for object', [ + 'object_id' => $object['identifier'] ?? 'unknown', + 'properties_count' => count($props), + 'property_definition_map_size' => count($propertyDefinitionMap), + 'sample_property_definitions' => array_slice($propertyDefinitionMap, 0, 5, true) + ]); + + foreach ($props as $propIndex => $prop) { if (!isset($prop['_attributes']['propertyDefinitionRef'])) { + $this->logger->warning('Property missing propertyDefinitionRef', [ + 'object_id' => $object['identifier'] ?? 'unknown', + 'property_index' => $propIndex, + 'property_structure' => array_keys($prop ?? []) + ]); continue; } $defRef = $prop['_attributes']['propertyDefinitionRef']; $value = $prop['value']['_value'] ?? $prop['value'] ?? null; + // Debug: Log property reference lookup + if (!isset($propertyDefinitionMap[$defRef])) { + $this->logger->warning('Property definition not found in map', [ + 'object_id' => $object['identifier'] ?? 'unknown', + 'property_def_ref' => $defRef, + 'available_refs' => array_keys($propertyDefinitionMap) + ]); + continue; + } + if ($value !== null && isset($propertyDefinitionMap[$defRef])) { $propertyName = $propertyDefinitionMap[$defRef]; $camelCaseName = $this->convertToCamelCase($propertyName); @@ -2416,17 +2720,42 @@ private function flattenPropertiesBatch(array &$object, array $properties, array $processedProperties[] = [ 'original' => $propertyName, 'camelCase' => $camelCaseName, - 'value' => $value + 'value' => $value, + 'def_ref' => $defRef ]; // Set slug for Object ID property if (strtolower($propertyName) === 'object id') { $object['@self']['slug'] = $value; } + + // Debug: Log GEMMA type properties specifically + if (stripos($propertyName, 'gemma') !== false || $defRef === 'propid-3') { + $this->logger->info('GEMMA type property processed', [ + 'object_id' => $object['identifier'] ?? 'unknown', + 'property_name' => $propertyName, + 'camel_case_name' => $camelCaseName, + 'value' => $value, + 'def_ref' => $defRef + ]); + } + } else { + $this->logger->warning('Property value is null or mapping missing', [ + 'object_id' => $object['identifier'] ?? 'unknown', + 'property_def_ref' => $defRef, + 'value' => $value, + 'mapping_exists' => isset($propertyDefinitionMap[$defRef]) + ]); } } - // OPTIMIZATION: Removed debug logging from tight loop for performance + // Debug: Log final property flattening results + $this->logger->debug('Property flattening completed', [ + 'object_id' => $object['identifier'] ?? 'unknown', + 'processed_count' => count($processedProperties), + 'processed_properties' => $processedProperties, + 'object_keys_after_flattening' => array_keys($object) + ]); } /** diff --git a/lib/Service/ArchiMateService.php b/lib/Service/ArchiMateService.php index 16e281a0..461fc9b6 100644 --- a/lib/Service/ArchiMateService.php +++ b/lib/Service/ArchiMateService.php @@ -76,6 +76,13 @@ class ArchiMateService * @var array|null */ private ?array $propertyDefinitionMapCache = null; + + /** + * Flag to track if we've already logged finding a GEMMA type property + * + * @var bool + */ + private bool $gemmaTypePropertyFound = false; private const CONFIG_KEYS = [ 'archimate_register_id' => 'archimate_register_id', 'archimate_schema_id' => 'archimate_schema_id', @@ -2089,6 +2096,69 @@ private function calculateOptimizedStatistics(array $savedObjects): array return $statistics; } + /** + * Extract GEMMA type from an object using multiple possible property names + * + * This method tries different variations of GEMMA type property names to ensure + * compatibility with different ArchiMate model variations. + * + * @param array $object The object to extract GEMMA type from + * @return string|null The GEMMA type value or null if not found + */ + private function extractGemmaType(array $object): ?string + { + // Try various possible property names for GEMMA type + $possiblePropertyNames = [ + 'gemmaType', // Standard camelCase conversion of "GEMMA Type" + 'gemmatype', // Lowercase version + 'GemmaType', // PascalCase version + 'GEMMA_Type', // Underscore version + 'gemma_type', // Lowercase underscore version + 'GEMMAType', // All caps first word + 'type', // Sometimes just "Type" in models + 'elementType', // Alternative naming + 'componentType' // Another alternative + ]; + + foreach ($possiblePropertyNames as $propertyName) { + if (isset($object[$propertyName]) && !empty($object[$propertyName])) { + $value = (string) $object[$propertyName]; + + // Log the first successful match for debugging + if (!$this->gemmaTypePropertyFound) { + $this->logger->debug('GEMMA Type property found', [ + 'property_name' => $propertyName, + 'value' => $value, + 'object_id' => $object['identifier'] ?? 'unknown' + ]); + $this->gemmaTypePropertyFound = true; + } + + return $value; + } + } + + // If no direct property found, check _propertyMapping for original property names + if (isset($object['_propertyMapping'])) { + foreach ($object['_propertyMapping'] as $camelCase => $original) { + // Check if the original property name contains "gemma" or "type" + if (stripos($original, 'gemma') !== false && stripos($original, 'type') !== false) { + if (isset($object[$camelCase]) && !empty($object[$camelCase])) { + $this->logger->debug('GEMMA Type found via property mapping', [ + 'camel_case_name' => $camelCase, + 'original_name' => $original, + 'value' => $object[$camelCase], + 'object_id' => $object['identifier'] ?? 'unknown' + ]); + return (string) $object[$camelCase]; + } + } + } + } + + return null; + } + /** * Process GEMMA Referentiecomponent-Standaard relationships with Verbindingsrol support * @@ -2110,14 +2180,33 @@ private function processGemmaReferenceComponentStandards(array $objects): array $standaarden = []; $gemmaRelationshipMap = []; + // Debug: Count objects and property variations + $elementCount = 0; + $elementsWithGemmaType = 0; + $gemmaTypeVariations = []; + // PASS 1: Collect Referentiecomponenten and Standaarden, process relationships immediately foreach ($objects as $index => $object) { - // Check if this is an element with GEMMA type property - if (isset($object['section']) && $object['section'] === 'element' && isset($object['gemmaType'])) { - if ($object['gemmaType'] === 'Referentiecomponent') { - $referentieComponenten[$object['identifier']] = $index; - } elseif ($object['gemmaType'] === 'Standaard') { - $standaarden[$object['identifier']] = $index; + // Debug: Count elements and GEMMA types + if (isset($object['section']) && $object['section'] === 'element') { + $elementCount++; + + // Check for various possible GEMMA type property names + $gemmaTypeValue = $this->extractGemmaType($object); + if ($gemmaTypeValue !== null) { + $elementsWithGemmaType++; + + // Track GEMMA type variations for debugging + if (!isset($gemmaTypeVariations[$gemmaTypeValue])) { + $gemmaTypeVariations[$gemmaTypeValue] = 0; + } + $gemmaTypeVariations[$gemmaTypeValue]++; + + if ($gemmaTypeValue === 'Referentiecomponent') { + $referentieComponenten[$object['identifier']] = $index; + } elseif ($gemmaTypeValue === 'Standaard') { + $standaarden[$object['identifier']] = $index; + } } } @@ -2127,12 +2216,24 @@ private function processGemmaReferenceComponentStandards(array $objects): array } } - $this->logger->info('GEMMA objects found', [ + // Enhanced debug logging + $this->logger->info('GEMMA objects processing complete', [ + 'total_elements' => $elementCount, + 'elements_with_gemma_type' => $elementsWithGemmaType, + 'gemma_type_variations' => $gemmaTypeVariations, 'referentiecomponenten_count' => count($referentieComponenten), 'standaarden_count' => count($standaarden), 'processed_relationships' => count($gemmaRelationshipMap) ]); + // Additional debugging if no GEMMA types found + if ($elementsWithGemmaType === 0 && $elementCount > 0) { + $this->logger->warning('No GEMMA types found in any elements', [ + 'total_elements_processed' => $elementCount, + 'sample_element_keys' => 'Will need to examine individual objects' + ]); + } + // STEP 2: Apply the processed relationship mappings to Referentiecomponenten $enhancedCount = 0; foreach ($gemmaRelationshipMap as $referentieComponentId => $standaardenMap) { diff --git a/lib/Service/OrganizationSyncService.php b/lib/Service/OrganizationSyncService.php index ef323cbf..463129dd 100644 --- a/lib/Service/OrganizationSyncService.php +++ b/lib/Service/OrganizationSyncService.php @@ -233,7 +233,7 @@ public function performContactSync() :array } $contactEntity->setObject($contactEntityObject); - $objectService->saveObject(object: $contactEntity, register: $register, schema: $contactSchema); + $objectService->saveObject(object: $contactEntity, register: $register, schema: $contactSchema, rbac: false, multi: false); $stats['contactPersonsProcessed']++; } @@ -975,13 +975,13 @@ public function recordSyncTime(): void * * This method is designed to be called by the background job and includes * all necessary logging, error handling, and status tracking. - * Uses default 10-minute lookback for incremental sync. + * Uses default full sync (0 minutes) to process all organizations. * - * @param int $minutesBack Number of minutes to look back for changes (default: 10) + * @param int $minutesBack Number of minutes to look back for changes (default: 0 = full sync) * * @return array Synchronization results with detailed logging information */ - public function performScheduledSync(int $minutesBack = 10): array + public function performScheduledSync(int $minutesBack = 0): array { $this->logger->info('OrganizationSyncService: Starting scheduled synchronization', [ 'minutesBack' => $minutesBack, diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index 916077c7..9464a009 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -438,7 +438,7 @@ "SaaS" ], "facetable": true, - "title": "Cloud Dienstverleningsmodel", + "title": "Hosting vorm", "example": "Bijvoorbeeld: SaaS" }, "hostingJurisdictie": { @@ -450,7 +450,7 @@ "NL", "EU", "US", - "elders" + "Elders" ], "facetable": true, "title": "Hosting jurisdictie", @@ -465,7 +465,7 @@ "NL", "EU", "US", - "elders" + "Elders" ], "facetable": true, "title": "Hosting locatie", diff --git a/src/utils/heartbeat.js b/src/utils/heartbeat.js index 45c85638..e0b10b5b 100644 --- a/src/utils/heartbeat.js +++ b/src/utils/heartbeat.js @@ -158,3 +158,6 @@ export async function withHeartbeat(operation, interval = 30000) { } export default heartbeat + + + diff --git a/src/views/settings/sections/ArchiMateImportExport.vue b/src/views/settings/sections/ArchiMateImportExport.vue index f4cbb594..a5e97c89 100644 --- a/src/views/settings/sections/ArchiMateImportExport.vue +++ b/src/views/settings/sections/ArchiMateImportExport.vue @@ -407,38 +407,38 @@ export default { this.importError = null try { - // Create FormData for file upload - const formData = new FormData() - formData.append('archiMateFile', this.selectedFile) - - // Wrap the import operation with heartbeat to prevent 504 timeouts - const result = await withHeartbeat(async () => { - // Make the API call - const response = await fetch('/index.php/apps/softwarecatalog/api/archimate/import', { - method: 'POST', - headers: { - 'OCS-APIREQUEST': 'true', - requesttoken: OC.requestToken, - }, - body: formData, - }) - - const result = await response.json() - - if (!result.success) { - throw new Error(result.message || 'Import failed') - } + // Create FormData for file upload + const formData = new FormData() + formData.append('archiMateFile', this.selectedFile) + + // Wrap the import operation with heartbeat to prevent 504 timeouts + const result = await withHeartbeat(async () => { + // Make the API call + const response = await fetch('/index.php/apps/softwarecatalog/api/archimate/import', { + method: 'POST', + headers: { + 'OCS-APIREQUEST': 'true', + requesttoken: OC.requestToken, + }, + body: formData, + }) + + const result = await response.json() + + if (!result.success) { + throw new Error(result.message || 'Import failed') + } - return result - }, 30000) // 30-second heartbeat interval + return result + }, 30000) // 30-second heartbeat interval - // Handle successful result - this.importResult = result - // Show success notification - OC.Notification.showTemporary( - `Successfully imported ${result.performance_metrics.objects_processed} objects in ${this.formatTime(result.performance_metrics.total_time_seconds)}`, - { type: 'success' }, - ) + // Handle successful result + this.importResult = result + // Show success notification + OC.Notification.showTemporary( + `Successfully imported ${result.performance_metrics.objects_processed} objects in ${this.formatTime(result.performance_metrics.total_time_seconds)}`, + { type: 'success' }, + ) } catch (error) { console.error('Error importing ArchiMate file:', error) @@ -467,63 +467,63 @@ export default { this.exporting = true try { - // Prepare export data with organization filter - const exportData = { - organization: this.selectedOrganization?.value ?? null, - } + // Prepare export data with organization filter + const exportData = { + organization: this.selectedOrganization?.value ?? null, + } - // Wrap the export operation with heartbeat to prevent 504 timeouts - await withHeartbeat(async () => { - // Make the API call - const response = await fetch('/index.php/apps/softwarecatalog/api/archimate/export', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'OCS-APIREQUEST': 'true', - requesttoken: OC.requestToken, - }, - body: JSON.stringify(exportData), - }) - - // Handle file download - if (response.ok) { - const blob = await response.blob() - const url = window.URL.createObjectURL(blob) - - // Get filename from response headers or create default - const contentDisposition = response.headers.get('content-disposition') - let fileName = 'archimate_export.xml' - if (contentDisposition) { - const match = contentDisposition.match(/filename="?([^"]*)"?/) - if (match) { - fileName = match[1] + // Wrap the export operation with heartbeat to prevent 504 timeouts + await withHeartbeat(async () => { + // Make the API call + const response = await fetch('/index.php/apps/softwarecatalog/api/archimate/export', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'OCS-APIREQUEST': 'true', + requesttoken: OC.requestToken, + }, + body: JSON.stringify(exportData), + }) + + // Handle file download + if (response.ok) { + const blob = await response.blob() + const url = window.URL.createObjectURL(blob) + + // Get filename from response headers or create default + const contentDisposition = response.headers.get('content-disposition') + let fileName = 'archimate_export.xml' + if (contentDisposition) { + const match = contentDisposition.match(/filename="?([^"]*)"?/) + if (match) { + fileName = match[1] + } } - } - // Create download link and trigger download - const a = document.createElement('a') - a.href = url - a.download = fileName - document.body.appendChild(a) - a.click() - window.URL.revokeObjectURL(url) - document.body.removeChild(a) - - // Show success notification - const orgName = this.selectedOrganization - ? this.organizationOptions.find(opt => opt.value === this.selectedOrganization)?.label - : 'Generic' - OC.Notification.showTemporary( - `ArchiMate file exported successfully for ${orgName}`, - { type: 'success' }, - ) - } else { - const errorData = await response.json() - throw new Error(errorData.message || 'Export failed') - } - }, 30000) // 30-second heartbeat interval + // Create download link and trigger download + const a = document.createElement('a') + a.href = url + a.download = fileName + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + document.body.removeChild(a) + + // Show success notification + const orgName = this.selectedOrganization + ? this.organizationOptions.find(opt => opt.value === this.selectedOrganization)?.label + : 'Generic' + OC.Notification.showTemporary( + `ArchiMate file exported successfully for ${orgName}`, + { type: 'success' }, + ) + } else { + const errorData = await response.json() + throw new Error(errorData.message || 'Export failed') + } + }, 30000) // 30-second heartbeat interval - } catch (error) { + } catch (error) { console.error('Error exporting ArchiMate file:', error) // Show error notification @@ -564,25 +564,25 @@ export default { } }, - /** - * Load organization options from the API - * - * @async - * @return {Promise} - */ - async loadOrganizations() { - try { - // Get organization objects from OpenRegister - // This would need to be implemented based on your organization schema - // For now, we'll keep the default Generic option - const response = await fetch('/index.php/apps/openregister/api/objects/6/35', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'OCS-APIREQUEST': 'true', - requesttoken: OC.requestToken, - }, - }) + /** + * Load organization options from the API + * + * @async + * @return {Promise} + */ + async loadOrganizations() { + try { + // Get organization objects from OpenRegister + // This would need to be implemented based on your organization schema + // For now, we'll keep the default Generic option + const response = await fetch('/index.php/apps/openregister/api/objects/6/35', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'OCS-APIREQUEST': 'true', + requesttoken: OC.requestToken, + }, + }) if (response.ok) { const result = await response.json() From 81b2afa013129b079bfccad58817040f11fcd39e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 26 Aug 2025 21:43:26 +0000 Subject: [PATCH 48/83] Bump version to 0.1.56 [skip ci] --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 40aec8bb..d7f16308 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Submit a [bug report](https://github.com/OpenCatalogi/.github/issues/new/choose) Submit a [feature request](https://github.com/OpenCatalogi/.github/issues/new/choose). ]]> - 0.1.55 + 0.1.56 agpl organization Conduction From 3fcf29f584f5e4d1d21c4a8320e4731c3f051238 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 27 Aug 2025 08:12:50 +0200 Subject: [PATCH 49/83] Optimizing the sync --- lib/Controller/SettingsController.php | 41 ++++++- lib/Service/OrganizationSyncService.php | 149 ++++++++++++++++++++++-- 2 files changed, 175 insertions(+), 15 deletions(-) diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index f732e65f..443c181b 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -589,12 +589,41 @@ public function getSyncStatus(int $minutesBack = 10): JSONResponse */ public function performSync(int $minutesBack = 0): JSONResponse { - $result = $this->organizationSyncService->performManualSync($minutesBack); - - if ($result['success']) { - return new JSONResponse($result); - } else { - return new JSONResponse($result, 500); + try { + // For full sync (minutesBack = 0), use optimized batch processing to handle large datasets + if ($minutesBack === 0) { + $result = $this->organizationSyncService->performOptimizedManualSync( + maxRounds: 15, // Up to 15 rounds of processing + batchSize: 75 // 75 items per batch for good performance + ); + + return new JSONResponse([ + 'success' => true, + 'results' => $result, + 'message' => 'Optimized synchronization completed successfully', + 'isOptimized' => true + ]); + } else { + // For incremental sync, use the original method + $result = $this->organizationSyncService->performManualSync($minutesBack); + + if ($result['success']) { + return new JSONResponse($result); + } else { + return new JSONResponse($result, 500); + } + } + } catch (\Exception $e) { + $this->logger->error('Manual sync failed', [ + 'minutesBack' => $minutesBack, + 'exception' => $e->getMessage() + ]); + + return new JSONResponse([ + 'success' => false, + 'message' => 'Synchronization failed: ' . $e->getMessage(), + 'error' => $e->getMessage() + ], 500); } } diff --git a/lib/Service/OrganizationSyncService.php b/lib/Service/OrganizationSyncService.php index 463129dd..a9ebaaef 100644 --- a/lib/Service/OrganizationSyncService.php +++ b/lib/Service/OrganizationSyncService.php @@ -113,7 +113,7 @@ public function __construct( $this->settingsService = $settingsService; } - public function performOrganizationsSync(): array + public function performOrganizationsSync(int $batchSize = 50, int $maxExecutionSeconds = 45): array { // Check configuration $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); @@ -121,6 +121,7 @@ public function performOrganizationsSync(): array $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? ''; // $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? ''; + $startTime = time(); $stats = [ 'organizationsProcessed' => 0, 'entitiesCreated' => 0, @@ -129,6 +130,9 @@ public function performOrganizationsSync(): array 'usersCreated' => 0, 'usersUpdated' => 0, 'errors' => [], + 'batchSize' => $batchSize, + 'maxExecutionSeconds' => $maxExecutionSeconds, + 'timeoutReached' => false, 'startTime' => date('Y-m-d H:i:s'), 'endTime' => null, 'duration' => null @@ -145,7 +149,9 @@ public function performOrganizationsSync(): array $qb->expr()->neq('o2.active', $qb->createFunction('(json_unquote(json_extract(o.object, \'$.status\')) = \'actief\')')), $qb->expr()->isNull('o2.uuid') )) - ->andWhere($qb->expr()->neq($qb->createFunction('json_unquote(json_extract(o.object, \'$.status\'))'), $qb->createNamedParameter('concept'))); + ->andWhere($qb->expr()->neq($qb->createFunction('json_unquote(json_extract(o.object, \'$.status\'))'), $qb->createNamedParameter('concept'))) + ->orderBy('o.updated', 'ASC') // Process oldest first for consistency + ->setMaxResults($batchSize); // Limit batch size $sql = $qb->getSQL(); $objects = $qb->execute()->fetchAll(); @@ -153,6 +159,17 @@ public function performOrganizationsSync(): array foreach($objects as $object) { + // Check if we're approaching the time limit + if (time() - $startTime >= $maxExecutionSeconds) { + $stats['timeoutReached'] = true; + $this->logger->info('OrganizationSyncService: Execution time limit reached', [ + 'processedCount' => $stats['organizationsProcessed'], + 'executionTime' => time() - $startTime, + 'maxExecutionSeconds' => $maxExecutionSeconds + ]); + break; + } + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); if($objectService instanceOf ObjectService === false) { return []; @@ -168,7 +185,7 @@ public function performOrganizationsSync(): array return $stats; } - public function performContactSync() :array + public function performContactSync(int $batchSize = 100, int $maxExecutionSeconds = 30) :array { // Check configuration $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); @@ -176,6 +193,7 @@ public function performContactSync() :array // $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? ''; $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? ''; + $startTime = time(); $stats = [ 'organizationsProcessed' => 0, 'entitiesCreated' => 0, @@ -184,6 +202,9 @@ public function performContactSync() :array 'usersCreated' => 0, 'usersUpdated' => 0, 'errors' => [], + 'batchSize' => $batchSize, + 'maxExecutionSeconds' => $maxExecutionSeconds, + 'timeoutReached' => false, 'startTime' => date('Y-m-d H:i:s'), 'endTime' => null, 'duration' => null @@ -212,11 +233,24 @@ public function performContactSync() :array ) ->where($qb->expr()->eq('o.register', $qb->createNamedParameter($register))) ->andWhere($qb->expr()->eq('o.schema', $qb->createNamedParameter($contactSchema))) - ->andWhere($qb->expr()->isNull($qb->createFunction('json_unquote(json_extract(o.object, \'$.username\'))'))); + ->andWhere($qb->expr()->isNull($qb->createFunction('json_unquote(json_extract(o.object, \'$.username\'))'))) + ->orderBy('o.updated', 'ASC') // Process oldest first + ->setMaxResults($batchSize); // Limit batch size $contacts = $qb->execute()->fetchAll(); foreach ($contacts as $contact) { + // Check if we're approaching the time limit + if (time() - $startTime >= $maxExecutionSeconds) { + $stats['timeoutReached'] = true; + $this->logger->info('OrganizationSyncService: Contact sync time limit reached', [ + 'contactsProcessed' => $stats['contactPersonsProcessed'], + 'executionTime' => time() - $startTime, + 'maxExecutionSeconds' => $maxExecutionSeconds + ]); + break; + } + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); $contactEntity = $objectService->find(id: $contact['uuid'], register: $register, schema: $contactSchema); $contactEntityObject = $contactEntity->getObject(); @@ -970,6 +1004,96 @@ public function recordSyncTime(): void $this->config->setValueString('softwarecatalog', 'last_sync_time', date('Y-m-d H:i:s')); } + /** + * Performs optimized manual synchronization for large datasets + * + * This method processes organizations in multiple batches to handle large numbers + * (800+) efficiently while staying under timeout limits. + * + * @param int $maxRounds Maximum number of batch rounds to execute (default: 10) + * @param int $batchSize Number of items to process per batch (default: 100) + * + * @return array Comprehensive synchronization results + */ + public function performOptimizedManualSync(int $maxRounds = 10, int $batchSize = 100): array + { + $totalStartTime = time(); + $allResults = [ + 'totalRounds' => 0, + 'organizationsProcessed' => 0, + 'contactPersonsProcessed' => 0, + 'entitiesCreated' => 0, + 'entitiesUpdated' => 0, + 'usersCreated' => 0, + 'usersUpdated' => 0, + 'totalExecutionTime' => 0, + 'timeoutReached' => false, + 'roundsCompleted' => [], + 'errors' => [], + 'startTime' => date('Y-m-d H:i:s') + ]; + + $this->logger->info('OrganizationSyncService: Starting optimized manual sync', [ + 'maxRounds' => $maxRounds, + 'batchSize' => $batchSize + ]); + + for ($round = 1; $round <= $maxRounds; $round++) { + $roundStartTime = time(); + + // Process organizations batch + $orgResults = $this->performOrganizationsSync($batchSize, 45); + + // Process contacts batch + $contactResults = $this->performContactSync($batchSize, 15); + + // Accumulate results + $allResults['organizationsProcessed'] += $orgResults['organizationsProcessed']; + $allResults['contactPersonsProcessed'] += $contactResults['contactPersonsProcessed']; + $allResults['entitiesCreated'] += $orgResults['entitiesCreated']; + $allResults['entitiesUpdated'] += $orgResults['entitiesUpdated']; + $allResults['usersCreated'] += $contactResults['usersCreated']; + $allResults['usersUpdated'] += $contactResults['usersUpdated']; + + $roundTime = time() - $roundStartTime; + $allResults['roundsCompleted'][] = [ + 'round' => $round, + 'organizationsProcessed' => $orgResults['organizationsProcessed'], + 'contactPersonsProcessed' => $contactResults['contactPersonsProcessed'], + 'duration' => $roundTime, + 'orgTimeoutReached' => $orgResults['timeoutReached'] ?? false, + 'contactTimeoutReached' => $contactResults['timeoutReached'] ?? false + ]; + + // If no items were processed in this round, we're done + if ($orgResults['organizationsProcessed'] === 0 && $contactResults['contactPersonsProcessed'] === 0) { + $this->logger->info('OrganizationSyncService: No more items to process, stopping', [ + 'round' => $round, + 'totalProcessed' => $allResults['organizationsProcessed'] + $allResults['contactPersonsProcessed'] + ]); + break; + } + + $allResults['totalRounds'] = $round; + + // Small pause between rounds to prevent resource exhaustion + if ($round < $maxRounds) { + sleep(1); + } + } + + // Final user sync + $this->performUserSync(); + $this->recordSyncTime(); + + $allResults['totalExecutionTime'] = time() - $totalStartTime; + $allResults['endTime'] = date('Y-m-d H:i:s'); + + $this->logger->info('OrganizationSyncService: Optimized manual sync completed', $allResults); + + return $allResults; + } + /** * Performs scheduled synchronization with comprehensive logging * @@ -989,11 +1113,18 @@ public function performScheduledSync(int $minutesBack = 0): array ]); try { - // Perform the core synchronization with time-based filtering -// $syncResults = $this->performFullSync($minutesBack); - $syncResults = $this->performOrganizationsSync(); - - $syncResults = array_merge($this->performContactSync(), $syncResults); + // Perform optimized batch synchronization + // Use smaller batches for scheduled sync to ensure it completes within time limits + $orgBatchSize = 25; // Conservative batch size for organizations + $contactBatchSize = 50; // Larger batch size for contacts (faster processing) + $maxOrgTime = 30; // 30 seconds max for organizations + $maxContactTime = 15; // 15 seconds max for contacts + + $syncResults = $this->performOrganizationsSync($orgBatchSize, $maxOrgTime); + + $contactResults = $this->performContactSync($contactBatchSize, $maxContactTime); + $syncResults = array_merge($contactResults, $syncResults); + $this->performUserSync(); // Record the sync time $this->recordSyncTime(); From 7b74da3e2098b095be72d2b46fa025ac73d72be3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 27 Aug 2025 06:13:41 +0000 Subject: [PATCH 50/83] Bump version to 0.1.57 [skip ci] --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index d7f16308..92c5d11e 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Submit a [bug report](https://github.com/OpenCatalogi/.github/issues/new/choose) Submit a [feature request](https://github.com/OpenCatalogi/.github/issues/new/choose). ]]> - 0.1.56 + 0.1.57 agpl organization Conduction From 8307cf95c60d539072302a0f9c1aaf6a4a3a5f6c Mon Sep 17 00:00:00 2001 From: Remko Huisman <43807324+remko48@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:03:04 +0200 Subject: [PATCH 51/83] Update softwarecatalogus_register.json --- lib/Settings/softwarecatalogus_register.json | 10157 ++++++++--------- 1 file changed, 5022 insertions(+), 5135 deletions(-) diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index 9464a009..ea5a30ab 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -1,5144 +1,5031 @@ { - "components": { - "registers": { - "voorzieningen": { - "slug": "voorzieningen", - "title": "Voorzieningen", - "version": "1.0.7", - "description": "Register voor voorzieningen uit de softwarecatalogus", - "schemas": [ - "sector", - "product", - "dienst", - "kwetsbaarheid", - "contactpersoon", - "organisatie", - "gebruik", - "contract", - "koppeling", - "beoordeeling", - "module", - "compliancy", - "moduleVersie" - ], - "source": "internal", - "tablePrefix": "", - "folder": "500", - "updated": "2025-07-29T12:00:00+00:00", - "created": "2025-05-09T12:14:18+00:00", - "owner": "1", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "authorization": null, - "groups": null, - "deleted": null - }, - "vng-gemma": { - "slug": "vng-gemma", - "title": "AMEF", - "version": "0.0.3", - "description": "Register voor AMEF (ArchiMate Model Exchange Format) modellen en architectuur elementen", - "schemas": [ - "element", - "model", - "organization", - "property-definition", - "relation", - "view", - "extendview", - "property" - ], - "source": "", - "tablePrefix": "", - "folder": "Open Registers/VNG Gemma Register", - "updated": "2025-05-15T12:05:44+00:00", - "created": "2025-05-02T10:30:47+00:00", - "owner": null, - "application": null, - "organisation": null, - "authorization": null, - "deleted": null + "components": { + "registers": { + "voorzieningen": { + "slug": "voorzieningen", + "title": "Voorzieningen", + "version": "1.0.7", + "description": "Register voor voorzieningen uit de softwarecatalogus", + "schemas": [ + "sector", + "product", + "dienst", + "kwetsbaarheid", + "contactpersoon", + "organisatie", + "gebruik", + "contract", + "koppeling", + "beoordeeling", + "module", + "compliancy", + "moduleVersie" + ], + "source": "internal", + "tablePrefix": "", + "folder": "500", + "updated": "2025-07-29T12:00:00+00:00", + "created": "2025-05-09T12:14:18+00:00", + "owner": "1", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "authorization": null, + "groups": null, + "deleted": null + }, + "vng-gemma": { + "slug": "vng-gemma", + "title": "AMEF", + "version": "0.0.3", + "description": "Register voor AMEF (ArchiMate Model Exchange Format) modellen en architectuur elementen", + "schemas": [ + "element", + "model", + "organization", + "property-definition", + "relation", + "view", + "extendview", + "property" + ], + "source": "", + "tablePrefix": "", + "folder": "Open Registers/VNG Gemma Register", + "updated": "2025-05-15T12:05:44+00:00", + "created": "2025-05-02T10:30:47+00:00", + "owner": null, + "application": null, + "organisation": null, + "authorization": null, + "deleted": null + } + }, + "endpoints": { + "register": { + "name": "Register", + "description": "Endpoint voor het registreren van organisaties in de softwarecatalogus", + "reference": "", + "version": "0.0.3", + "endpoint": "register", + "endpointArray": ["register"], + "endpointRegex": "#^register$#", + "method": "POST", + "targetType": "register/schema", + "targetId": "voorzieningen/organisatie", + "conditions": [], + "inputMapping": null, + "outputMapping": null, + "rules": ["register-organization-rule"], + "configurations": [], + "slug": "register", + "created": "2025-05-16T13:11:49+00:00", + "updated": "2025-05-16T13:38:20+00:00" + } + }, + "schemas": { + "property": { + "uri": null, + "slug": "property", + "title": "Property", + "description": "Schema voor generieke eigenschappen die aan voorzieningen of relaties kunnen hangen", + "version": "0.0.4", + "summary": "", + "icon": "Tag", + "required": ["name", "type"], + "properties": { + "name": { + "description": "Naam van de eigenschap", + "type": "string", + "minLength": 1, + "maxLength": 200, + "title": "Naam" + }, + "type": { + "description": "Datatype van de eigenschap", + "type": "string", + "enum": ["string", "number", "boolean", "date"], + "title": "Type" + }, + "value": { + "description": "Waarde van de eigenschap", + "type": "string", + "maxLength": 1000, + "title": "Waarde" + }, + "lang": { + "description": "Taalcode (optioneel)", + "type": "string", + "minLength": 2, + "maxLength": 2, + "title": "Taal" + } + }, + "archive": [], + "source": "", + "hardValidation": false, + "updated": "2025-08-10T00:00:00+00:00", + "created": "2025-08-10T00:00:00+00:00", + "maxDepth": 0, + "owner": "system", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": { + "create": [ + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar", + "aanbod-beheerder" + ], + "read": [ + "public", + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "gebruik-raadpleger" + ], + "update": [ + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "gebruik-raadpleger", + "organisatie-beheerder", + "organisaties-beheerder", + "software-catalog-admins", + "software-catalog-users", + "vng-raadpleger" + ], + "delete": [ + "aanbod-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar" + ] + }, + "deleted": null, + "configuration": null + }, + "sector": { + "uri": null, + "slug": "sector", + "title": "Sector", + "description": "Schema voor sectoren binnen de softwarecatalogus", + "version": "0.0.5", + "summary": "", + "icon": "Domain", + "required": ["naam"], + "properties": { + "naam": { + "description": "Naam van de sector", + "type": "string", + "required": true, + "visible": true, + "order": 1, + "facetable": false, + "title": "Naam", + "maxLength": 200, + "example": "Bijvoorbeeld: Overheid" + }, + "beschrijving": { + "description": "Beschrijving van de sector", + "type": "string", + "visible": true, + "order": 2, + "facetable": false, + "title": "Beschrijving", + "maxLength": 1000, + "example": "Bijvoorbeeld: Publieke sector en overheidsdiensten" + } + }, + "archive": [], + "source": "", + "hardValidation": false, + "immutable": false, + "updated": "2025-05-13T19:35:39+00:00", + "created": "2025-05-01T14:49:42+00:00", + "maxDepth": 0, + "owner": "system", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": { + "create": [ + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar", + "aanbod-beheerder" + ], + "read": [ + "public", + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "gebruik-raadpleger" + ], + "update": [ + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "gebruik-raadpleger", + "organisatie-beheerder", + "organisaties-beheerder", + "software-catalog-admins", + "software-catalog-users", + "vng-raadpleger" + ], + "delete": [ + "aanbod-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar" + ] + }, + "deleted": null, + "configuration": { + "objectNameField": "naam", + "objectDescriptionField": "beschrijving" + } + }, + "product": { + "uri": null, + "slug": "product", + "title": "Product", + "description": "Een product, suite of monobrand", + "version": "0.0.84", + "summary": "", + "icon": "ApplicationCog", + "required": ["naam", "beschrijvingKort"], + "properties": { + "naam": { + "description": "Naam van het product,suite of monobrand", + "type": "string", + "required": true, + "visible": true, + "order": 1, + "minLength": null, + "maxLength": 200, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Naam", + "example": "Voorbeeld: VNG Product Suite" + }, + "beschrijvingLang": { + "description": "Beschrijving van het product, suite of monobrand voor op de detailpagina", + "type": "string", + "format": "markdown", + "visible": true, + "order": 5, + "minLength": null, + "maxLength": 5000, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Uitgebreide omschrijving", + "example": "Bijvoorbeeld: Een uitgebreide beschrijving van het product met alle functionaliteiten en kenmerken" + }, + "beschrijvingKort": { + "type": "string", + "title": "Korte omschrijving", + "description": "Korte beschrijving van het product, suite of monobrand voor in de weergave in tabellen en zoekresultaten", + "facetable": false, + "maxLength": 255, + "order": 3, + "example": "Bijvoorbeeld: Een korte samenvatting van het product" + }, + "website": { + "description": "Website van het product", + "type": "string", + "format": "url", + "required": true, + "visible": true, + "order": 1, + "minLength": null, + "maxLength": 500, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Website", + "example": "https://voorbeeld.nl/product" + }, + "contactpersoon": { + "description": "Contactpersoon voor het product, suite of monobrand", + "type": "object", + "visible": true, + "order": 0, + "facetable": false, + "title": "Contact", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/contactpersoon" + }, + "cloudDienstverleningsmodel": { + "description": "Het cloud dienstverleningsmodel voor het product, suite of monobrand", + "type": "string", + "order": 0, + "objectConfiguration": {}, + "fileConfiguration": {}, + "oneOf": [], + "enum": ["On-premises (self-managed)", "IaaS", "PaaS", "SaaS"], + "facetable": true, + "title": "Hosting vorm", + "example": "Bijvoorbeeld: SaaS" + }, + "hostingJurisdictie": { + "description": "De jurisdictie waar de hosting plaatsvindt", + "type": "string", + "visible": true, + "order": 0, + "enum": ["NL", "EU", "US", "Elders"], + "facetable": true, + "title": "In welk land wordt de data opgeslagen?", + "example": "Bijvoorbeeld: NL" + }, + "hostingLocatie": { + "description": "De locatie waar de hosting plaatsvindt", + "type": "string", + "visible": true, + "order": 0, + "enum": ["NL", "EU", "US", "Elders"], + "facetable": true, + "title": "Waar wordt de applicatie gehost?", + "example": "Bijvoorbeeld: NL" + }, + "logo": { + "description": "URL naar het logo van het product, suite of monobrand", + "type": "string", + "format": "url", + "visible": true, + "order": 4, + "objectConfiguration": {}, + "fileConfiguration": {}, + "oneOf": [], + "facetable": false, + "title": "Logo", + "example": "https://voorbeeld.nl/logo.png" + }, + "aanbieder": { + "description": "De aanbieder van het product, suite of monobrand", + "type": "object", + "visible": true, + "order": 2, + "facetable": false, + "title": "Aanbieder", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/organisatie" + }, + "modules": { + "description": "De applicaties en systeemsoftware die onderdeel zijn van dit product, suite of monobrand", + "type": "array", + "visible": true, + "order": 12, + "facetable": false, + "title": "Modules", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/module", + "inversedBy": "product" } + }, + "omvat": { + "description": "Andere producten, suites of monobrands die onderdeel zijn van dit product", + "type": "array", + "visible": true, + "order": 13, + "facetable": false, + "title": "Omvat", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/product", + "inversedBy": "onderdeelVan" + } + }, + "onderdeelVan": { + "description": "Producten, suites of monobrands waarvan dit product, suite of monobrand onderdeel is", + "type": "array", + "visible": true, + "order": 14, + "facetable": false, + "title": "Onderdeel van", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/product", + "inversedBy": "omvat" + } + } }, - "endpoints": { - "register": { - "name": "Register", - "description": "Endpoint voor het registreren van organisaties in de softwarecatalogus", - "reference": "", - "version": "0.0.3", - "endpoint": "register", - "endpointArray": [ - "register" - ], - "endpointRegex": "#^register$#", - "method": "POST", - "targetType": "register/schema", - "targetId": "voorzieningen/organisatie", - "conditions": [], - "inputMapping": null, - "outputMapping": null, - "rules": [ - "register-organization-rule" + "archive": [], + "source": "internal", + "hardValidation": false, + "immutable": false, + "updated": "2025-07-29T09:35:54+00:00", + "created": "2025-05-09T12:14:18+00:00", + "maxDepth": 0, + "owner": "1", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": { + "create": [ + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar", + "aanbod-beheerder" + ], + "read": [ + "public", + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "gebruik-raadpleger" + ], + "update": [ + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "gebruik-raadpleger", + "organisatie-beheerder", + "organisaties-beheerder", + "software-catalog-admins", + "software-catalog-users", + "vng-raadpleger" + ], + "delete": [ + "aanbod-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar" + ] + }, + "deleted": null, + "configuration": { + "objectNameField": "naam", + "objectSummaryField": "beschrijvingKort", + "objectDescriptionField": "beschrijvingLang", + "objectImageField": "logo", + "allowFiles": true, + "allowedTags": ["DPIA", "Handleiding"] + } + }, + "dienst": { + "uri": null, + "slug": "dienst", + "title": "Dienst", + "description": "Een specifiek aanbod van een dienst op een of meerdere producten door een leverancier", + "version": "0.0.40", + "summary": "", + "icon": "Handshake", + "required": ["naam", "producten", "aanbieder"], + "properties": { + "naam": { + "description": "De naam van de dienst", + "type": "string", + "required": true, + "visible": true, + "order": 1, + "minLength": null, + "maxLength": 200, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Naam", + "example": "Bijvoorbeeld: Implementatie en ondersteuning" + }, + "beschrijvingKort": { + "type": "string", + "description": "Korte beschrijving van de dienst", + "title": "Samenvatting", + "facetable": false, + "maxLength": 255, + "order": 5, + "example": "Bijvoorbeeld: Korte beschrijving van de dienst" + }, + "beschrijvingLang": { + "description": "Uitgebreide beschrijving van de dienst", + "type": "string", + "format": "markdown", + "visible": true, + "order": 6, + "facetable": false, + "title": "Beschrijving", + "maxLength": 5000, + "example": "Bijvoorbeeld: Uitgebreide beschrijving van de dienst met alle details" + }, + "website": { + "type": "string", + "format": "url", + "description": "De website waarop meer informatie over dit aanbod te vinden is", + "facetable": false, + "title": "Website", + "order": 4, + "visible": true, + "maxLength": 500, + "example": "https://dienst.voorbeeld.nl" + }, + "status": { + "description": "De status van dit aanbod", + "type": "string", + "default": "concept", + "example": "Bijvoorbeeld: concept" + }, + "contactpersoon": { + "description": "Contactpersoon voor deze dienst", + "type": "object", + "visible": true, + "order": 1, + "facetable": false, + "title": "Contactpersoon", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/contactpersoon" + }, + "producten": { + "description": "Welke producten worden via de dienst aangeboden", + "type": "array", + "required": true, + "visible": true, + "order": 2, + "facetable": false, + "title": "Producten", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/product" + } + }, + "aanbieder": { + "description": "De leverende partij die deze dienst beschikbaar stelt", + "type": "object", + "required": true, + "visible": true, + "order": 3, + "facetable": false, + "title": "Aanbieder", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/organisatie" + }, + "type": { + "description": "Het type dienst dat wordt aangeboden", + "type": "string", + "visible": true, + "order": 4, + "facetable": true, + "title": "Soort dienst", + "enum": [ + "Functioneel beheer", + "Applicatiebeheer", + "Technisch beheer", + "Implementatieondersteuning", + "Opleidingen", + "Licentiereseller" + ], + "example": "Bijvoorbeeld: Implementatieondersteuning" + }, + "logo": { + "description": "URL naar het logo van de dienst", + "type": "string", + "format": "url", + "visible": true, + "order": 5, + "facetable": false, + "title": "Logo", + "example": "https://dienst.voorbeeld.nl/logo.png" + }, + "modules": { + "description": "De modules die onderdeel zijn van deze dienst", + "type": "array", + "visible": true, + "order": 7, + "facetable": false, + "title": "Modules", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/module", + "inversedBy": "dienst" + } + }, + "koppelingen": { + "description": "Koppelingen die gebruikt worden door deze dienst", + "type": "array", + "visible": true, + "order": 8, + "facetable": false, + "title": "Koppelingen", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/koppeling", + "inversedBy": "dienst" + } + } + }, + "archive": [], + "source": "internal", + "hardValidation": false, + "immutable": false, + "updated": "2025-07-29T09:35:54+00:00", + "created": "2025-05-09T12:14:18+00:00", + "maxDepth": 0, + "owner": "1", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": { + "create": [ + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar", + "aanbod-beheerder" + ], + "read": [ + "public", + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "gebruik-raadpleger" + ], + "update": [ + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "gebruik-raadpleger", + "organisatie-beheerder", + "organisaties-beheerder", + "software-catalog-admins", + "software-catalog-users", + "vng-raadpleger" + ], + "delete": [ + "aanbod-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar" + ] + }, + "deleted": null, + "configuration": { + "objectNameField": "naam", + "objectSummaryField": "beschrijvingKort", + "objectDescriptionField": "beschrijvingLang", + "objectImageField": "logo", + "allowFiles": true, + "allowedTags": [ + "ISO-9001", + "ISO-27001", + "ISO-16075", + "Verklaring van toepasselijkheid" + ] + } + }, + "kwetsbaarheid": { + "uri": null, + "slug": "kwetsbaarheid", + "title": "Kwetsbaarheid", + "description": "Schema voor kwetsbaarheden", + "version": "1.0.14", + "summary": "", + "icon": "ShieldAlert", + "required": ["naam", "beschrijvingKort", "modules"], + "properties": { + "naam": { + "description": "Naam van de kwetsbaarheid", + "type": "string", + "visible": true, + "order": 1, + "facetable": false, + "title": "Naam", + "maxLength": 200, + "example": "Bijvoorbeeld: SQL Injection" + }, + "beschrijvingKort": { + "description": "Korte beschrijving van de kwetsbaarheid", + "type": "string", + "maxLength": 255, + "visible": true, + "order": 2, + "facetable": false, + "title": "Samenvatting", + "example": "Bijvoorbeeld: Korte beschrijving van de kwetsbaarheid" + }, + "beschrijvingLang": { + "description": "Uitgebreide beschrijving van de kwetsbaarheid", + "type": "string", + "format": "markdown", + "visible": true, + "order": 3, + "facetable": false, + "title": "Beschrijving", + "maxLength": 5000, + "example": "Bijvoorbeeld: Uitgebreide beschrijving van de kwetsbaarheid" + }, + "cveCode": { + "description": "CVE (Common Vulnerabilities and Exposures) identificatiecode", + "type": "string", + "pattern": "^CVE-\\d{4}-\\d{4,}$", + "visible": true, + "order": 4, + "facetable": true, + "title": "CVE Code", + "maxLength": 20, + "example": "Bijvoorbeeld: CVE-2021-44228" + }, + "cvssScore": { + "description": "CVSS (Common Vulnerability Scoring System) score van 0.0 tot 10.0", + "type": "number", + "minimum": 0.0, + "maximum": 10.0, + "visible": true, + "order": 5, + "facetable": true, + "title": "CVSS Score", + "example": "Bijvoorbeeld: 9.8" + }, + "modules": { + "description": "De modules die door deze kwetsbaarheid getroffen worden", + "type": "array", + "visible": true, + "order": 6, + "facetable": false, + "title": "Getroffen Modules", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/module", + "inversedBy": "kwetsbaarheid" + } + } + }, + "archive": [], + "source": "internal", + "hardValidation": true, + "immutable": false, + "updated": "2025-05-09T12:14:18+00:00", + "created": "2025-05-09T12:14:18+00:00", + "maxDepth": 0, + "owner": "1", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": { + "create": [ + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar", + "aanbod-beheerder" + ], + "read": [ + "public", + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "gebruik-raadpleger" + ], + "update": [ + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "gebruik-raadpleger", + "organisatie-beheerder", + "organisaties-beheerder", + "software-catalog-admins", + "software-catalog-users", + "vng-raadpleger" + ], + "delete": [ + "aanbod-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar" + ] + }, + "deleted": null, + "configuration": { + "objectNameField": "naam", + "objectSummaryField": "beschrijvingKort", + "objectDescriptionField": "beschrijvingLang" + } + }, + "contactpersoon": { + "uri": null, + "slug": "contactpersoon", + "title": "Contact Persoon", + "description": "Contactgegevens van een persoon", + "version": "0.0.20", + "summary": "", + "icon": "AccountMultiple", + "required": ["organisatie", "e-mailadres"], + "properties": { + "voornaam": { + "type": "string", + "description": "Voornaam van de contactpersoon", + "facetable": false, + "title": "Voornaam", + "order": 11, + "maxLength": 100, + "example": "Bijvoorbeeld: Jan" + }, + "tussenvoegsel": { + "type": "string", + "description": "Tussenvoegsel van de contactpersoon", + "facetable": false, + "title": "Tussenvoegsel", + "order": 9, + "maxLength": 20, + "example": "Bijvoorbeeld: van" + }, + "achternaam": { + "type": "string", + "description": "Achternaam van de contactpersoon", + "facetable": false, + "title": "Achternaam", + "order": 1, + "maxLength": 100, + "example": "Bijvoorbeeld: Jansen" + }, + "functie": { + "description": "Functie van de medewerker", + "type": "string", + "visible": true, + "minLength": null, + "maxLength": 100, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Functie", + "order": 3, + "example": "Bijvoorbeeld: Beheerder" + }, + "organisatie": { + "type": "object", + "title": "Organisatie", + "description": "De organisatie waartoe deze contactpersoon behoort", + "facetable": false, + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/organisatie", + "required": true, + "order": 6 + }, + "username": { + "description": "Gebruikersnaam van de contactpersoon", + "title": "Gebruikersnaam", + "type": "string", + "visible": true, + "facetable": false, + "order": 10, + "minLength": null, + "maxLength": 50, + "immutable": true, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "telefoonnummer": { + "type": "string", + "description": "Telefoonnummer van de contactpersoon", + "facetable": false, + "title": "Telefoonnummer", + "order": 8, + "example": "Bijvoorbeeld: 06 12345678" + }, + "isAanspreekpunt": { + "type": "boolean", + "title": "Is aanspreekpunt", + "description": "Geeft aan of deze persoon het aanspreekpunt is voor deze organisatie (publiek word gedeeld", + "facetable": false, + "order": 4, + "example": "Bijvoorbeeld: true" + }, + "notificaties": { + "type": "array", + "title": "Notificaties", + "description": "Lijst van notificaties voor deze contactpersoon", + "facetable": false, + "items": { + "type": "string" + }, + "order": 5, + "example": "Bijvoorbeeld: [\"email\", \"sms\"]" + }, + "rollen": { + "description": "De rollen die deze contactpersoon heeft", + "title": "Rollen", + "type": "array", + "visible": true, + "facetable": false, + "order": 7, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "register": "", + "writeBack": false, + "removeAfterWriteBack": false, + "items": { + "type": "string" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "enum": [ + "Aanbod-beheerder", + "Gebruik-beheerder", + "Gebruik-raadpleger", + "Functioneel-beheerder", + "VNG-raadpleger", + "Organisatie-beheerder" + ], + "example": "Bijvoorbeeld: [\"Aanbod-beheerder\", \"Functioneel-beheerder\"]" + }, + "e-mailadres": { + "description": "E-mailadres van de contactpersoon", + "title": "E-mailadres", + "type": "string", + "format": "email", + "required": true, + "visible": true, + "facetable": false, + "order": 2, + "minLength": null, + "maxLength": 320, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "register": "", + "writeBack": false, + "removeAfterWriteBack": false, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "example": "Bijvoorbeeld: jan.jansen@organisatie.nl" + } + }, + "archive": [], + "source": "internal", + "hardValidation": false, + "immutable": false, + "updated": "2025-06-05T11:53:54+00:00", + "created": "2025-05-09T12:14:18+00:00", + "maxDepth": 0, + "owner": "1", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": { + "create": [ + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar", + "aanbod-beheerder" + ], + "read": [ + "public", + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "gebruik-raadpleger" + ], + "update": [ + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "gebruik-raadpleger", + "organisatie-beheerder", + "organisaties-beheerder", + "software-catalog-admins", + "software-catalog-users", + "vng-raadpleger" + ], + "delete": [ + "aanbod-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar" + ] + }, + "deleted": null, + "configuration": { + "objectNameField": "achternaam", + "objectDescriptionField": "functie" + } + }, + "organisatie": { + "uri": null, + "slug": "organisatie", + "title": "Organisatie", + "description": "Een organisatie die voorzieningen aanbiedt", + "version": "0.0.93", + "summary": "", + "icon": "OfficeBuildingOutline", + "required": ["naam", "type", "website"], + "properties": { + "naam": { + "description": "Naam van de organisatie", + "type": "string", + "required": true, + "visible": true, + "order": 1, + "minLength": null, + "maxLength": 200, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Naam", + "example": "Bijvoorbeeld: VNG Realisatie" + }, + "beschrijvingKort": { + "description": "Beschrijving van de leverancier", + "type": "string", + "visible": true, + "order": 1, + "facetable": false, + "title": "Samenvatting", + "maxLength": 255 + }, + "beschrijvingLang": { + "description": "Overige informatie", + "type": "string", + "visible": true, + "order": 2, + "facetable": false, + "title": "Beschrijving", + "format": "markdown", + "maxLength": 5000 + }, + "logo": { + "description": "Logo van de organisatie", + "type": "string", + "format": "uri", + "visible": true, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Logo", + "order": 10 + }, + "cbsCode": { + "description": "CBS nummer van de organisatie", + "type": "number", + "visible": true, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "CBS Nummer", + "order": 3 + }, + "kvkNummer": { + "description": "KvK-nummer van de organisatie", + "type": "string", + "visible": true, + "order": 9, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "KvK Nummer", + "example": "12345678" + }, + "contactpersonen": { + "description": "De contactpersoon van de organisatie", + "type": "array", + "visible": true, + "order": 4, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "items": { + "cascadeDelete": false, + "$ref": "#/components/schemas/contactpersoon", + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "inversedBy": "organisatie" + }, + "objectConfiguration": { + "handling": null, + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Contactpersonen" + }, + "e-mailadres": { + "description": "Het e-mailadres van de contactpersoon of de organisatie", + "type": "string", + "visible": true, + "order": 7, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "E-mailadres", + "example": "contact@organisatie.nl" + }, + "website": { + "description": "URL van de website van de organisatie", + "title": "Website", + "type": "string", + "required": true, + "visible": true, + "facetable": false, + "order": 16, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "example": "https://www.organisatie.nl" + }, + "telefoonnummer": { + "description": "Telefoonnummer van de contactpersoon of de organisatie", + "title": "Telefoonnummer", + "type": "string", + "visible": true, + "facetable": false, + "order": 15, + "minLength": null, + "maxLength": null, + "example": "06 12345678", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "deelnames": { + "description": "Deelnames van deze organisatie in andere organisaties", + "type": "array", + "visible": true, + "order": 5, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "items": { + "cascadeDelete": false, + "$ref": "#/components/schemas/organisatie", + "type": "object", + "objectConfiguration": { + "handling": "related-object" + } + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Deelnames" + }, + "deelnemers": { + "description": "Deelnemers in deze organisatie", + "type": "array", + "visible": true, + "order": 6, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "items": { + "cascadeDelete": false, + "$ref": "#/components/schemas/organisatie", + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "inversedBy": "deelnames" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Deelnemers", + "writeBack": true, + "removeAfterWriteBack": false + }, + "type": { + "description": "Type van de organisatie (Gemeente, Leverancier, Samenwerking) ", + "title": "Organisatie Type", + "type": "string", + "required": true, + "visible": true, + "facetable": true, + "order": 3, + "minLength": null, + "maxLength": null, + "immutable": true, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "enum": ["Gemeente", "Leverancier", "Samenwerking", "Community"] + }, + "status": { + "description": "Geeft aan of de VNG de organisatie positief beoordeeld heeft voor toegang tot de Softwarecatalogus", + "title": "Status", + "type": "string", + "default": "concept", + "visible": true, + "facetable": false, + "order": 17, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "enum": ["Concept", "Actief", "Deactief"] + }, + "samenwerkingtype": { + "description": "Type samenwerking van de organisatie", + "title": "Samenwerkingstype", + "type": "string", + "visible": true, + "facetable": true, + "order": 14, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "enum": [ + "Uitvoeringsorganisatie", + "Sociaal Domein samenwerking", + "Shared Service Center", + "samenwerkingtype", + "Omgevingsdienst", + "ICT (bijvoorbeeld Shared Service Center)", + "Gemeentelijke herindeling (gepland)", + "Gemeenschappelijke Regeling (samenwerking meerdere domeinen)", + "Gemeenschappelijke Regeling", + "DVO", + "Centrumgemeenteregeling", + "Belastingsamenwerking", + "Bedrijfsvoeringsorganisatie", + "Archiefdienst (regionaal)", + "Ambtelijke fusie" + ] + } + }, + "archive": [], + "source": "internal", + "hardValidation": false, + "immutable": false, + "updated": "2025-07-29T09:35:54+00:00", + "created": "2025-05-09T12:14:18+00:00", + "maxDepth": 0, + "owner": "1", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": { + "create": [ + "public", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar", + "aanbod-beheerder" + ], + "read": [ + "public", + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "gebruik-raadpleger" + ], + "update": [ + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "gebruik-raadpleger", + "organisatie-beheerder", + "organisaties-beheerder", + "software-catalog-admins", + "software-catalog-users", + "vng-raadpleger" + ], + "delete": [ + "aanbod-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar" + ] + }, + "deleted": null, + "configuration": { + "objectNameField": "naam", + "objectSummaryField": "beschrijvingKort", + "objectDescriptionField": "beschrijvingLang", + "objectImageField": "logo", + "allowFiles": true, + "allowedTags": [ + "Verklaring betaling sociale premies", + "Verklaring betaling belastingen", + "Bestuurdersverklaring", + "Uittreksel KvK", + "BTW-nummer bevestiging", + "Compliance verklaring", + "Jaarrekening", + "ISO-certificaten", + "Privacy verklaring", + "AVG compliance document", + "ESPD (Europees aanbestedingsdocument)", + "Integriteitsverklaring", + "Financiële capaciteitsverklaring", + "Technische capaciteitsverklaring", + "Kwaliteitscertificaten", + "Milieucertificaten", + "Verzekeringsbewijs", + "Beroepsaansprakelijkheidsverzekering", + "Referentieprojecten", + "VCA-certificaat", + "BRL-certificaten", + "CE-markering documenten", + "Aanbestedingsdocumentatie" + ] + } + }, + "gebruik": { + "uri": null, + "slug": "gebruik", + "title": "Gebruik", + "description": "Het gebruik van producten, modules, diensten en koppelingen door afnemers", + "version": "1.0.3", + "summary": "", + "icon": "Usage", + "required": ["afnemer", "product", "status"], + "properties": { + "afnemer": { + "type": "object", + "title": "Afnemer", + "description": "De organisatie die afnemer is van het product", + "facetable": false, + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/organisatie", + "required": true, + "order": 11 + }, + "product": { + "type": "object", + "title": "Product", + "description": "Het product dat gebruikt wordt", + "facetable": false, + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/product", + "required": true, + "order": 19 + }, + "contactpersoon": { + "type": "object", + "description": "De contactpersoon voor dit productgebruik", + "facetable": false, + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/contactpersoon", + "title": "Contactpersoon", + "order": 3 + }, + "deelnemers": { + "description": "De organisaties die deelnemen aan dit gebruik (voor samenwerkingen)", + "type": "array", + "visible": true, + "facetable": false, + "title": "Deelnemers", + "order": 6, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/organisatie" + } + }, + "startDatumVerwerving": { + "description": "De start datum voor het \"Verwerving\" status", + "type": "string", + "format": "date", + "visible": true, + "order": 14, + "facetable": false, + "title": "Startdatum Verwerving", + "example": "Bijvoorbeeld: 2025-01-01" + }, + "startDatumGepland": { + "description": "De start datum voor het \"Gepland\" status", + "type": "string", + "format": "date", + "visible": true, + "order": 16, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Geplande Startdatum", + "example": "Bijvoorbeeld: 2025-02-01" + }, + "startDatumInProductie": { + "description": "De start datum voor het \"actief\" status", + "type": "string", + "format": "date", + "visible": true, + "order": 14, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Startdatum In Productie", + "example": "Bijvoorbeeld: 2025-03-01" + }, + "startDatumUitTeFaseren": { + "description": "De start datum voor het \"Beëindigd\" status", + "type": "string", + "format": "date", + "visible": true, + "order": 15, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Startdatum Uit Te Faseren", + "example": "Bijvoorbeeld: 2025-12-31" + }, + "startDatumUitGefaseerd": { + "description": "De start datum voor het \"Uit gefaseerd\" status", + "type": "string", + "format": "date", + "visible": true, + "order": 15, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Startdatum Uit Gefaseerd", + "example": "Bijvoorbeeld: 2025-12-31" + }, + "status": { + "description": "De status van het gebruik", + "type": "string", + "default": "concept", + "required": true, + "visible": true, + "order": 17, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "enum": [ + "Verwerving", + "Gepland", + "In productie", + "Uit te faseren", + "Uitgefaseerd" + ], + "facetable": false, + "title": "Status", + "example": "Bijvoorbeeld: Gepland" + }, + "interneAantekening": { + "description": "Aanvullende interne informatie over het gebruik van de voorziening", + "type": "string", + "visible": true, + "hideOnCollection": true, + "order": 10, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [], + "facetable": false, + "title": "Interne Aantekening", + "example": "Bijvoorbeeld: Interne notitie over het gebruik" + }, + "module": { + "description": "De specifieke module die gebruikt wordt", + "type": "object", + "visible": true, + "order": 20, + "facetable": false, + "title": "Module", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/module", + "inversedBy": "gebruik" + }, + "moduleVersie": { + "description": "De specifieke versie van de module die gebruikt wordt", + "type": "object", + "visible": true, + "order": 21, + "facetable": false, + "title": "Module Versie", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/moduleVersie", + "inversedBy": "gebruik" + }, + "gebruiktVoorReferentiecomponenten": { + "description": "GEMMA referentiecomponenten waarvoor dit product wordt gebruikt", + "type": "array", + "visible": true, + "order": 22, + "facetable": false, + "title": "Referentiecomponenten", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object", + "queryParams": "gemmaType=referentiecomponent&_extend=aanbevolenStandaarden,verplichteStandaarden" + }, + "$ref": "#/components/schemas/element" + } + }, + "koppelingen": { + "description": "De koppelingen die gebruikt worden binnen dit productgebruik", + "type": "array", + "visible": true, + "order": 24, + "facetable": false, + "title": "Koppelingen", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/koppeling" + } + }, + "diensten": { + "description": "De diensten die onderdeel zijn van dit gebruik", + "type": "array", + "visible": true, + "order": 25, + "facetable": false, + "title": "Diensten", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/dienst" + } + } + }, + "archive": [], + "source": "internal", + "hardValidation": false, + "immutable": false, + "updated": "2025-07-29T09:35:54+00:00", + "created": "2025-05-09T12:14:18+00:00", + "maxDepth": 0, + "owner": "1", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": { + "create": [ + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar", + "aanbod-beheerder" + ], + "read": [ + "public", + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "gebruik-raadpleger" + ], + "update": [ + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "gebruik-raadpleger", + "organisatie-beheerder", + "organisaties-beheerder", + "software-catalog-admins", + "software-catalog-users", + "vng-raadpleger" + ], + "delete": [ + "aanbod-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar" + ] + }, + "deleted": null, + "configuration": { + "objectNameField": "afnemer", + "objectDescriptionField": "product", + "allowFiles": true, + "allowedTags": ["DPIA", "Contract", "Verwerkingsovereenkomst"] + } + }, + "contract": { + "uri": null, + "slug": "contract", + "title": "Contract", + "description": "Een formele overeenkomst voor het inzetten van een Dienst op een Gebruik", + "version": "0.0.7", + "summary": "", + "icon": "FileDocumentEdit", + "required": [ + "dienst", + "gebruik", + "startDatum", + "contractNummer", + "contractType", + "status" + ], + "properties": { + "dienst": { + "description": "De dienst waarop dit contract betrekking heeft", + "type": "object", + "facetable": false, + "required": true, + "title": "Dienst", + "order": 12, + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/dienst" + }, + "gebruik": { + "description": "Het gebruik van de voorziening waarop dit contract betrekking heeft", + "type": "object", + "facetable": false, + "required": true, + "title": "Gebruik", + "order": 13, + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/gebruik" + }, + "startDatum": { + "type": "string", + "format": "date", + "description": "De startdatum van het contract", + "facetable": false, + "required": true, + "title": "Startdatum", + "order": 10, + "example": "Bijvoorbeeld: 2025-01-01" + }, + "eindDatum": { + "type": "string", + "format": "date", + "description": "De einddatum van het contract (indien van toepassing)", + "facetable": false, + "title": "Einddatum", + "order": 6, + "example": "Bijvoorbeeld: 2025-12-31" + }, + "contractNummer": { + "type": "string", + "description": "Het referentienummer van het contract", + "facetable": false, + "required": true, + "title": "Contract Nummer", + "order": 3, + "example": "Bijvoorbeeld: CON-2025-001" + }, + "contractType": { + "type": "string", + "enum": ["SLA", "Licentie", "Onderhoud"], + "description": "Het type contract", + "facetable": false, + "required": true, + "title": "Contract Type", + "order": 4, + "example": "Bijvoorbeeld: SLA" + }, + "kosten": { + "type": "number", + "description": "De kosten verbonden aan het contract", + "facetable": false, + "title": "Kosten", + "order": 7, + "example": "Bijvoorbeeld: 1000.00" + }, + "kostenPeriode": { + "type": "string", + "enum": ["Maandelijks", "Jaarlijks", "Eenmalig"], + "description": "De periode waarop de kosten betrekking hebben", + "facetable": false, + "title": "Kosten Periode", + "order": 8, + "example": "Bijvoorbeeld: Jaarlijks" + }, + "contactpersoonAanbieder": { + "type": "object", + "properties": { + "naam": { + "type": "string" + }, + "email": { + "type": "string" + } + }, + "description": "De contactpersoon bij de aanbieder", + "facetable": false, + "objectConfiguration": { + "handling": "nested-object" + }, + "title": "Contactpersoon Aanbieder", + "order": 1 + }, + "contactpersoonGebruiker": { + "type": "object", + "properties": { + "naam": { + "type": "string" + }, + "email": { + "type": "string" + } + }, + "description": "De contactpersoon bij de gebruiker", + "facetable": false, + "objectConfiguration": { + "handling": "nested-object" + }, + "title": "Contactpersoon Gebruiker", + "order": 2 + }, + "documentReferentie": { + "type": "string", + "description": "Referentie naar het contractdocument", + "facetable": false, + "title": "Document Referentie", + "order": 5, + "example": "Bijvoorbeeld: CON-2025-001.pdf" + }, + "status": { + "type": "string", + "enum": ["Actief", "Verlopen", "In onderhandeling"], + "description": "De status van het contract", + "facetable": false, + "required": true, + "title": "Status", + "order": 11, + "example": "Bijvoorbeeld: Actief" + }, + "opmerkingen": { + "type": "string", + "description": "Aanvullende informatie over het contract", + "facetable": false, + "title": "Remarks", + "order": 9, + "example": "Bijvoorbeeld: Aanvullende opmerkingen over het contract" + } + }, + "archive": [], + "source": "internal", + "hardValidation": false, + "immutable": false, + "updated": "2025-05-09T12:14:18+00:00", + "created": "2025-05-09T12:14:18+00:00", + "maxDepth": 0, + "owner": "1", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": { + "create": [ + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar", + "aanbod-beheerder" + ], + "read": [ + "public", + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisatie-beheerder", + "organisaties-beheerder", + "gebruik-raadpleger" + ], + "update": [ + "aanbod-beheerder", + "ambtenaar", + "functioneel-beheerder", + "gebruik-beheerder", + "gebruik-raadpleger", + "organisatie-beheerder", + "organisaties-beheerder", + "software-catalog-admins", + "software-catalog-users", + "vng-raadpleger" + ], + "delete": [ + "aanbod-beheerder", + "vng-raadpleger", + "software-catalog-users", + "software-catalog-admins", + "organisaties-beheerder", + "organisatie-beheerder", + "gebruik-raadpleger", + "gebruik-beheerder", + "functioneel-beheerder", + "ambtenaar" + ] + }, + "deleted": null, + "configuration": { + "objectNameField": "contractNummer", + "objectDescriptionField": "contractType" + } + }, + "koppeling": { + "uri": null, + "slug": "koppeling", + "title": "Koppeling", + "description": "Schema voor koppelingen tussen modules en systemen. Er moet óf ModuleB óf buitengemeentelijkVoorziening gevuld zijn.", + "version": "0.0.4", + "summary": "", + "icon": "Link", + "required": [], + "properties": { + "naam": { + "description": "Naam van de koppeling", + "type": "string", + "order": 1, + "title": "Naam", + "example": "Bijvoorbeeld: API Koppeling" + }, + "beschrijvingKort": { + "description": "Korte beschrijving van de koppeling", + "type": "string", + "order": 5, + "title": "Samenvatting", + "example": "Bijvoorbeeld: Korte beschrijving van de koppeling" + }, + "beschrijvingLang": { + "description": "Uitgebreide beschrijving van de koppeling", + "type": "string", + "format": "markdown", + "order": 6, + "title": "Beschrijving", + "example": "Bijvoorbeeld: Uitgebreide beschrijving van de koppeling" + }, + "type": { + "description": "Het type koppeling, bijvoorbeeld 'bestandsoverdracht', 'digikoppeling', of 'api'.", + "type": "string", + "order": 1, + "title": "Type", + "enum": [ + "n.v.t.", + "bestandsoverdracht", + "digikoppeling", + "message que", + "upload naar portaal", + "webservices", + "api" + ], + "example": "Bijvoorbeeld: api" + }, + "status": { + "description": "De status van de koppeling", + "type": "string", + "order": 3, + "title": "Status", + "enum": [ + "in ontwikkeling", + "in gebruik", + "einde ondersteuning", + "teruggetrokken" + ] + }, + "datumInOntwikkeling": { + "description": "Startdatum van de ontwikkelingsfase", + "type": "string", + "format": "date", + "order": 4, + "title": "Datum In Ontwikkeling", + "example": "Bijvoorbeeld: 2025-01-01" + }, + "datumInGebruik": { + "description": "Startdatum van gebruik", + "type": "string", + "format": "date", + "order": 5, + "title": "Datum In Gebruik" + }, + "datumEindeOndersteuning": { + "description": "Startdatum einde ondersteuning", + "type": "string", + "format": "date", + "order": 6, + "title": "Datum Einde Ondersteuning" + }, + "datumTeruggetrokken": { + "description": "Datum waarop de koppeling teruggetrokken is", + "type": "string", + "format": "date", + "order": 7, + "title": "Datum Teruggetrokken" + }, + "gegevensuitwisselingRichting": { + "description": "De richting van de gegevensuitwisseling", + "type": "string", + "order": 8, + "title": "Gegevensuitwisseling Richting", + "enum": ["AnaarB", "BnaarA", "bi-directioneel"] + }, + "moduleA": { + "description": "De module waarvan de gegevens uitgewisseld worden", + "type": "object", + "order": 9, + "title": "Module A", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/module", + "inversedBy": "koppeling" + }, + "moduleB": { + "description": "De module waarnaar de gegevens uitgewisseld worden", + "type": "object", + "order": 10, + "title": "Module B", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/module", + "inversedBy": "koppeling" + }, + "buitengemeentelijkVoorziening": { + "description": "Buitengemeentelijke voorziening waarmee gekoppeld wordt", + "type": "object", + "order": 11, + "title": "Buitengemeentelijke Voorziening", + "objectConfiguration": { + "handling": "related-object", + "queryParams": "gemmaType=Buitengemeentenlijke voorziening" + }, + "$ref": "#/components/schemas/element" + }, + "standaardversies": { + "description": "Standaardversies die door deze koppeling geïmplementeerd worden", + "type": "array", + "order": 12, + "title": "Standaard Versies", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object", + "queryParams": "gemmaType=standaardversie" + }, + "$ref": "#/components/schemas/element" + } + }, + "gerealiseerdMetIntermediairModule": { + "description": "Intermediaire module die wordt gebruikt voor de realisatie van deze koppeling", + "type": "object", + "order": 13, + "title": "Gerealiseerd Met Intermediair Module", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/module", + "inversedBy": "koppeling" + }, + "aanbieder": { + "description": "De aanbieder van deze koppeling", + "type": "object", + "visible": true, + "order": 14, + "facetable": false, + "title": "Aanbieder", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/organisatie" + }, + "dienst": { + "description": "De dienst die deze koppeling gebruikt", + "type": "object", + "visible": true, + "order": 15, + "facetable": false, + "title": "Dienst", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/dienst", + "inversedBy": "koppelingen" + } + }, + "archive": [], + "source": "", + "hardValidation": false, + "updated": "2025-08-08T07:11:40+00:00", + "created": "2025-08-08T07:11:40+00:00", + "maxDepth": 0, + "owner": "system", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": null, + "deleted": null, + "configuration": { + "objectNameField": "type", + "objectDescriptionField": "beschrijvingKort" + } + }, + "beoordeeling": { + "uri": null, + "slug": "beoordeeling", + "title": "Beoordeeling", + "description": "Schema voor beoordelingen en waarderingen van producten, modules en diensten", + "version": "0.0.12", + "summary": "", + "icon": "Star", + "required": ["naam", "waardering", "product"], + "properties": { + "naam": { + "description": "Naam van de beoordeling", + "type": "string", + "visible": true, + "required": true, + "facetable": false, + "title": "Naam", + "order": 1 + }, + "beschrijvingKort": { + "description": "Korte beschrijving van de beoordeling", + "type": "string", + "maxLength": 255, + "visible": true, + "facetable": false, + "title": "Samenvatting", + "order": 2 + }, + "beschrijvingLang": { + "description": "Uitgebreide beschrijving van de beoordeling", + "type": "string", + "format": "markdown", + "visible": true, + "facetable": false, + "title": "Beschrijving", + "order": 3 + }, + "waardering": { + "description": "Waardering van 1 tot en met 10", + "type": "integer", + "minimum": 1, + "maximum": 10, + "visible": true, + "required": true, + "facetable": true, + "title": "Waardering", + "order": 4 + }, + "product": { + "description": "Het product dat beoordeeld wordt", + "type": "object", + "visible": true, + "required": true, + "facetable": false, + "title": "Product", + "order": 5, + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/product" + }, + "modules": { + "description": "Optioneel: specifieke modules die beoordeeld worden", + "type": "array", + "visible": true, + "facetable": false, + "title": "Modules", + "order": 6, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/module", + "inversedBy": "beoordeeling" + } + }, + "diensten": { + "description": "Optioneel: specifieke diensten die beoordeeld worden", + "type": "array", + "visible": true, + "facetable": false, + "title": "Diensten", + "order": 7, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/dienst" + } + }, + "koppelingen": { + "description": "Optioneel: specifieke koppelingen die beoordeeld worden", + "type": "array", + "visible": true, + "facetable": false, + "title": "Koppelingen", + "order": 8, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/koppeling" + } + }, + "gebruik": { + "description": "Optioneel: het specifieke gebruik dat beoordeeld wordt", + "type": "object", + "visible": true, + "facetable": false, + "title": "Gebruik", + "order": 9, + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/gebruik" + } + }, + "archive": [], + "source": "internal", + "hardValidation": false, + "immutable": false, + "updated": "2025-05-12T20:01:42+00:00", + "created": "2025-05-12T19:58:51+00:00", + "maxDepth": 0, + "owner": "system", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": null, + "deleted": null, + "configuration": { + "objectNameField": "naam", + "objectSummaryField": "beschrijvingKort", + "objectDescriptionField": "beschrijvingLang" + } + }, + "element": { + "slug": "element", + "title": "Element", + "description": "AMEF Element - Architectuur elementen uit het ArchiMate model", + "version": "0.0.3", + "summary": "", + "icon": "Cube", + "required": ["identifier", "type", "properties"], + "properties": { + "identifier": { + "description": "De identifier van dit Element", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "id-009fa62f25844aa3a87d252bf2b6bb0c", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "type": { + "description": "Het type van dit Element", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Capability", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name": { + "description": "De naam van dit Element", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Publiceren en gebruiken van informatie over datadiensten", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name-lang": { + "description": "De name-language van dit Element", + "type": "string", + "minLength": 2, + "maxLength": 2, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation": { + "description": "De documentation van dit Element", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Dienstenafnemers moeten in online catalogi kunnen opvragen welke diensten, met welke kenmerken, door dienstenaanbieder worden aangeboden. \\nOnder andere ontwikkelaars hebben baat bij informatie over beschikbare diensten en de vereisten voor het gebruik van de dienst (bijv. specificatie van een dienst conform de OAS-standaard).", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation-lang": { + "description": "De documentation-language van dit Element", + "type": "string", + "minLength": 2, + "maxLength": 2, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "properties": { + "description": "De properties van dit Element", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Model_Property.json/19", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + } + }, + "archive": [], + "source": "", + "hardValidation": false, + "updated": "2025-04-01T12:16:33+00:00", + "created": "2025-03-03T13:56:42+00:00", + "maxDepth": 4, + "owner": null, + "application": null, + "organisation": null, + "authorization": null, + "deleted": null, + "configuration": { + "objectNameField": "name", + "objectSummaryField": "summary" + } + }, + "view": { + "slug": "view", + "title": "View", + "description": "AMEF View - Architectuur views en diagrammen uit het ArchiMate model", + "version": "0.0.3", + "summary": "", + "icon": "Eye", + "required": [ + "identifier", + "type", + "name", + "properties", + "nodes", + "connections" + ], + "properties": { + "identifier": { + "description": "De identifier van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "id-a6ee6077d3094afa91fc6ea92a9a2a40", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "type": { + "description": "De type van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Diagram", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "viewpoint": { + "description": "De viewpoint van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Application Structure", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name": { + "description": "De name van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "LV01 BGT basisregistratie en SVB view", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name-lang": { + "description": "De name-language van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation": { + "description": "De documentation van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Toont de referentiecomponenten ter ondersteuning van applicatieservices voor publieksdiensten", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation-lang": { + "description": "De documentation-language van deze View", + "type": "string", + "minLength": 2, + "maxLength": 2, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "properties": { + "description": "De properties van deze View", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Model_Property.json", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "nodes": { + "description": "De nodes van deze View", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View_Node.json", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "connections": { + "description": "De connections van deze View", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View_Connection.json", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + } + }, + "archive": [], + "source": "", + "hardValidation": false, + "updated": "2025-03-25T16:30:49+00:00", + "created": "2025-03-03T13:56:48+00:00", + "maxDepth": 0, + "owner": null, + "application": null, + "organisation": null, + "authorization": null, + "deleted": null, + "configuration": null + }, + "model": { + "slug": "model", + "title": "Model", + "description": "AMEF Model - Volledig ArchiMate model met alle elementen, relaties en views", + "version": "0.0.39", + "summary": "", + "icon": "Database", + "required": [ + "xmlns", + "xsi", + "schemaLocation", + "identifier", + "name", + "name-lang", + "version", + "documentation", + "documentation-lang", + "properties", + "elements", + "relationships", + "organizations", + "propertyDefinitions", + "views" + ], + "properties": { + "xmlns": { + "description": "De xmlns van dit GEMMA Model", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "http://www.opengroup.org/xsd/archimate/3.0/", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "xsi": { + "description": "De xsi van dit GEMMA Model", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "http://www.w3.org/2001/XMLSchema-instance", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "schemaLocation": { + "description": "De schemaLocation van dit GEMMA Model", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "http://www.opengroup.org/xsd/archimate/3.0/ http://www.opengroup.org/xsd/archimate/3.1/archimate3_Diagram.xsd", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "identifier": { + "description": "De identifier van dit GEMMA Model", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "id-b58b6b03-a59d-472b-bd87-88ba77ded4e6", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name": { + "description": "De name van dit GEMMA Model", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "GEMMA release (test)", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name-lang": { + "description": "De name-language van dit GEMMA Model", + "type": "string", + "minLength": 2, + "maxLength": 2, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "version": { + "description": "De version van dit GEMMA Model", + "type": "string", + "minLength": 3, + "maxLength": null, + "example": "3.0", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation": { + "description": "De documentation van dit GEMMA Model", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "De GEMeentelijk Model Architectuur (GEMMA) bevat een blauwdruk van de gemeente en haar informatievoorziening. De GEMMA kan worden gebruikt als basis voor de projectmodellen", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation-lang": { + "description": "De documentation-language van dit GEMMA Model", + "type": "string", + "minLength": 2, + "maxLength": 2, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "properties": { + "description": "De properties van dit GEMMA Model", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": 1, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Model_Property.json/19", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "elements": { + "description": "De elements van dit GEMMA Model", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": 1, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Element.json/10", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "relationships": { + "description": "De relationships van dit GEMMA Model", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": 1, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Relation.json/13", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "organizations": { + "description": "De organizations van dit GEMMA Model", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": 1, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "gebruiker", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "propertyDefinitions": { + "description": "De propertyDefinitions van dit GEMMA Model", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": 1, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Property_Definition.json/15", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "views": { + "description": "De views van dit GEMMA Model", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": 1, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View.json/11", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + } + }, + "archive": [], + "source": "", + "hardValidation": false, + "updated": "2025-04-01T15:12:59+00:00", + "created": "2025-03-03T13:57:16+00:00", + "maxDepth": 4, + "owner": null, + "application": null, + "organisation": null, + "authorization": null, + "deleted": null, + "configuration": null + }, + "organization": { + "slug": "organization", + "title": "Organization", + "description": "AMEF Organization - Organisatie structuren uit het ArchiMate model", + "version": "0.0.3", + "summary": "", + "icon": "Building", + "required": [], + "properties": { + "identifierRef": { + "description": "De identifierRef van deze Organization", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "id-009fa62f25844aa3a87d252bf2b6bb0c", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "label": { + "description": "De label van deze Organization", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Strategy", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "label-lang": { + "description": "De label-language van deze Organization", + "type": "string", + "minLength": 2, + "maxLength": 2, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation": { + "description": "De documentation van deze Organization", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Het Ondernemingsdossier stelt een ondernemer in staat om bepaalde informatie uit de bedrijfsvoering eenmalig vast te leggen en meerdere keren beschikbaar te stellen aan overheden zoals toezichthouders en vergunningverleners", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation-lang": { + "description": "De documentation-language van deze Organization", + "type": "string", + "minLength": 2, + "maxLength": 2, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "item": { + "description": "De items (Organizations) van deze Organization", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "gebruiker", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + } + }, + "archive": [], + "source": "", + "hardValidation": false, + "updated": "2025-04-03T09:28:37+00:00", + "created": "2025-03-03T13:56:55+00:00", + "maxDepth": 4, + "owner": null, + "application": null, + "organisation": null, + "authorization": null, + "deleted": null, + "configuration": null + }, + "property-definition": { + "slug": "property-definition", + "title": "Property Definition", + "description": "AMEF Property Definition - Definitie van eigenschappen voor ArchiMate elementen", + "version": "0.0.3", + "summary": "", + "icon": "Settings", + "required": ["identifier", "type"], + "properties": { + "identifier": { + "description": "De identifier van deze Property Definition", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "propid-43", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "type": { + "description": "De type van deze Property Definition", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "string", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name": { + "description": "De name van deze Property Definition", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "API-portaal", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name-lang": { + "description": "De name-language van deze Property Definition", + "type": "string", + "minLength": 2, + "maxLength": 2, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + } + }, + "archive": [], + "source": "", + "hardValidation": false, + "updated": "2025-03-27T14:48:34+00:00", + "created": "2025-03-10T13:31:19+00:00", + "maxDepth": 0, + "owner": null, + "application": null, + "organisation": null, + "authorization": null, + "deleted": null, + "configuration": null + }, + "relation": { + "slug": "relation", + "title": "Relation", + "description": "AMEF Relation - Relaties tussen architectuur elementen uit het ArchiMate model", + "version": "0.0.3", + "summary": "", + "icon": "ArrowRight", + "required": ["identifier", "source", "target", "type", "properties"], + "properties": { + "identifier": { + "description": "De identifier van deze Relation", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "id-1b46181d68e5477a9c0b5a95a0677924", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "source": { + "description": "De source van deze Relation", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "id-d143a1fc-02dc-11e6-11ba-005056a85f9c", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "target": { + "description": "De target van deze Relation", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "id-a85f22d89af14222a914fcb9ecfe6815", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "type": { + "description": "De type van deze Relation", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Access", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "accessType": { + "description": "De accessType van deze Relation", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Read", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "isDirected": { + "description": "De isDirected van deze Relation", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "true", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name": { + "description": "De name van deze Relation", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Verplicht", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name-lang": { + "description": "De name-language van deze Relation", + "type": "string", + "minLength": 2, + "maxLength": 2, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation": { + "description": "De documentation van deze Relation", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Op basis van het zaaktype routeert de servicebuscomponent de aanvraag naar een Zaakafhandelcomponent (generiek of specifiek).", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation-lang": { + "description": "De documentation-lang van deze Relation", + "type": "string", + "minLength": 2, + "maxLength": 2, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "properties": { + "description": "De properties van deze Relation", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Model_Property.json/19", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + } + }, + "archive": [], + "source": "", + "hardValidation": false, + "updated": "2025-04-01T08:47:45+00:00", + "created": "2025-03-03T13:57:01+00:00", + "maxDepth": 4, + "owner": null, + "application": null, + "organisation": null, + "authorization": null, + "deleted": null, + "configuration": null + }, + "extendview": { + "slug": "extendview", + "title": "Extended View", + "description": "AMEF Extended View - Vooraf uitgebreide view kopie voor prestatie optimalisatie", + "version": "0.0.5", + "summary": "", + "icon": "EyePlus", + "required": [ + "identifier", + "type", + "name", + "properties", + "nodes", + "connections" + ], + "properties": { + "identifier": { + "description": "De identifier van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "id-a6ee6077d3094afa91fc6ea92a9a2a40", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "type": { + "description": "De type van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Diagram", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "viewpoint": { + "description": "De viewpoint van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Application Structure", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name": { + "description": "De name van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "LV01 BGT basisregistratie en SVB view", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "name-lang": { + "description": "De name-language van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation": { + "description": "De documentation van deze View", + "type": "string", + "minLength": null, + "maxLength": null, + "example": "Toont de referentiecomponenten ter ondersteuning van applicatieservices voor publieksdiensten", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "documentation-lang": { + "description": "De documentation-language van deze View", + "type": "string", + "minLength": 2, + "maxLength": 2, + "example": "nl", + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "properties": { + "description": "De properties van deze View", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Model_Property.json", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "nodes": { + "description": "De nodes van deze View", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View_Node.json", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + }, + "connections": { + "description": "De connections van deze View", + "type": "array", + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "$ref": "", + "items": { + "cascadeDelete": true, + "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View_Connection.json", + "type": "object" + }, + "objectConfiguration": { + "handling": "nested-object", + "schema": "" + }, + "fileConfiguration": { + "handling": "ignore", + "allowedMimeTypes": [], + "location": "", + "maxSize": 0 + }, + "oneOf": [] + } + }, + "archive": [], + "source": "", + "hardValidation": false, + "updated": "2025-05-15T10:54:50+00:00", + "created": "2025-05-13T19:49:15+00:00", + "maxDepth": 0, + "owner": null, + "application": null, + "organisation": null, + "authorization": null, + "deleted": null, + "configuration": null + }, + "module": { + "uri": null, + "slug": "module", + "title": "Module", + "description": "Een module is een onderdeel van een product (voorziening)", + "version": "0.0.4", + "summary": "", + "icon": "Package", + "required": ["naam", "product", "beschrijvingKort"], + "properties": { + "naam": { + "type": "string", + "description": "Naam van de module", + "title": "Naam", + "order": 1, + "facetable": false, + "required": true, + "maxLength": 200 + }, + "beschrijvingKort": { + "type": "string", + "description": "Korte beschrijving van de module", + "title": "Korte omschrijving", + "order": 2, + "facetable": false, + "maxLength": 255 + }, + "beschrijvingLang": { + "type": "string", + "description": "Uitgebreide beschrijving van de module", + "title": "Beschrijving", + "order": 3, + "facetable": false, + "format": "markdown", + "maxLength": 5000 + }, + "licentietype": { + "description": "Het type licentie van de voorziening (open source of closed source)", + "type": "string", + "visible": true, + "order": 9, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "default": "Closed source", + "enum": ["Closed source", "Open source"], + "objectConfiguration": {}, + "fileConfiguration": {}, + "oneOf": [], + "facetable": false, + "title": "Licentievorm" + }, + "licentie": { + "description": "De specifieke licentie van de voorziening, alleen opgeven indien Open Source", + "type": "string", + "visible": true, + "order": 10, + "minLength": null, + "maxLength": null, + "minimum": null, + "maximum": null, + "multipleOf": null, + "minItems": null, + "maxItems": null, + "inversedBy": "", + "$ref": "", + "objectConfiguration": {}, + "fileConfiguration": {}, + "oneOf": [], + "facetable": false, + "enum": [ + "MIT License", + "GNU General Public License (GPL)", + "Apache License 2.0", + "BSD Licentie (Berkeley Software Distribution)", + "European Union Public Licence (EUPL), versie 1.2" + ], + "licentie": "License" + }, + "referentieComponenten": { + "description": "GEMMA referentiecomponenten die de module implementeert", + "type": "array", + "visible": true, + "order": 9, + "facetable": true, + "title": "Referentie Componenten", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object", + "queryParams": "gemmaType=referentiecomponent&_extend=aanbevolenStandaarden,verplichteStandaarden" + }, + "$ref": "#/components/schemas/element" + } + }, + "type": { + "description": "Het type module zoals geregistreerd in de catalogus", + "type": "string", + "visible": true, + "order": 11, + "facetable": false, + "title": "Type", + "default": "Applicatie", + "enum": ["Applicatie", "Systeemsoftware"] + }, + "logo": { + "description": "URL naar het logo van de module", + "type": "string", + "format": "url", + "visible": true, + "order": 18, + "facetable": false, + "title": "Logo", + "maxLength": 500 + }, + "producten": { + "description": "De producten waarvan deze module onderdeel is", + "type": "array", + "visible": true, + "order": 19, + "facetable": false, + "title": "Producten", + "hideOnForm": true, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/product", + "inversedBy": "modules" + } + }, + "diensten": { + "description": "De diensten waarvan deze module onderdeel is", + "type": "array", + "visible": true, + "order": 20, + "facetable": false, + "title": "Diensten", + "hideOnForm": true, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/dienst", + "inversedBy": "modules" + } + }, + "koppelingen": { + "description": "De koppelingen waarbij deze module betrokken is", + "type": "array", + "visible": true, + "order": 21, + "facetable": false, + "title": "Koppelingen", + "hideOnForm": true, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/koppeling" + } + }, + "compliancy": { + "description": "De standaarden waar deze module aan voldoet (compliance registraties)", + "type": "array", + "visible": true, + "order": 22, + "facetable": false, + "title": "Compliance", + "hideOnForm": true, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/compliancy" + } + }, + "moduleVersies": { + "description": "De versies van deze module", + "type": "array", + "visible": true, + "order": 23, + "facetable": false, + "title": "Module Versies", + "hideOnForm": true, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/moduleVersie" + } + }, + "gebruiken": { + "description": "Het gebruik van deze module", + "type": "array", + "visible": true, + "order": 24, + "facetable": false, + "title": "Gebruik", + "hideOnForm": true, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/gebruik" + } + }, + "beoordelingen": { + "description": "De beoordelingen van deze module", + "type": "array", + "visible": true, + "order": 25, + "facetable": false, + "title": "Beoordelingen", + "hideOnForm": true, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/beoordeeling" + } + }, + "kwetsbaarheden": { + "description": "De kwetsbaarheden die deze module treffen", + "type": "array", + "visible": true, + "order": 26, + "facetable": false, + "title": "Kwetsbaarheden", + "hideOnForm": true, + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/kwetsbaarheid" + } + } + }, + "archive": [], + "source": "internal", + "hardValidation": false, + "immutable": false, + "updated": "2025-07-29T09:24:00+00:00", + "created": "2025-07-29T09:24:00+00:00", + "maxDepth": 0, + "owner": "system", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": null, + "deleted": null, + "configuration": { + "objectNameField": "naam", + "objectSummaryField": "beschrijvingKort", + "objectDescriptionField": "beschrijvingLang", + "objectImageField": "logo", + "allowFiles": true, + "allowedTags": ["Documentatie", "Handleiding", "Technische specificatie"] + } + }, + "compliancy": { + "uri": null, + "slug": "compliancy", + "title": "Compliancy", + "description": "Schema voor compliancy en standaard ondersteuning", + "version": "0.0.4", + "summary": "", + "icon": "CheckCircle", + "required": [], + "properties": { + "standaardversie": { + "description": "Standaardversie die door deze compliance wordt ondersteund", + "type": "object", + "order": 1, + "title": "Standaard Versie", + "visible": true, + "facetable": false, + "objectConfiguration": { + "handling": "related-object", + "queryParams": "gemmaType=standaardversie" + }, + "$ref": "#/components/schemas/element" + }, + "module": { + "description": "De module waarvan de compliance wordt geregistreerd", + "type": "object", + "order": 4, + "title": "Module", + "visible": true, + "facetable": false, + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/module", + "inversedBy": "compliancy" + }, + "bewijs": { + "description": "Bewijsstuk voor de compliance (bijvoorbeeld testrapport of certificaat)", + "type": "file", + "format": "base64", + "order": 5, + "title": "Bewijs", + "visible": true, + "facetable": false, + "fileConfiguration": { + "allowedMimeTypes": [ + "application/pdf", + "image/jpeg", + "image/png", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ], - "configurations": [], - "slug": "register", - "created": "2025-05-16T13:11:49+00:00", - "updated": "2025-05-16T13:38:20+00:00" + "maxSize": 10485760 } - + } }, - "schemas": { - "property": { - "uri": null, - "slug": "property", - "title": "Property", - "description": "Schema voor generieke eigenschappen die aan voorzieningen of relaties kunnen hangen", - "version": "0.0.4", - "summary": "", - "icon": "Tag", - "required": [ - "name", - "type" - ], - "properties": { - "name": { - "description": "Naam van de eigenschap", - "type": "string", - "minLength": 1, - "maxLength": 200, - "title": "Naam" - }, - "type": { - "description": "Datatype van de eigenschap", - "type": "string", - "enum": [ - "string", - "number", - "boolean", - "date" - ], - "title": "Type" - }, - "value": { - "description": "Waarde van de eigenschap", - "type": "string", - "maxLength": 1000, - "title": "Waarde" - }, - "lang": { - "description": "Taalcode (optioneel)", - "type": "string", - "minLength": 2, - "maxLength": 2, - "title": "Taal" - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-08-10T00:00:00+00:00", - "created": "2025-08-10T00:00:00+00:00", - "maxDepth": 0, - "owner": "system", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": { - "create": [ - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar", - "aanbod-beheerder" - ], - "read": [ - "public", - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisatie-beheerder", - "organisaties-beheerder", - "gebruik-raadpleger" - ], - "update": [ - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "gebruik-raadpleger", - "organisatie-beheerder", - "organisaties-beheerder", - "software-catalog-admins", - "software-catalog-users", - "vng-raadpleger" - ], - "delete": [ - "aanbod-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar" - ] - }, - "deleted": null, - "configuration": null - }, - "sector": { - "uri": null, - "slug": "sector", - "title": "Sector", - "description": "Schema voor sectoren binnen de softwarecatalogus", - "version": "0.0.5", - "summary": "", - "icon": "Domain", - "required": [ - "naam" - ], - "properties": { - "naam": { - "description": "Naam van de sector", - "type": "string", - "required": true, - "visible": true, - "order": 1, - "facetable": false, - "title": "Naam", - "maxLength": 200, - "example": "Bijvoorbeeld: Overheid" - }, - "beschrijving": { - "description": "Beschrijving van de sector", - "type": "string", - "visible": true, - "order": 2, - "facetable": false, - "title": "Beschrijving", - "maxLength": 1000, - "example": "Bijvoorbeeld: Publieke sector en overheidsdiensten" - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "immutable": false, - "updated": "2025-05-13T19:35:39+00:00", - "created": "2025-05-01T14:49:42+00:00", - "maxDepth": 0, - "owner": "system", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": { - "create": [ - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar", - "aanbod-beheerder" - ], - "read": [ - "public", - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisatie-beheerder", - "organisaties-beheerder", - "gebruik-raadpleger" - ], - "update": [ - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "gebruik-raadpleger", - "organisatie-beheerder", - "organisaties-beheerder", - "software-catalog-admins", - "software-catalog-users", - "vng-raadpleger" - ], - "delete": [ - "aanbod-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar" - ] - }, - "deleted": null, - "configuration": { - "objectNameField": "naam", - "objectDescriptionField": "beschrijving" - } - }, - "product": { - "uri": null, - "slug": "product", - "title": "Product", - "description": "Een product, suite of monobrand", - "version": "0.0.84", - "summary": "", - "icon": "ApplicationCog", - "required": [ - "naam", - "beschrijvingKort" - ], - "properties": { - "naam": { - "description": "Naam van het product,suite of monobrand", - "type": "string", - "required": true, - "visible": true, - "order": 1, - "minLength": null, - "maxLength": 200, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Naam", - "example": "Voorbeeld: VNG Product Suite" - }, - "beschrijvingLang": { - "description": "Beschrijving van het product, suite of monobrand voor op de detailpagina", - "type": "string", - "format": "markdown", - "visible": true, - "order": 5, - "minLength": null, - "maxLength": 5000, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Beschrijving", - "example": "Bijvoorbeeld: Een uitgebreide beschrijving van het product met alle functionaliteiten en kenmerken" - }, - "beschrijvingKort": { - "type": "string", - "title": "Samenvatting", - "description": "Korte beschrijving van het product, suite of monobrand voor in de weergave in tabellen en zoekresultaten", - "facetable": false, - "maxLength": 255, - "order": 3, - "example": "Bijvoorbeeld: Een korte samenvatting van het product" - }, - "website": { - "description": "Website van het product", - "type": "string", - "format": "url", - "required": true, - "visible": true, - "order": 1, - "minLength": null, - "maxLength": 500, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Website", - "example": "https://voorbeeld.nl/product" - }, - "contactpersoon": { - "description": "Contactpersoon voor het product, suite of monobrand", - "type": "object", - "visible": true, - "order": 0, - "facetable": false, - "title": "Contact", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/contactpersoon" - }, - "cloudDienstverleningsmodel": { - "description": "Het cloud dienstverleningsmodel voor het product, suite of monobrand", - "type": "string", - "order": 0, - "objectConfiguration": {}, - "fileConfiguration": {}, - "oneOf": [], - "enum": [ - "On-premises (self-managed)", - "IaaS", - "PaaS", - "SaaS" - ], - "facetable": true, - "title": "Hosting vorm", - "example": "Bijvoorbeeld: SaaS" - }, - "hostingJurisdictie": { - "description": "De jurisdictie waar de hosting plaatsvindt", - "type": "string", - "visible": true, - "order": 0, - "enum": [ - "NL", - "EU", - "US", - "Elders" - ], - "facetable": true, - "title": "Hosting jurisdictie", - "example": "Bijvoorbeeld: NL" - }, - "hostingLocatie": { - "description": "De locatie waar de hosting plaatsvindt", - "type": "string", - "visible": true, - "order": 0, - "enum": [ - "NL", - "EU", - "US", - "Elders" - ], - "facetable": true, - "title": "Hosting locatie", - "example": "Bijvoorbeeld: NL" - }, - "logo": { - "description": "URL naar het logo van het product, suite of monobrand", - "type": "string", - "format": "url", - "visible": true, - "order": 4, - "objectConfiguration": {}, - "fileConfiguration": {}, - "oneOf": [], - "facetable": false, - "title": "Logo", - "example": "https://voorbeeld.nl/logo.png" - }, - "aanbieder": { - "description": "De aanbieder van het product, suite of monobrand", - "type": "object", - "visible": true, - "order": 2, - "facetable": false, - "title": "Aanbieder", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/organisatie" - }, - "modules": { - "description": "De applicaties en systeemsoftware die onderdeel zijn van dit product, suite of monobrand", - "type": "array", - "visible": true, - "order": 12, - "facetable": false, - "title": "Modules", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/module", - "inversedBy": "product" - } - }, - "omvat": { - "description": "Andere producten, suites of monobrands die onderdeel zijn van dit product", - "type": "array", - "visible": true, - "order": 13, - "facetable": false, - "title": "Omvat", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/product", - "inversedBy": "onderdeelVan" - } - }, - "onderdeelVan": { - "description": "Producten, suites of monobrands waarvan dit product, suite of monobrand onderdeel is", - "type": "array", - "visible": true, - "order": 14, - "facetable": false, - "title": "Onderdeel van", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/product", - "inversedBy": "omvat" - } - } - }, - "archive": [], - "source": "internal", - "hardValidation": false, - "immutable": false, - "updated": "2025-07-29T09:35:54+00:00", - "created": "2025-05-09T12:14:18+00:00", - "maxDepth": 0, - "owner": "1", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": { - "create": [ - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar", - "aanbod-beheerder" - ], - "read": [ - "public", - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisatie-beheerder", - "organisaties-beheerder", - "gebruik-raadpleger" - ], - "update": [ - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "gebruik-raadpleger", - "organisatie-beheerder", - "organisaties-beheerder", - "software-catalog-admins", - "software-catalog-users", - "vng-raadpleger" - ], - "delete": [ - "aanbod-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar" - ] - }, - "deleted": null, - "configuration": { - "objectNameField": "naam", - "objectSummaryField": "beschrijvingKort", - "objectDescriptionField": "beschrijvingLang", - "objectImageField": "logo", - "allowFiles": true, - "allowedTags": [ - "DPIA", - "Handleiding" - ] - } - }, - "dienst": { - "uri": null, - "slug": "dienst", - "title": "Dienst", - "description": "Een specifiek aanbod van een dienst op een of meerdere producten door een leverancier", - "version": "0.0.40", - "summary": "", - "icon": "Handshake", - "required": [ - "naam", - "producten", - "aanbieder" - ], - "properties": { - "naam": { - "description": "De naam van de dienst", - "type": "string", - "required": true, - "visible": true, - "order": 1, - "minLength": null, - "maxLength": 200, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Naam", - "example": "Bijvoorbeeld: Implementatie en ondersteuning" - }, - "beschrijvingKort": { - "type": "string", - "description": "Korte beschrijving van de dienst", - "title": "Samenvatting", - "facetable": false, - "maxLength": 255, - "order": 5, - "example": "Bijvoorbeeld: Korte beschrijving van de dienst" - }, - "beschrijvingLang": { - "description": "Uitgebreide beschrijving van de dienst", - "type": "string", - "format": "markdown", - "visible": true, - "order": 6, - "facetable": false, - "title": "Beschrijving", - "maxLength": 5000, - "example": "Bijvoorbeeld: Uitgebreide beschrijving van de dienst met alle details" - }, - "website": { - "type": "string", - "format": "url", - "description": "De website waarop meer informatie over dit aanbod te vinden is", - "facetable": false, - "title": "Website", - "order": 4, - "visible": true, - "maxLength": 500, - "example": "https://dienst.voorbeeld.nl" - }, - "status": { - "description": "De status van dit aanbod", - "type": "string", - "default": "concept", - "example": "Bijvoorbeeld: concept" - }, - "contactpersoon": { - "description": "Contactpersoon voor deze dienst", - "type": "object", - "visible": true, - "order": 1, - "facetable": false, - "title": "Contactpersoon", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/contactpersoon" - }, - "producten": { - "description": "Welke producten worden via de dienst aangeboden", - "type": "array", - "required": true, - "visible": true, - "order": 2, - "facetable": false, - "title": "Producten", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/product" - } - }, - "aanbieder": { - "description": "De leverende partij die deze dienst beschikbaar stelt", - "type": "object", - "required": true, - "visible": true, - "order": 3, - "facetable": false, - "title": "Aanbieder", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/organisatie" - }, - "type": { - "description": "Het type dienst dat wordt aangeboden", - "type": "string", - "visible": true, - "order": 4, - "facetable": true, - "title": "Type Dienst", - "enum": [ - "Functioneel beheer", - "Applicatiebeheer", - "Technisch beheer", - "Implementatieondersteuning", - "Opleidingen", - "Licentiereseller" - ], - "example": "Bijvoorbeeld: Implementatieondersteuning" - }, - "logo": { - "description": "URL naar het logo van de dienst", - "type": "string", - "format": "url", - "visible": true, - "order": 5, - "facetable": false, - "title": "Logo", - "example": "https://dienst.voorbeeld.nl/logo.png" - }, - "modules": { - "description": "De modules die onderdeel zijn van deze dienst", - "type": "array", - "visible": true, - "order": 7, - "facetable": false, - "title": "Modules", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/module", - "inversedBy": "dienst" - } - }, - "koppelingen": { - "description": "Koppelingen die gebruikt worden door deze dienst", - "type": "array", - "visible": true, - "order": 8, - "facetable": false, - "title": "Koppelingen", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/koppeling", - "inversedBy": "dienst" - } - } - }, - "archive": [], - "source": "internal", - "hardValidation": false, - "immutable": false, - "updated": "2025-07-29T09:35:54+00:00", - "created": "2025-05-09T12:14:18+00:00", - "maxDepth": 0, - "owner": "1", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": { - "create": [ - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar", - "aanbod-beheerder" - ], - "read": [ - "public", - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisatie-beheerder", - "organisaties-beheerder", - "gebruik-raadpleger" - ], - "update": [ - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "gebruik-raadpleger", - "organisatie-beheerder", - "organisaties-beheerder", - "software-catalog-admins", - "software-catalog-users", - "vng-raadpleger" - ], - "delete": [ - "aanbod-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar" - ] - }, - "deleted": null, - "configuration": { - "objectNameField": "naam", - "objectSummaryField": "beschrijvingKort", - "objectDescriptionField": "beschrijvingLang", - "objectImageField": "logo", - "allowFiles": true, - "allowedTags": [ - "ISO-9001", - "ISO-27001", - "ISO-16075", - "Verklaring van toepasselijkheid" - ] - } - }, - "kwetsbaarheid": { - "uri": null, - "slug": "kwetsbaarheid", - "title": "Kwetsbaarheid", - "description": "Schema voor kwetsbaarheden", - "version": "1.0.14", - "summary": "", - "icon": "ShieldAlert", - "required": [ - "naam", - "beschrijvingKort", - "modules" - ], - "properties": { - "naam": { - "description": "Naam van de kwetsbaarheid", - "type": "string", - "visible": true, - "order": 1, - "facetable": false, - "title": "Naam", - "maxLength": 200, - "example": "Bijvoorbeeld: SQL Injection" - }, - "beschrijvingKort": { - "description": "Korte beschrijving van de kwetsbaarheid", - "type": "string", - "maxLength": 255, - "visible": true, - "order": 2, - "facetable": false, - "title": "Samenvatting", - "example": "Bijvoorbeeld: Korte beschrijving van de kwetsbaarheid" - }, - "beschrijvingLang": { - "description": "Uitgebreide beschrijving van de kwetsbaarheid", - "type": "string", - "format": "markdown", - "visible": true, - "order": 3, - "facetable": false, - "title": "Beschrijving", - "maxLength": 5000, - "example": "Bijvoorbeeld: Uitgebreide beschrijving van de kwetsbaarheid" - }, - "cveCode": { - "description": "CVE (Common Vulnerabilities and Exposures) identificatiecode", - "type": "string", - "pattern": "^CVE-\\d{4}-\\d{4,}$", - "visible": true, - "order": 4, - "facetable": true, - "title": "CVE Code", - "maxLength": 20, - "example": "Bijvoorbeeld: CVE-2021-44228" - }, - "cvssScore": { - "description": "CVSS (Common Vulnerability Scoring System) score van 0.0 tot 10.0", - "type": "number", - "minimum": 0.0, - "maximum": 10.0, - "visible": true, - "order": 5, - "facetable": true, - "title": "CVSS Score", - "example": "Bijvoorbeeld: 9.8" - }, - "modules": { - "description": "De modules die door deze kwetsbaarheid getroffen worden", - "type": "array", - "visible": true, - "order": 6, - "facetable": false, - "title": "Getroffen Modules", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/module", - "inversedBy": "kwetsbaarheid" - } - } - }, - "archive": [], - "source": "internal", - "hardValidation": true, - "immutable": false, - "updated": "2025-05-09T12:14:18+00:00", - "created": "2025-05-09T12:14:18+00:00", - "maxDepth": 0, - "owner": "1", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": { - "create": [ - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar", - "aanbod-beheerder" - ], - "read": [ - "public", - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisatie-beheerder", - "organisaties-beheerder", - "gebruik-raadpleger" - ], - "update": [ - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "gebruik-raadpleger", - "organisatie-beheerder", - "organisaties-beheerder", - "software-catalog-admins", - "software-catalog-users", - "vng-raadpleger" - ], - "delete": [ - "aanbod-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar" - ] - }, - "deleted": null, - "configuration": { - "objectNameField": "naam", - "objectSummaryField": "beschrijvingKort", - "objectDescriptionField": "beschrijvingLang" - } - }, - "contactpersoon": { - "uri": null, - "slug": "contactpersoon", - "title": "Contact Persoon", - "description": "Contactgegevens van een persoon", - "version": "0.0.20", - "summary": "", - "icon": "AccountMultiple", - "required": [ - "organisatie", - "e-mailadres" - ], - "properties": { - "voornaam": { - "type": "string", - "description": "Voornaam van de contactpersoon", - "facetable": false, - "title": "Voornaam", - "order": 11, - "maxLength": 100, - "example": "Bijvoorbeeld: Jan" - }, - "tussenvoegsel": { - "type": "string", - "description": "Tussenvoegsel van de contactpersoon", - "facetable": false, - "title": "Tussenvoegsel", - "order": 9, - "maxLength": 20, - "example": "Bijvoorbeeld: van" - }, - "achternaam": { - "type": "string", - "description": "Achternaam van de contactpersoon", - "facetable": false, - "title": "Achternaam", - "order": 1, - "maxLength": 100, - "example": "Bijvoorbeeld: Jansen" - }, - "functie": { - "description": "Functie van de medewerker", - "type": "string", - "visible": true, - "minLength": null, - "maxLength": 100, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Functie", - "order": 3, - "example": "Bijvoorbeeld: Beheerder" - }, - "organisatie": { - "type": "object", - "title": "Organisatie", - "description": "De organisatie waartoe deze contactpersoon behoort", - "facetable": false, - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/organisatie", - "required": true, - "order": 6 - }, - "username": { - "description": "Gebruikersnaam van de contactpersoon", - "title": "Gebruikersnaam", - "type": "string", - "visible": true, - "facetable": false, - "order": 10, - "minLength": null, - "maxLength": 50, - "immutable": true, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "telefoonnummer": { - "type": "string", - "description": "Telefoonnummer van de contactpersoon", - "facetable": false, - "title": "Telefoonnummer", - "order": 8, - "example": "Bijvoorbeeld: 06 12345678" - }, - "isAanspreekpunt": { - "type": "boolean", - "title": "Is aanspreekpunt", - "description": "Geeft aan of deze persoon het aanspreekpunt is voor deze organisatie (publiek word gedeeld", - "facetable": false, - "order": 4, - "example": "Bijvoorbeeld: true" - }, - "notificaties": { - "type": "array", - "title": "Notificaties", - "description": "Lijst van notificaties voor deze contactpersoon", - "facetable": false, - "items": { - "type": "string" - }, - "order": 5, - "example": "Bijvoorbeeld: [\"email\", \"sms\"]" - }, - "rollen": { - "description": "De rollen die deze contactpersoon heeft", - "title": "Rollen", - "type": "array", - "visible": true, - "facetable": false, - "order": 7, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "register": "", - "writeBack": false, - "removeAfterWriteBack": false, - "items": { - "type": "string" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "enum": [ - "Aanbod-beheerder", - "Gebruik-beheerder", - "Gebruik-raadpleger", - "Functioneel-beheerder", - "VNG-raadpleger", - "Organisatie-beheerder" - ], - "example": "Bijvoorbeeld: [\"Aanbod-beheerder\", \"Functioneel-beheerder\"]" - }, - "e-mailadres": { - "description": "E-mailadres van de contactpersoon", - "title": "E-mailadres", - "type": "string", - "format": "email", - "required": true, - "visible": true, - "facetable": false, - "order": 2, - "minLength": null, - "maxLength": 320, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "register": "", - "writeBack": false, - "removeAfterWriteBack": false, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "example": "Bijvoorbeeld: jan.jansen@organisatie.nl" - } - }, - "archive": [], - "source": "internal", - "hardValidation": false, - "immutable": false, - "updated": "2025-06-05T11:53:54+00:00", - "created": "2025-05-09T12:14:18+00:00", - "maxDepth": 0, - "owner": "1", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": { - "create": [ - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar", - "aanbod-beheerder" - ], - "read": [ - "public", - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisatie-beheerder", - "organisaties-beheerder", - "gebruik-raadpleger" - ], - "update": [ - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "gebruik-raadpleger", - "organisatie-beheerder", - "organisaties-beheerder", - "software-catalog-admins", - "software-catalog-users", - "vng-raadpleger" - ], - "delete": [ - "aanbod-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar" - ] - }, - "deleted": null, - "configuration": { - "objectNameField": "achternaam", - "objectDescriptionField": "functie" - } - }, - "organisatie": { - "uri": null, - "slug": "organisatie", - "title": "Organisatie", - "description": "Een organisatie die voorzieningen aanbiedt", - "version": "0.0.93", - "summary": "", - "icon": "OfficeBuildingOutline", - "required": [ - "naam", - "type", - "website" - ], - "properties": { - "naam": { - "description": "Naam van de organisatie", - "type": "string", - "required": true, - "visible": true, - "order": 1, - "minLength": null, - "maxLength": 200, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Naam", - "example": "Bijvoorbeeld: VNG Realisatie" - }, - "beschrijvingKort": { - "description": "Beschrijving van de leverancier", - "type": "string", - "visible": true, - "order": 1, - "facetable": false, - "title": "Samenvatting", - "maxLength": 255 - }, - "beschrijvingLang": { - "description": "Overige informatie", - "type": "string", - "visible": true, - "order": 2, - "facetable": false, - "title": "Beschrijving", - "format": "markdown", - "maxLength": 5000 - }, - "logo": { - "description": "Logo van de organisatie", - "type": "string", - "format": "uri", - "visible": true, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Logo", - "order": 10 - }, - "cbsCode": { - "description": "CBS nummer van de organisatie", - "type": "number", - "visible": true, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "CBS Nummer", - "order": 3 - }, - "kvkNummer": { - "description": "KvK-nummer van de organisatie", - "type": "string", - "visible": true, - "order": 9, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "KvK Nummer", - "example": "12345678" - }, - "contactpersonen": { - "description": "De contactpersoon van de organisatie", - "type": "array", - "visible": true, - "order": 4, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "items": { - "cascadeDelete": false, - "$ref": "#/components/schemas/contactpersoon", - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "inversedBy": "organisatie" - }, - "objectConfiguration": { - "handling": null, - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Contactpersonen" - }, - "e-mailadres": { - "description": "Het e-mailadres van de contactpersoon of de organisatie", - "type": "string", - "visible": true, - "order": 7, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "E-mailadres", - "example": "contact@organisatie.nl" - }, - "website": { - "description": "URL van de website van de organisatie", - "title": "Website", - "type": "string", - "required": true, - "visible": true, - "facetable": false, - "order": 16, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "example": "https://www.organisatie.nl" - }, - "telefoonnummer": { - "description": "Telefoonnummer van de contactpersoon of de organisatie", - "title": "Telefoonnummer", - "type": "string", - "visible": true, - "facetable": false, - "order": 15, - "minLength": null, - "maxLength": null, - "example": "06 12345678", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "deelnames": { - "description": "Deelnames van deze organisatie in andere organisaties", - "type": "array", - "visible": true, - "order": 5, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "items": { - "cascadeDelete": false, - "$ref": "#/components/schemas/organisatie", - "type": "object", - "objectConfiguration": { - "handling": "related-object" - } - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Deelnames" - }, - "deelnemers": { - "description": "Deelnemers in deze organisatie", - "type": "array", - "visible": true, - "order": 6, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "items": { - "cascadeDelete": false, - "$ref": "#/components/schemas/organisatie", - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "inversedBy": "deelnames" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Deelnemers", - "writeBack": true, - "removeAfterWriteBack": false - }, - "type": { - "description": "Type van de organisatie (Gemeente, Leverancier, Samenwerking) ", - "title": "Organisatie Type", - "type": "string", - "required": true, - "visible": true, - "facetable": true, - "order": 3, - "minLength": null, - "maxLength": null, - "immutable": true, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "enum": [ - "Gemeente", - "Leverancier", - "Samenwerking", - "Community" - ] - }, - "status": { - "description": "Geeft aan of de VNG de organisatie positief beoordeeld heeft voor toegang tot de Softwarecatalogus", - "title": "Status", - "type": "string", - "default": "concept", - "visible": true, - "facetable": false, - "order": 17, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "enum": [ - "Concept", - "Actief", - "Deactief" - ] - }, - "samenwerkingtype": { - "description": "Type samenwerking van de organisatie", - "title": "Samenwerkingstype", - "type": "string", - "visible": true, - "facetable": true, - "order": 14, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "enum": [ - "Uitvoeringsorganisatie", - "Sociaal Domein samenwerking", - "Shared Service Center", - "samenwerkingtype", - "Omgevingsdienst", - "ICT (bijvoorbeeld Shared Service Center)", - "Gemeentelijke herindeling (gepland)", - "Gemeenschappelijke Regeling (samenwerking meerdere domeinen)", - "Gemeenschappelijke Regeling", - "DVO", - "Centrumgemeenteregeling", - "Belastingsamenwerking", - "Bedrijfsvoeringsorganisatie", - "Archiefdienst (regionaal)", - "Ambtelijke fusie" - ] - } - }, - "archive": [], - "source": "internal", - "hardValidation": false, - "immutable": false, - "updated": "2025-07-29T09:35:54+00:00", - "created": "2025-05-09T12:14:18+00:00", - "maxDepth": 0, - "owner": "1", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": { - "create": [ - "public", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar", - "aanbod-beheerder" - ], - "read": [ - "public", - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisatie-beheerder", - "organisaties-beheerder", - "gebruik-raadpleger" - ], - "update": [ - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "gebruik-raadpleger", - "organisatie-beheerder", - "organisaties-beheerder", - "software-catalog-admins", - "software-catalog-users", - "vng-raadpleger" - ], - "delete": [ - "aanbod-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar" - ] - }, - "deleted": null, - "configuration": { - "objectNameField": "naam", - "objectSummaryField": "beschrijvingKort", - "objectDescriptionField": "beschrijvingLang", - "objectImageField": "logo", - "allowFiles": true, - "allowedTags": [ - "Verklaring betaling sociale premies", - "Verklaring betaling belastingen", - "Bestuurdersverklaring", - "Uittreksel KvK", - "BTW-nummer bevestiging", - "Compliance verklaring", - "Jaarrekening", - "ISO-certificaten", - "Privacy verklaring", - "AVG compliance document", - "ESPD (Europees aanbestedingsdocument)", - "Integriteitsverklaring", - "Financiële capaciteitsverklaring", - "Technische capaciteitsverklaring", - "Kwaliteitscertificaten", - "Milieucertificaten", - "Verzekeringsbewijs", - "Beroepsaansprakelijkheidsverzekering", - "Referentieprojecten", - "VCA-certificaat", - "BRL-certificaten", - "CE-markering documenten", - "Aanbestedingsdocumentatie" - ] - } - }, - "gebruik": { - "uri": null, - "slug": "gebruik", - "title": "Gebruik", - "description": "Het gebruik van producten, modules, diensten en koppelingen door afnemers", - "version": "1.0.3", - "summary": "", - "icon": "Usage", - "required": [ - "afnemer", - "product", - "status" - ], - "properties": { - "afnemer": { - "type": "object", - "title": "Afnemer", - "description": "De organisatie die afnemer is van het product", - "facetable": false, - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/organisatie", - "required": true, - "order": 11 - }, - "product": { - "type": "object", - "title": "Product", - "description": "Het product dat gebruikt wordt", - "facetable": false, - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/product", - "required": true, - "order": 19 - }, - "contactpersoon": { - "type": "object", - "description": "De contactpersoon voor dit productgebruik", - "facetable": false, - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/contactpersoon", - "title": "Contactpersoon", - "order": 3 - }, - "deelnemers": { - "description": "De organisaties die deelnemen aan dit gebruik (voor samenwerkingen)", - "type": "array", - "visible": true, - "facetable": false, - "title": "Deelnemers", - "order": 6, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/organisatie" - } - }, - "startDatumVerwerving": { - "description": "De start datum voor het \"Verwerving\" status", - "type": "string", - "format": "date", - "visible": true, - "order": 14, - "facetable": false, - "title": "Startdatum Verwerving", - "example": "Bijvoorbeeld: 2025-01-01" - }, - "startDatumGepland": { - "description": "De start datum voor het \"Gepland\" status", - "type": "string", - "format": "date", - "visible": true, - "order": 16, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Geplande Startdatum", - "example": "Bijvoorbeeld: 2025-02-01" - }, - "startDatumInProductie": { - "description": "De start datum voor het \"actief\" status", - "type": "string", - "format": "date", - "visible": true, - "order": 14, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Startdatum In Productie", - "example": "Bijvoorbeeld: 2025-03-01" - }, - "startDatumUitTeFaseren": { - "description": "De start datum voor het \"Beëindigd\" status", - "type": "string", - "format": "date", - "visible": true, - "order": 15, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Startdatum Uit Te Faseren", - "example": "Bijvoorbeeld: 2025-12-31" - }, - "startDatumUitGefaseerd": { - "description": "De start datum voor het \"Uit gefaseerd\" status", - "type": "string", - "format": "date", - "visible": true, - "order": 15, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Startdatum Uit Gefaseerd", - "example": "Bijvoorbeeld: 2025-12-31" - }, - "status": { - "description": "De status van het gebruik", - "type": "string", - "default": "concept", - "required": true, - "visible": true, - "order": 17, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "enum": [ - "Verwerving", - "Gepland", - "In productie", - "Uit te faseren", - "Uitgefaseerd" - ], - "facetable": false, - "title": "Status", - "example": "Bijvoorbeeld: Gepland" - }, - "interneAantekening": { - "description": "Aanvullende interne informatie over het gebruik van de voorziening", - "type": "string", - "visible": true, - "hideOnCollection": true, - "order": 10, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [], - "facetable": false, - "title": "Interne Aantekening", - "example": "Bijvoorbeeld: Interne notitie over het gebruik" - }, - "module": { - "description": "De specifieke module die gebruikt wordt", - "type": "object", - "visible": true, - "order": 20, - "facetable": false, - "title": "Module", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/module", - "inversedBy": "gebruik" - }, - "moduleVersie": { - "description": "De specifieke versie van de module die gebruikt wordt", - "type": "object", - "visible": true, - "order": 21, - "facetable": false, - "title": "Module Versie", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/moduleVersie", - "inversedBy": "gebruik" - }, - "gebruiktVoorReferentiecomponenten": { - "description": "GEMMA referentiecomponenten waarvoor dit product wordt gebruikt", - "type": "array", - "visible": true, - "order": 22, - "facetable": false, - "title": "Referentiecomponenten", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object", - "queryParams": "gemmaType=referentiecomponent&_extend=aanbevolenStandaarden,verplichteStandaarden" - }, - "$ref": "#/components/schemas/element" - } - }, - "koppelingen": { - "description": "De koppelingen die gebruikt worden binnen dit productgebruik", - "type": "array", - "visible": true, - "order": 24, - "facetable": false, - "title": "Koppelingen", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/koppeling" - } - }, - "diensten": { - "description": "De diensten die onderdeel zijn van dit gebruik", - "type": "array", - "visible": true, - "order": 25, - "facetable": false, - "title": "Diensten", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/dienst" - } - } - }, - "archive": [], - "source": "internal", - "hardValidation": false, - "immutable": false, - "updated": "2025-07-29T09:35:54+00:00", - "created": "2025-05-09T12:14:18+00:00", - "maxDepth": 0, - "owner": "1", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": { - "create": [ - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar", - "aanbod-beheerder" - ], - "read": [ - "public", - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisatie-beheerder", - "organisaties-beheerder", - "gebruik-raadpleger" - ], - "update": [ - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "gebruik-raadpleger", - "organisatie-beheerder", - "organisaties-beheerder", - "software-catalog-admins", - "software-catalog-users", - "vng-raadpleger" - ], - "delete": [ - "aanbod-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar" - ] - }, - "deleted": null, - "configuration": { - "objectNameField": "afnemer", - "objectDescriptionField": "product", - "allowFiles": true, - "allowedTags": [ - "DPIA", - "Contract", - "Verwerkingsovereenkomst" - ] - } - }, - "contract": { - "uri": null, - "slug": "contract", - "title": "Contract", - "description": "Een formele overeenkomst voor het inzetten van een Dienst op een Gebruik", - "version": "0.0.7", - "summary": "", - "icon": "FileDocumentEdit", - "required": [ - "dienst", - "gebruik", - "startDatum", - "contractNummer", - "contractType", - "status" - ], - "properties": { - "dienst": { - "description": "De dienst waarop dit contract betrekking heeft", - "type": "object", - "facetable": false, - "required": true, - "title": "Dienst", - "order": 12, - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/dienst" - }, - "gebruik": { - "description": "Het gebruik van de voorziening waarop dit contract betrekking heeft", - "type": "object", - "facetable": false, - "required": true, - "title": "Gebruik", - "order": 13, - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/gebruik" - }, - "startDatum": { - "type": "string", - "format": "date", - "description": "De startdatum van het contract", - "facetable": false, - "required": true, - "title": "Startdatum", - "order": 10, - "example": "Bijvoorbeeld: 2025-01-01" - }, - "eindDatum": { - "type": "string", - "format": "date", - "description": "De einddatum van het contract (indien van toepassing)", - "facetable": false, - "title": "Einddatum", - "order": 6, - "example": "Bijvoorbeeld: 2025-12-31" - }, - "contractNummer": { - "type": "string", - "description": "Het referentienummer van het contract", - "facetable": false, - "required": true, - "title": "Contract Nummer", - "order": 3, - "example": "Bijvoorbeeld: CON-2025-001" - }, - "contractType": { - "type": "string", - "enum": [ - "SLA", - "Licentie", - "Onderhoud" - ], - "description": "Het type contract", - "facetable": false, - "required": true, - "title": "Contract Type", - "order": 4, - "example": "Bijvoorbeeld: SLA" - }, - "kosten": { - "type": "number", - "description": "De kosten verbonden aan het contract", - "facetable": false, - "title": "Kosten", - "order": 7, - "example": "Bijvoorbeeld: 1000.00" - }, - "kostenPeriode": { - "type": "string", - "enum": [ - "Maandelijks", - "Jaarlijks", - "Eenmalig" - ], - "description": "De periode waarop de kosten betrekking hebben", - "facetable": false, - "title": "Kosten Periode", - "order": 8, - "example": "Bijvoorbeeld: Jaarlijks" - }, - "contactpersoonAanbieder": { - "type": "object", - "properties": { - "naam": { - "type": "string" - }, - "email": { - "type": "string" - } - }, - "description": "De contactpersoon bij de aanbieder", - "facetable": false, - "objectConfiguration": { - "handling": "nested-object" - }, - "title": "Contactpersoon Aanbieder", - "order": 1 - }, - "contactpersoonGebruiker": { - "type": "object", - "properties": { - "naam": { - "type": "string" - }, - "email": { - "type": "string" - } - }, - "description": "De contactpersoon bij de gebruiker", - "facetable": false, - "objectConfiguration": { - "handling": "nested-object" - }, - "title": "Contactpersoon Gebruiker", - "order": 2 - }, - "documentReferentie": { - "type": "string", - "description": "Referentie naar het contractdocument", - "facetable": false, - "title": "Document Referentie", - "order": 5, - "example": "Bijvoorbeeld: CON-2025-001.pdf" - }, - "status": { - "type": "string", - "enum": [ - "Actief", - "Verlopen", - "In onderhandeling" - ], - "description": "De status van het contract", - "facetable": false, - "required": true, - "title": "Status", - "order": 11, - "example": "Bijvoorbeeld: Actief" - }, - "opmerkingen": { - "type": "string", - "description": "Aanvullende informatie over het contract", - "facetable": false, - "title": "Remarks", - "order": 9, - "example": "Bijvoorbeeld: Aanvullende opmerkingen over het contract" - } - }, - "archive": [], - "source": "internal", - "hardValidation": false, - "immutable": false, - "updated": "2025-05-09T12:14:18+00:00", - "created": "2025-05-09T12:14:18+00:00", - "maxDepth": 0, - "owner": "1", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": { - "create": [ - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar", - "aanbod-beheerder" - ], - "read": [ - "public", - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisatie-beheerder", - "organisaties-beheerder", - "gebruik-raadpleger" - ], - "update": [ - "aanbod-beheerder", - "ambtenaar", - "functioneel-beheerder", - "gebruik-beheerder", - "gebruik-raadpleger", - "organisatie-beheerder", - "organisaties-beheerder", - "software-catalog-admins", - "software-catalog-users", - "vng-raadpleger" - ], - "delete": [ - "aanbod-beheerder", - "vng-raadpleger", - "software-catalog-users", - "software-catalog-admins", - "organisaties-beheerder", - "organisatie-beheerder", - "gebruik-raadpleger", - "gebruik-beheerder", - "functioneel-beheerder", - "ambtenaar" - ] - }, - "deleted": null, - "configuration": { - "objectNameField": "contractNummer", - "objectDescriptionField": "contractType" - } - }, - "koppeling": { - "uri": null, - "slug": "koppeling", - "title": "Koppeling", - "description": "Schema voor koppelingen tussen modules en systemen. Er moet óf ModuleB óf buitengemeentelijkVoorziening gevuld zijn.", - "version": "0.0.4", - "summary": "", - "icon": "Link", - "required": [], - "properties": { - "naam": { - "description": "Naam van de koppeling", - "type": "string", - "order": 1, - "title": "Naam", - "example": "Bijvoorbeeld: API Koppeling" - }, - "beschrijvingKort": { - "description": "Korte beschrijving van de koppeling", - "type": "string", - "order": 5, - "title": "Samenvatting", - "example": "Bijvoorbeeld: Korte beschrijving van de koppeling" - }, - "beschrijvingLang": { - "description": "Uitgebreide beschrijving van de koppeling", - "type": "string", - "format": "markdown", - "order": 6, - "title": "Beschrijving", - "example": "Bijvoorbeeld: Uitgebreide beschrijving van de koppeling" - }, - "type": { - "description": "Het type koppeling, bijvoorbeeld 'bestandsoverdracht', 'digikoppeling', of 'api'.", - "type": "string", - "order": 1, - "title": "Type", - "enum": [ - "n.v.t.", - "bestandsoverdracht", - "digikoppeling", - "message que", - "upload naar portaal", - "webservices", - "api" - ], - "example": "Bijvoorbeeld: api" - }, - "status": { - "description": "De status van de koppeling", - "type": "string", - "order": 3, - "title": "Status", - "enum": [ - "in ontwikkeling", - "in gebruik", - "einde ondersteuning", - "teruggetrokken" - ] - }, - "datumInOntwikkeling": { - "description": "Startdatum van de ontwikkelingsfase", - "type": "string", - "format": "date", - "order": 4, - "title": "Datum In Ontwikkeling", - "example": "Bijvoorbeeld: 2025-01-01" - }, - "datumInGebruik": { - "description": "Startdatum van gebruik", - "type": "string", - "format": "date", - "order": 5, - "title": "Datum In Gebruik" - }, - "datumEindeOndersteuning": { - "description": "Startdatum einde ondersteuning", - "type": "string", - "format": "date", - "order": 6, - "title": "Datum Einde Ondersteuning" - }, - "datumTeruggetrokken": { - "description": "Datum waarop de koppeling teruggetrokken is", - "type": "string", - "format": "date", - "order": 7, - "title": "Datum Teruggetrokken" - }, - "gegevensuitwisselingRichting": { - "description": "De richting van de gegevensuitwisseling", - "type": "string", - "order": 8, - "title": "Gegevensuitwisseling Richting", - "enum": [ - "AnaarB", - "BnaarA", - "bi-directioneel" - ] - }, - "moduleA": { - "description": "De module waarvan de gegevens uitgewisseld worden", - "type": "object", - "order": 9, - "title": "Module A", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/module", - "inversedBy": "koppeling" - }, - "moduleB": { - "description": "De module waarnaar de gegevens uitgewisseld worden", - "type": "object", - "order": 10, - "title": "Module B", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/module", - "inversedBy": "koppeling" - }, - "buitengemeentelijkVoorziening": { - "description": "Buitengemeentelijke voorziening waarmee gekoppeld wordt", - "type": "object", - "order": 11, - "title": "Buitengemeentelijke Voorziening", - "objectConfiguration": { - "handling": "related-object", - "queryParams": "gemmaType=Buitengemeentenlijke voorziening" - }, - "$ref": "#/components/schemas/element" - }, - "standaardversies": { - "description": "Standaardversies die door deze koppeling geïmplementeerd worden", - "type": "array", - "order": 12, - "title": "Standaard Versies", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object", - "queryParams": "gemmaType=standaardversie" - }, - "$ref": "#/components/schemas/element" - } - }, - "gerealiseerdMetIntermediairModule": { - "description": "Intermediaire module die wordt gebruikt voor de realisatie van deze koppeling", - "type": "object", - "order": 13, - "title": "Gerealiseerd Met Intermediair Module", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/module", - "inversedBy": "koppeling" - }, - "aanbieder": { - "description": "De aanbieder van deze koppeling", - "type": "object", - "visible": true, - "order": 14, - "facetable": false, - "title": "Aanbieder", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/organisatie" - }, - "dienst": { - "description": "De dienst die deze koppeling gebruikt", - "type": "object", - "visible": true, - "order": 15, - "facetable": false, - "title": "Dienst", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/dienst", - "inversedBy": "koppelingen" - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-08-08T07:11:40+00:00", - "created": "2025-08-08T07:11:40+00:00", - "maxDepth": 0, - "owner": "system", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": null, - "deleted": null, - "configuration": { - "objectNameField": "type", - "objectDescriptionField": "beschrijvingKort" - } - }, - "beoordeeling": { - "uri": null, - "slug": "beoordeeling", - "title": "Beoordeeling", - "description": "Schema voor beoordelingen en waarderingen van producten, modules en diensten", - "version": "0.0.12", - "summary": "", - "icon": "Star", - "required": [ - "naam", - "waardering", - "product" - ], - "properties": { - "naam": { - "description": "Naam van de beoordeling", - "type": "string", - "visible": true, - "required": true, - "facetable": false, - "title": "Naam", - "order": 1 - }, - "beschrijvingKort": { - "description": "Korte beschrijving van de beoordeling", - "type": "string", - "maxLength": 255, - "visible": true, - "facetable": false, - "title": "Samenvatting", - "order": 2 - }, - "beschrijvingLang": { - "description": "Uitgebreide beschrijving van de beoordeling", - "type": "string", - "format": "markdown", - "visible": true, - "facetable": false, - "title": "Beschrijving", - "order": 3 - }, - "waardering": { - "description": "Waardering van 1 tot en met 10", - "type": "integer", - "minimum": 1, - "maximum": 10, - "visible": true, - "required": true, - "facetable": true, - "title": "Waardering", - "order": 4 - }, - "product": { - "description": "Het product dat beoordeeld wordt", - "type": "object", - "visible": true, - "required": true, - "facetable": false, - "title": "Product", - "order": 5, - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/product" - }, - "modules": { - "description": "Optioneel: specifieke modules die beoordeeld worden", - "type": "array", - "visible": true, - "facetable": false, - "title": "Modules", - "order": 6, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/module", - "inversedBy": "beoordeeling" - } - }, - "diensten": { - "description": "Optioneel: specifieke diensten die beoordeeld worden", - "type": "array", - "visible": true, - "facetable": false, - "title": "Diensten", - "order": 7, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/dienst" - } - }, - "koppelingen": { - "description": "Optioneel: specifieke koppelingen die beoordeeld worden", - "type": "array", - "visible": true, - "facetable": false, - "title": "Koppelingen", - "order": 8, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/koppeling" - } - }, - "gebruik": { - "description": "Optioneel: het specifieke gebruik dat beoordeeld wordt", - "type": "object", - "visible": true, - "facetable": false, - "title": "Gebruik", - "order": 9, - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/gebruik" - } - }, - "archive": [], - "source": "internal", - "hardValidation": false, - "immutable": false, - "updated": "2025-05-12T20:01:42+00:00", - "created": "2025-05-12T19:58:51+00:00", - "maxDepth": 0, - "owner": "system", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": null, - "deleted": null, - "configuration": { - "objectNameField": "naam", - "objectSummaryField": "beschrijvingKort", - "objectDescriptionField": "beschrijvingLang" - } - }, - "element": { - "slug": "element", - "title": "Element", - "description": "AMEF Element - Architectuur elementen uit het ArchiMate model", - "version": "0.0.3", - "summary": "", - "icon": "Cube", - "required": [ - "identifier", - "type", - "properties" - ], - "properties": { - "identifier": { - "description": "De identifier van dit Element", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "id-009fa62f25844aa3a87d252bf2b6bb0c", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "type": { - "description": "Het type van dit Element", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Capability", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name": { - "description": "De naam van dit Element", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Publiceren en gebruiken van informatie over datadiensten", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name-lang": { - "description": "De name-language van dit Element", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation": { - "description": "De documentation van dit Element", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Dienstenafnemers moeten in online catalogi kunnen opvragen welke diensten, met welke kenmerken, door dienstenaanbieder worden aangeboden. \\nOnder andere ontwikkelaars hebben baat bij informatie over beschikbare diensten en de vereisten voor het gebruik van de dienst (bijv. specificatie van een dienst conform de OAS-standaard).", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation-lang": { - "description": "De documentation-language van dit Element", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "properties": { - "description": "De properties van dit Element", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Model_Property.json/19", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-04-01T12:16:33+00:00", - "created": "2025-03-03T13:56:42+00:00", - "maxDepth": 4, - "owner": null, - "application": null, - "organisation": null, - "authorization": null, - "deleted": null, - "configuration": { - "objectNameField": "name", - "objectSummaryField": "summary" - } - }, - "view": { - "slug": "view", - "title": "View", - "description": "AMEF View - Architectuur views en diagrammen uit het ArchiMate model", - "version": "0.0.3", - "summary": "", - "icon": "Eye", - "required": [ - "identifier", - "type", - "name", - "properties", - "nodes", - "connections" - ], - "properties": { - "identifier": { - "description": "De identifier van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "id-a6ee6077d3094afa91fc6ea92a9a2a40", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "type": { - "description": "De type van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Diagram", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "viewpoint": { - "description": "De viewpoint van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Application Structure", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name": { - "description": "De name van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "LV01 BGT basisregistratie en SVB view", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name-lang": { - "description": "De name-language van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation": { - "description": "De documentation van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Toont de referentiecomponenten ter ondersteuning van applicatieservices voor publieksdiensten", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation-lang": { - "description": "De documentation-language van deze View", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "properties": { - "description": "De properties van deze View", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Model_Property.json", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "nodes": { - "description": "De nodes van deze View", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View_Node.json", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "connections": { - "description": "De connections van deze View", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View_Connection.json", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-03-25T16:30:49+00:00", - "created": "2025-03-03T13:56:48+00:00", - "maxDepth": 0, - "owner": null, - "application": null, - "organisation": null, - "authorization": null, - "deleted": null, - "configuration": null - }, - "model": { - "slug": "model", - "title": "Model", - "description": "AMEF Model - Volledig ArchiMate model met alle elementen, relaties en views", - "version": "0.0.39", - "summary": "", - "icon": "Database", - "required": [ - "xmlns", - "xsi", - "schemaLocation", - "identifier", - "name", - "name-lang", - "version", - "documentation", - "documentation-lang", - "properties", - "elements", - "relationships", - "organizations", - "propertyDefinitions", - "views" - ], - "properties": { - "xmlns": { - "description": "De xmlns van dit GEMMA Model", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "http://www.opengroup.org/xsd/archimate/3.0/", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "xsi": { - "description": "De xsi van dit GEMMA Model", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "http://www.w3.org/2001/XMLSchema-instance", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "schemaLocation": { - "description": "De schemaLocation van dit GEMMA Model", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "http://www.opengroup.org/xsd/archimate/3.0/ http://www.opengroup.org/xsd/archimate/3.1/archimate3_Diagram.xsd", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "identifier": { - "description": "De identifier van dit GEMMA Model", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "id-b58b6b03-a59d-472b-bd87-88ba77ded4e6", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name": { - "description": "De name van dit GEMMA Model", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "GEMMA release (test)", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name-lang": { - "description": "De name-language van dit GEMMA Model", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "version": { - "description": "De version van dit GEMMA Model", - "type": "string", - "minLength": 3, - "maxLength": null, - "example": "3.0", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation": { - "description": "De documentation van dit GEMMA Model", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "De GEMeentelijk Model Architectuur (GEMMA) bevat een blauwdruk van de gemeente en haar informatievoorziening. De GEMMA kan worden gebruikt als basis voor de projectmodellen", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation-lang": { - "description": "De documentation-language van dit GEMMA Model", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "properties": { - "description": "De properties van dit GEMMA Model", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": 1, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Model_Property.json/19", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "elements": { - "description": "De elements van dit GEMMA Model", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": 1, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Element.json/10", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "relationships": { - "description": "De relationships van dit GEMMA Model", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": 1, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Relation.json/13", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "organizations": { - "description": "De organizations van dit GEMMA Model", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": 1, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "gebruiker", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "propertyDefinitions": { - "description": "De propertyDefinitions van dit GEMMA Model", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": 1, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Property_Definition.json/15", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "views": { - "description": "De views van dit GEMMA Model", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": 1, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View.json/11", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-04-01T15:12:59+00:00", - "created": "2025-03-03T13:57:16+00:00", - "maxDepth": 4, - "owner": null, - "application": null, - "organisation": null, - "authorization": null, - "deleted": null, - "configuration": null - }, - "organization": { - "slug": "organization", - "title": "Organization", - "description": "AMEF Organization - Organisatie structuren uit het ArchiMate model", - "version": "0.0.3", - "summary": "", - "icon": "Building", - "required": [], - "properties": { - "identifierRef": { - "description": "De identifierRef van deze Organization", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "id-009fa62f25844aa3a87d252bf2b6bb0c", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "label": { - "description": "De label van deze Organization", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Strategy", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "label-lang": { - "description": "De label-language van deze Organization", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation": { - "description": "De documentation van deze Organization", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Het Ondernemingsdossier stelt een ondernemer in staat om bepaalde informatie uit de bedrijfsvoering eenmalig vast te leggen en meerdere keren beschikbaar te stellen aan overheden zoals toezichthouders en vergunningverleners", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation-lang": { - "description": "De documentation-language van deze Organization", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "item": { - "description": "De items (Organizations) van deze Organization", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "gebruiker", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-04-03T09:28:37+00:00", - "created": "2025-03-03T13:56:55+00:00", - "maxDepth": 4, - "owner": null, - "application": null, - "organisation": null, - "authorization": null, - "deleted": null, - "configuration": null - }, - "property-definition": { - "slug": "property-definition", - "title": "Property Definition", - "description": "AMEF Property Definition - Definitie van eigenschappen voor ArchiMate elementen", - "version": "0.0.3", - "summary": "", - "icon": "Settings", - "required": [ - "identifier", - "type" - ], - "properties": { - "identifier": { - "description": "De identifier van deze Property Definition", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "propid-43", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "type": { - "description": "De type van deze Property Definition", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "string", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name": { - "description": "De name van deze Property Definition", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "API-portaal", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name-lang": { - "description": "De name-language van deze Property Definition", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-03-27T14:48:34+00:00", - "created": "2025-03-10T13:31:19+00:00", - "maxDepth": 0, - "owner": null, - "application": null, - "organisation": null, - "authorization": null, - "deleted": null, - "configuration": null - }, - "relation": { - "slug": "relation", - "title": "Relation", - "description": "AMEF Relation - Relaties tussen architectuur elementen uit het ArchiMate model", - "version": "0.0.3", - "summary": "", - "icon": "ArrowRight", - "required": [ - "identifier", - "source", - "target", - "type", - "properties" - ], - "properties": { - "identifier": { - "description": "De identifier van deze Relation", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "id-1b46181d68e5477a9c0b5a95a0677924", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "source": { - "description": "De source van deze Relation", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "id-d143a1fc-02dc-11e6-11ba-005056a85f9c", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "target": { - "description": "De target van deze Relation", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "id-a85f22d89af14222a914fcb9ecfe6815", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "type": { - "description": "De type van deze Relation", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Access", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "accessType": { - "description": "De accessType van deze Relation", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Read", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "isDirected": { - "description": "De isDirected van deze Relation", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "true", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name": { - "description": "De name van deze Relation", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Verplicht", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name-lang": { - "description": "De name-language van deze Relation", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation": { - "description": "De documentation van deze Relation", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Op basis van het zaaktype routeert de servicebuscomponent de aanvraag naar een Zaakafhandelcomponent (generiek of specifiek).", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation-lang": { - "description": "De documentation-lang van deze Relation", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "properties": { - "description": "De properties van deze Relation", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Model_Property.json/19", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-04-01T08:47:45+00:00", - "created": "2025-03-03T13:57:01+00:00", - "maxDepth": 4, - "owner": null, - "application": null, - "organisation": null, - "authorization": null, - "deleted": null, - "configuration": null - }, - "extendview": { - "slug": "extendview", - "title": "Extended View", - "description": "AMEF Extended View - Vooraf uitgebreide view kopie voor prestatie optimalisatie", - "version": "0.0.5", - "summary": "", - "icon": "EyePlus", - "required": [ - "identifier", - "type", - "name", - "properties", - "nodes", - "connections" - ], - "properties": { - "identifier": { - "description": "De identifier van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "id-a6ee6077d3094afa91fc6ea92a9a2a40", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "type": { - "description": "De type van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Diagram", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "viewpoint": { - "description": "De viewpoint van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Application Structure", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name": { - "description": "De name van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "LV01 BGT basisregistratie en SVB view", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name-lang": { - "description": "De name-language van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation": { - "description": "De documentation van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Toont de referentiecomponenten ter ondersteuning van applicatieservices voor publieksdiensten", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation-lang": { - "description": "De documentation-language van deze View", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "properties": { - "description": "De properties van deze View", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Model_Property.json", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "nodes": { - "description": "De nodes van deze View", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View_Node.json", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "connections": { - "description": "De connections van deze View", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View_Connection.json", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-05-15T10:54:50+00:00", - "created": "2025-05-13T19:49:15+00:00", - "maxDepth": 0, - "owner": null, - "application": null, - "organisation": null, - "authorization": null, - "deleted": null, - "configuration": null - }, - "module": { - "uri": null, - "slug": "module", - "title": "Module", - "description": "Een module is een onderdeel van een product (voorziening)", - "version": "0.0.4", - "summary": "", - "icon": "Package", - "required": [ - "naam", - "product", - "beschrijvingKort" - ], - "properties": { - "naam": { - "type": "string", - "description": "Naam van de module", - "title": "Naam", - "order": 1, - "facetable": false, - "required": true, - "maxLength": 200 - }, - "beschrijvingKort": { - "type": "string", - "description": "Korte beschrijving van de module", - "title": "Samenvatting", - "order": 2, - "facetable": false, - "maxLength": 255 - }, - "beschrijvingLang": { - "type": "string", - "description": "Uitgebreide beschrijving van de module", - "title": "Beschrijving", - "order": 3, - "facetable": false, - "format": "markdown", - "maxLength": 5000 - }, - "licentietype": { - "description": "Het type licentie van de voorziening (open source of closed source)", - "type": "string", - "visible": true, - "order": 9, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "default": "Closed source", - "enum": [ - "Closed source", - "Open source" - ], - "objectConfiguration": {}, - "fileConfiguration": {}, - "oneOf": [], - "facetable": false, - "title": "License Type" - }, - "licentie": { - "description": "De specifieke licentie van de voorziening, alleen opgeven indien Open Source", - "type": "string", - "visible": true, - "order": 10, - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "inversedBy": "", - "$ref": "", - "objectConfiguration": { - }, - "fileConfiguration": { - }, - "oneOf": [], - "facetable": false, - "enum": [ - "MIT License", - "GNU General Public License (GPL)", - "Apache License 2.0", - "BSD Licentie (Berkeley Software Distribution)", - "European Union Public Licence (EUPL), versie 1.2" - ], - "licentie": "License" - }, - "referentieComponenten": { - "description": "GEMMA referentiecomponenten die de module implementeert", - "type": "array", - "visible": true, - "order": 9, - "facetable": true, - "title": "Referentie Componenten", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object", - "queryParams": "gemmaType=referentiecomponent&_extend=aanbevolenStandaarden,verplichteStandaarden" - }, - "$ref": "#/components/schemas/element" - } - }, - "type": { - "description": "Het type module zoals geregistreerd in de catalogus", - "type": "string", - "visible": true, - "order": 11, - "facetable": false, - "title": "Type", - "default": "Applicatie", - "enum": [ - "Applicatie", - "Systeemsoftware" - ] - }, - "logo": { - "description": "URL naar het logo van de module", - "type": "string", - "format": "url", - "visible": true, - "order": 18, - "facetable": false, - "title": "Logo", - "maxLength": 500 - }, - "producten": { - "description": "De producten waarvan deze module onderdeel is", - "type": "array", - "visible": true, - "order": 19, - "facetable": false, - "title": "Producten", - "hideOnForm": true, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/product", - "inversedBy": "modules" - } - }, - "diensten": { - "description": "De diensten waarvan deze module onderdeel is", - "type": "array", - "visible": true, - "order": 20, - "facetable": false, - "title": "Diensten", - "hideOnForm": true, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/dienst", - "inversedBy": "modules" - } - }, - "koppelingen": { - "description": "De koppelingen waarbij deze module betrokken is", - "type": "array", - "visible": true, - "order": 21, - "facetable": false, - "title": "Koppelingen", - "hideOnForm": true, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/koppeling" - } - }, - "compliancy": { - "description": "De standaarden waar deze module aan voldoet (compliance registraties)", - "type": "array", - "visible": true, - "order": 22, - "facetable": false, - "title": "Compliance", - "hideOnForm": true, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/compliancy" - } - }, - "moduleVersies": { - "description": "De versies van deze module", - "type": "array", - "visible": true, - "order": 23, - "facetable": false, - "title": "Module Versies", - "hideOnForm": true, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/moduleVersie" - } - }, - "gebruiken": { - "description": "Het gebruik van deze module", - "type": "array", - "visible": true, - "order": 24, - "facetable": false, - "title": "Gebruik", - "hideOnForm": true, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/gebruik" - } - }, - "beoordelingen": { - "description": "De beoordelingen van deze module", - "type": "array", - "visible": true, - "order": 25, - "facetable": false, - "title": "Beoordelingen", - "hideOnForm": true, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/beoordeeling" - } - }, - "kwetsbaarheden": { - "description": "De kwetsbaarheden die deze module treffen", - "type": "array", - "visible": true, - "order": 26, - "facetable": false, - "title": "Kwetsbaarheden", - "hideOnForm": true, - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/kwetsbaarheid" - } - } - }, - "archive": [], - "source": "internal", - "hardValidation": false, - "immutable": false, - "updated": "2025-07-29T09:24:00+00:00", - "created": "2025-07-29T09:24:00+00:00", - "maxDepth": 0, - "owner": "system", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": null, - "deleted": null, - "configuration": { - "objectNameField": "naam", - "objectSummaryField": "beschrijvingKort", - "objectDescriptionField": "beschrijvingLang", - "objectImageField": "logo", - "allowFiles": true, - "allowedTags": [ - "Documentatie", - "Handleiding", - "Technische specificatie" - ] - } - }, - "compliancy": { - "uri": null, - "slug": "compliancy", - "title": "Compliancy", - "description": "Schema voor compliancy en standaard ondersteuning", - "version": "0.0.4", - "summary": "", - "icon": "CheckCircle", - "required": [], - "properties": { - "standaardversie": { - "description": "Standaardversie die door deze compliance wordt ondersteund", - "type": "object", - "order": 1, - "title": "Standaard Versie", - "visible": true, - "facetable": false, - "objectConfiguration": { - "handling": "related-object", - "queryParams": "gemmaType=standaardversie" - }, - "$ref": "#/components/schemas/element" - }, - "module": { - "description": "De module waarvan de compliance wordt geregistreerd", - "type": "object", - "order": 4, - "title": "Module", - "visible": true, - "facetable": false, - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/module", - "inversedBy": "compliancy" - }, - "bewijs": { - "description": "Bewijsstuk voor de compliance (bijvoorbeeld testrapport of certificaat)", - "type": "file", - "format": "base64", - "order": 5, - "title": "Bewijs", - "visible": true, - "facetable": false, - "fileConfiguration": { - "allowedMimeTypes": ["application/pdf", "image/jpeg", "image/png", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"], - "maxSize": 10485760 - } - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-08-08T07:11:40+00:00", - "created": "2025-08-08T07:11:40+00:00", - "maxDepth": 0, - "owner": "system", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": null, - "deleted": null, - "configuration": { - "objectNameField": "module", - "objectSummaryField": "standaardversie", - "objectDescriptionField": "module", - "allowFiles": true, - "allowedTags": [ - "testraport" - ] - } - }, - "moduleVersie": { - "uri": null, - "slug": "moduleVersie", - "title": "Module Versie", - "description": "Schema voor module versies", - "version": "0.0.4", - "summary": "", - "icon": "ViewModule", - "required": [], - "properties": { - "module": { - "description": "De module waarvan dit een versie is", - "type": "object", - "order": 1, - "title": "Module", - "facetable": false, - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/module", - "inversedBy": "moduleVersie" - }, - "versie": { - "description": "Versienummer in semantic versioning format (MAJOR.MINOR.PATCH)", - "type": "string", - "order": 2, - "title": "Versie", - "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", - "maxLength": 50, - "default": "1.0.0" - }, - "beschrijvingKort": { - "description": "Korte beschrijving van de module versie", - "type": "string", - "order": 3, - "title": "Samenvatting", - "maxLength": 255 - }, - "beschrijvingLang": { - "description": "Uitgebreide beschrijving van de module versie", - "type": "string", - "order": 4, - "title": "Beschrijving", - "format": "markdown", - "maxLength": 5000 - }, - "status": { - "description": "De status van de module", - "type": "string", - "order": 13, - "title": "Status", - "enum": [ - "in ontwikkeling", - "in gebruik", - "einde ondersteuning", - "teruggetrokken" - ], - "default": "in gebruik" - }, - "datumInOntwikkeling": { - "description": "Startdatum van de ontwikkelingsfase", - "type": "string", - "format": "date", - "order": 14, - "title": "Datum In Ontwikkeling" - }, - "datumInGebruik": { - "description": "Startdatum van gebruik", - "type": "string", - "format": "date", - "order": 15, - "title": "Datum In Gebruik" - }, - "datumEindeOndersteuning": { - "description": "Startdatum einde ondersteuning", - "type": "string", - "format": "date", - "order": 16, - "title": "Datum Einde Ondersteuning" - }, - "datumTeruggetrokken": { - "description": "Datum waarop de module teruggetrokken is", - "type": "string", - "format": "date", - "order": 17, - "title": "Datum Teruggetrokken" - }, - "gebruiken": { - "description": "Het gebruik van deze module versie", - "type": "array", - "visible": true, - "order": 18, - "facetable": false, - "title": "Gebruik", - "items": { - "type": "object", - "objectConfiguration": { - "handling": "related-object" - }, - "$ref": "#/components/schemas/gebruik" - } - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-08-08T07:11:40+00:00", - "created": "2025-08-08T07:11:40+00:00", - "maxDepth": 0, - "owner": "system", - "application": null, - "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", - "groups": null, - "authorization": null, - "deleted": null, - "configuration": { - "objectNameField": "versie", - "objectSummaryField": "beschrijvingKort", - "objectDescriptionField": "beschrijvingLang" - } + "archive": [], + "source": "", + "hardValidation": false, + "updated": "2025-08-08T07:11:40+00:00", + "created": "2025-08-08T07:11:40+00:00", + "maxDepth": 0, + "owner": "system", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": null, + "deleted": null, + "configuration": { + "objectNameField": "module", + "objectSummaryField": "standaardversie", + "objectDescriptionField": "module", + "allowFiles": true, + "allowedTags": ["testraport"] + } + }, + "moduleVersie": { + "uri": null, + "slug": "moduleVersie", + "title": "Module Versie", + "description": "Schema voor module versies", + "version": "0.0.4", + "summary": "", + "icon": "ViewModule", + "required": [], + "properties": { + "module": { + "description": "De module waarvan dit een versie is", + "type": "object", + "order": 1, + "title": "Module", + "facetable": false, + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/module", + "inversedBy": "moduleVersie" + }, + "versie": { + "description": "Versienummer in semantic versioning format (MAJOR.MINOR.PATCH)", + "type": "string", + "order": 2, + "title": "Versienummer", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "maxLength": 50, + "default": "1.0.0" + }, + "beschrijvingKort": { + "description": "Korte beschrijving van de module versie", + "type": "string", + "order": 3, + "title": "Wat is er nieuw of bijzonder in deze versie?", + "maxLength": 255 + }, + "beschrijvingLang": { + "description": "Uitgebreide beschrijving van de module versie", + "type": "string", + "order": 4, + "title": "Beschrijving", + "format": "markdown", + "maxLength": 5000 + }, + "status": { + "description": "De status van de module", + "type": "string", + "order": 13, + "title": "Status", + "enum": [ + "in ontwikkeling", + "in gebruik", + "einde ondersteuning", + "teruggetrokken" + ], + "default": "in gebruik" + }, + "datumInOntwikkeling": { + "description": "Startdatum van de ontwikkelingsfase", + "type": "string", + "format": "date", + "order": 14, + "title": "Datum In Ontwikkeling" + }, + "datumInGebruik": { + "description": "Startdatum van gebruik", + "type": "string", + "format": "date", + "order": 15, + "title": "Datum In Gebruik" + }, + "datumEindeOndersteuning": { + "description": "Startdatum einde ondersteuning", + "type": "string", + "format": "date", + "order": 16, + "title": "Datum Einde Ondersteuning" + }, + "datumTeruggetrokken": { + "description": "Datum waarop de module teruggetrokken is", + "type": "string", + "format": "date", + "order": 17, + "title": "Datum Teruggetrokken" + }, + "gebruiken": { + "description": "Het gebruik van deze module versie", + "type": "array", + "visible": true, + "order": 18, + "facetable": false, + "title": "Gebruik", + "items": { + "type": "object", + "objectConfiguration": { + "handling": "related-object" + }, + "$ref": "#/components/schemas/gebruik" } + } }, - "objects": [] + "archive": [], + "source": "", + "hardValidation": false, + "updated": "2025-08-08T07:11:40+00:00", + "created": "2025-08-08T07:11:40+00:00", + "maxDepth": 0, + "owner": "system", + "application": null, + "organisation": "cb2bca24-40bf-4568-a138-454c63ab761c", + "groups": null, + "authorization": null, + "deleted": null, + "configuration": { + "objectNameField": "versie", + "objectSummaryField": "beschrijvingKort", + "objectDescriptionField": "beschrijvingLang" + } + } }, - "openapi": "3.0.0", - "info": { - "id": 14, - "title": "Software Catalog Register", - "description": "Merged register containing both AMEF and Voorzieningen schemas and configurations", - "version": "1.0.1" - } -} \ No newline at end of file + "objects": [] + }, + "openapi": "3.0.0", + "info": { + "id": 14, + "title": "Software Catalog Register", + "description": "Merged register containing both AMEF and Voorzieningen schemas and configurations", + "version": "1.0.1" + } +} From 2da36d1cdbc2c6a5c36a642f4a64ecedd7d74831 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 28 Aug 2025 02:43:05 +0200 Subject: [PATCH 52/83] All of the fixes on all of the events --- appinfo/info.xml | 2 +- lib/AppInfo/Application.php | 23 + .../OpenRegisterEventsDebugListener.php | 368 ++++++++++ .../SoftwareCatalogEventListener.php | 147 +++- lib/EventListener/TestEventListener.php | 111 +++ lib/Service/ContactpersoonService.php | 65 +- lib/Service/OrganizationSyncService.php | 650 +++++++++++++++++- .../ContactPersonHandler.php | 187 +++-- src/utils/heartbeat.js | 1 + 9 files changed, 1441 insertions(+), 113 deletions(-) create mode 100644 lib/EventListener/OpenRegisterEventsDebugListener.php create mode 100644 lib/EventListener/TestEventListener.php diff --git a/appinfo/info.xml b/appinfo/info.xml index df3470cf..1f8d5476 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Submit a [bug report](https://github.com/OpenCatalogi/.github/issues/new/choose) Submit a [feature request](https://github.com/OpenCatalogi/.github/issues/new/choose). ]]> - 0.1.44 + 0.1.50 agpl organization Conduction diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 6c304ed1..87b30033 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -23,12 +23,22 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCA\SoftwareCatalog\EventListener\SoftwareCatalogEventListener; +use OCA\SoftwareCatalog\EventListener\TestEventListener; + use OCA\OpenRegister\Event\ObjectCreatedEvent; use OCA\OpenRegister\Event\ObjectUpdatedEvent; use OCA\OpenRegister\Event\ObjectDeletedEvent; use OCA\OpenRegister\Event\ObjectLockedEvent; use OCA\OpenRegister\Event\ObjectUnlockedEvent; use OCA\OpenRegister\Event\ObjectRevertedEvent; +use OCA\OpenRegister\Event\OrganisationCreatedEvent; +use OCA\OpenRegister\Event\RegisterCreatedEvent; +use OCA\OpenRegister\Event\RegisterDeletedEvent; +use OCA\OpenRegister\Event\RegisterUpdatedEvent; +use OCA\OpenRegister\Event\SchemaCreatedEvent; +use OCA\OpenRegister\Event\SchemaDeletedEvent; +use OCA\OpenRegister\Event\SchemaUpdatedEvent; +use OCP\User\Events\UserLoggedInEvent; use OCP\IConfig; use OCP\IDBConnection; use OCP\IUserManager; @@ -121,6 +131,11 @@ public function register(IRegistrationContext $context): void ); }); + + + // Register TEST event listener for easily triggerable Nextcloud events + $context->registerEventListener(UserLoggedInEvent::class, TestEventListener::class); + // Register event listeners for OpenRegister events $context->registerEventListener(ObjectCreatedEvent::class, SoftwareCatalogEventListener::class); $context->registerEventListener(ObjectUpdatedEvent::class, SoftwareCatalogEventListener::class); @@ -129,6 +144,8 @@ public function register(IRegistrationContext $context): void $context->registerEventListener(ObjectUnlockedEvent::class, SoftwareCatalogEventListener::class); $context->registerEventListener(ObjectRevertedEvent::class, SoftwareCatalogEventListener::class); + + // Organization event listeners removed - now using cron job for organization synchronization // Contact person event listeners are still active for real-time processing @@ -192,6 +209,8 @@ public function register(IRegistrationContext $context): void ); }); + // Event listener uses direct service access like OpenCatalogi - no service registration needed + // Register ArchiMate import service $context->registerService(\OCA\SoftwareCatalog\Service\ArchiMateImportService::class, function ($container) { return new \OCA\SoftwareCatalog\Service\ArchiMateImportService( @@ -370,5 +389,9 @@ public function boot(IBootContext $context): void 'exception' => $e->getMessage() ]); } + + } + + } diff --git a/lib/EventListener/OpenRegisterEventsDebugListener.php b/lib/EventListener/OpenRegisterEventsDebugListener.php new file mode 100644 index 00000000..00d7f9c9 --- /dev/null +++ b/lib/EventListener/OpenRegisterEventsDebugListener.php @@ -0,0 +1,368 @@ + + * @copyright 2024 Conduction B.V. + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * + * @version GIT: + * + * @link https://SoftwareCatalog.app + */ + +declare(strict_types=1); + +namespace OCA\SoftwareCatalog\EventListener; + +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCA\OpenRegister\Event\ObjectCreatedEvent; +use OCA\OpenRegister\Event\ObjectDeletedEvent; +use OCA\OpenRegister\Event\ObjectLockedEvent; +use OCA\OpenRegister\Event\ObjectRevertedEvent; +use OCA\OpenRegister\Event\ObjectUnlockedEvent; +use OCA\OpenRegister\Event\ObjectUpdatedEvent; +use OCA\OpenRegister\Event\OrganisationCreatedEvent; +use OCA\OpenRegister\Event\RegisterCreatedEvent; +use OCA\OpenRegister\Event\RegisterDeletedEvent; +use OCA\OpenRegister\Event\RegisterUpdatedEvent; +use OCA\OpenRegister\Event\SchemaCreatedEvent; +use OCA\OpenRegister\Event\SchemaDeletedEvent; +use OCA\OpenRegister\Event\SchemaUpdatedEvent; +use Psr\Log\LoggerInterface; + +/** + * Debug event listener for all OpenRegister events in SoftwareCatalog + * + * This listener provides comprehensive debugging information for all OpenRegister events + * received by the SoftwareCatalog app. It logs event details at info level and can be + * easily enabled/disabled. + * + * @template T of Event + * + * @implements IEventListener + */ +class OpenRegisterEventsDebugListener implements IEventListener +{ + + /** + * Logger instance for debug logging + * + * @var LoggerInterface + */ + private readonly LoggerInterface $logger; + + /** + * Whether debug logging is enabled + * + * @var bool + */ + private readonly bool $debugEnabled; + + + /** + * Constructor for the debug listener + * + * @param LoggerInterface $logger Logger instance for debug output + * @param bool $debugEnabled Whether debug logging should be enabled + * + * @return void + */ + public function __construct( + LoggerInterface $logger, + bool $debugEnabled = true + ) { + $this->logger = $logger; + $this->debugEnabled = $debugEnabled; + + }//end __construct() + + + /** + * Handle any OpenRegister event for debugging purposes + * + * This method processes all OpenRegister events and logs detailed debug information + * including event type, object details, and any relevant metadata. + * + * @param Event $event The event to handle + * + * @return void + * + * @phpstan-param T $event + */ + public function handle(Event $event): void + { + // CRITICAL: Always log regardless of debug flag to ensure we see if it's called + $eventClass = get_class($event); + $eventType = $this->getEventTypeName($eventClass); + + $this->logger->critical('🔍 SOFTWARECATALOG: OPENREGISTER DEBUG LISTENER TRIGGERED!', [ + 'app' => 'softwarecatalog', + 'eventType' => $eventType, + 'eventClass' => $eventClass, + 'listenerClass' => self::class, + 'debugEnabled' => $this->debugEnabled, + 'timestamp' => date('Y-m-d H:i:s'), + 'microtime' => microtime(true), + 'source' => 'OpenRegister', + ]); + + // Also use error_log for immediate stdout visibility + error_log('🔍 SOFTWARECATALOG_OPENREGISTER_DEBUG_LISTENER: ' . $eventType . ' (' . $eventClass . ') at ' . date('Y-m-d H:i:s')); + + if (!$this->debugEnabled) { + $this->logger->warning('SoftwareCatalog OpenRegister Debug: Debug disabled, skipping detailed logging'); + error_log('SOFTWARECATALOG_DEBUG_DISABLED: Debug logging disabled for ' . $eventType); + return; + } + + $eventData = $this->extractEventData($event); + + // Log comprehensive debug information + $this->logger->info( + '[SoftwareCatalog] 🔍 OPENREGISTER EVENT: {eventType} received from OpenRegister', + [ + 'app' => 'softwarecatalog', + 'eventType' => $eventType, + 'eventClass' => $eventClass, + 'listenerClass' => self::class, + 'eventData' => $eventData, + 'timestamp' => date('Y-m-d H:i:s'), + 'source' => 'OpenRegister', + ] + ); + + }//end handle() + + + /** + * Extract a human-readable event type name from the class name + * + * @param string $eventClass The full event class name + * + * @return string The simplified event type name + * + * @phpstan-return string + * @psalm-return string + */ + private function getEventTypeName(string $eventClass): string + { + // Extract the class name without namespace + $className = substr($eventClass, strrpos($eventClass, '\\') + 1); + + // Remove 'Event' suffix if present + if (str_ends_with($className, 'Event')) { + $className = substr($className, 0, -5); + } + + return $className; + + }//end getEventTypeName() + + + /** + * Extract relevant data from the event for debugging + * + * This method extracts useful information from different event types + * to provide comprehensive debug logging. + * + * @param Event $event The event to extract data from + * + * @return array Array of extracted event data + * + * @phpstan-return array + * @psalm-return array + */ + private function extractEventData(Event $event): array + { + $data = [ + 'eventClass' => get_class($event), + ]; + + // Handle Object events + if ($event instanceof ObjectCreatedEvent) { + $object = $event->getObject(); + $data = array_merge($data, [ + 'eventType' => 'ObjectCreated', + 'objectId' => $object->getId(), + 'objectUuid' => $object->getUuid(), + 'registerId' => $object->getRegister(), + 'schemaId' => $object->getSchema(), + 'owner' => $object->getOwner(), + 'created' => $object->getCreated()?->format('Y-m-d H:i:s'), + 'objectData' => $this->getSafeObjectData($object->getObject()), + ]); + } else if ($event instanceof ObjectUpdatedEvent) { + $newObject = $event->getNewObject(); + $oldObject = $event->getOldObject(); + $data = array_merge($data, [ + 'eventType' => 'ObjectUpdated', + 'newObjectId' => $newObject->getId(), + 'newObjectUuid' => $newObject->getUuid(), + 'oldObjectId' => $oldObject?->getId(), + 'oldObjectUuid' => $oldObject?->getUuid(), + 'registerId' => $newObject->getRegister(), + 'schemaId' => $newObject->getSchema(), + 'owner' => $newObject->getOwner(), + 'updated' => $newObject->getUpdated()?->format('Y-m-d H:i:s'), + 'newObjectData' => $this->getSafeObjectData($newObject->getObject()), + 'oldObjectData' => $oldObject ? $this->getSafeObjectData($oldObject->getObject()) : null, + ]); + } else if ($event instanceof ObjectDeletedEvent) { + $object = $event->getObject(); + $data = array_merge($data, [ + 'eventType' => 'ObjectDeleted', + 'objectId' => $object->getId(), + 'objectUuid' => $object->getUuid(), + 'registerId' => $object->getRegister(), + 'schemaId' => $object->getSchema(), + 'owner' => $object->getOwner(), + 'objectData' => $this->getSafeObjectData($object->getObject()), + ]); + } else if ($event instanceof ObjectLockedEvent) { + $object = $event->getObject(); + $data = array_merge($data, [ + 'eventType' => 'ObjectLocked', + 'objectId' => $object->getId(), + 'objectUuid' => $object->getUuid(), + 'registerId' => $object->getRegister(), + 'schemaId' => $object->getSchema(), + 'lockedBy' => $object->getLockedBy(), + 'lockedAt' => $object->getLockedAt()?->format('Y-m-d H:i:s'), + ]); + } else if ($event instanceof ObjectUnlockedEvent) { + $object = $event->getObject(); + $data = array_merge($data, [ + 'eventType' => 'ObjectUnlocked', + 'objectId' => $object->getId(), + 'objectUuid' => $object->getUuid(), + 'registerId' => $object->getRegister(), + 'schemaId' => $object->getSchema(), + ]); + } else if ($event instanceof ObjectRevertedEvent) { + $object = $event->getObject(); + $data = array_merge($data, [ + 'eventType' => 'ObjectReverted', + 'objectId' => $object->getId(), + 'objectUuid' => $object->getUuid(), + 'registerId' => $object->getRegister(), + 'schemaId' => $object->getSchema(), + 'revertedTo' => $event->getRevertedToVersion(), + ]); + } + + // Handle Register events + else if ($event instanceof RegisterCreatedEvent) { + $register = $event->getRegister(); + $data = array_merge($data, [ + 'eventType' => 'RegisterCreated', + 'registerId' => $register->getId(), + 'registerTitle' => $register->getTitle(), + 'registerSlug' => $register->getSlug(), + ]); + } else if ($event instanceof RegisterUpdatedEvent) { + $register = $event->getRegister(); + $data = array_merge($data, [ + 'eventType' => 'RegisterUpdated', + 'registerId' => $register->getId(), + 'registerTitle' => $register->getTitle(), + 'registerSlug' => $register->getSlug(), + ]); + } else if ($event instanceof RegisterDeletedEvent) { + $register = $event->getRegister(); + $data = array_merge($data, [ + 'eventType' => 'RegisterDeleted', + 'registerId' => $register->getId(), + 'registerTitle' => $register->getTitle(), + 'registerSlug' => $register->getSlug(), + ]); + } + + // Handle Schema events + else if ($event instanceof SchemaCreatedEvent) { + $schema = $event->getSchema(); + $data = array_merge($data, [ + 'eventType' => 'SchemaCreated', + 'schemaId' => $schema->getId(), + 'schemaTitle' => $schema->getTitle(), + 'schemaVersion' => $schema->getVersion(), + ]); + } else if ($event instanceof SchemaUpdatedEvent) { + $schema = $event->getSchema(); + $data = array_merge($data, [ + 'eventType' => 'SchemaUpdated', + 'schemaId' => $schema->getId(), + 'schemaTitle' => $schema->getTitle(), + 'schemaVersion' => $schema->getVersion(), + ]); + } else if ($event instanceof SchemaDeletedEvent) { + $schema = $event->getSchema(); + $data = array_merge($data, [ + 'eventType' => 'SchemaDeleted', + 'schemaId' => $schema->getId(), + 'schemaTitle' => $schema->getTitle(), + 'schemaVersion' => $schema->getVersion(), + ]); + } + + // Handle Organisation events + else if ($event instanceof OrganisationCreatedEvent) { + $organisation = $event->getOrganisation(); + $data = array_merge($data, [ + 'eventType' => 'OrganisationCreated', + 'organisationId' => $organisation->getId(), + 'organisationTitle' => $organisation->getTitle(), + ]); + } + + // Unknown event type + else { + $data['eventType'] = 'Unknown'; + $data['note'] = 'Event type not specifically handled by SoftwareCatalog debug listener'; + } + + return $data; + + }//end extractEventData() + + + /** + * Get safe object data for logging (truncated if too large) + * + * @param mixed $objectData The object data to make safe for logging + * + * @return mixed The safe object data + * + * @phpstan-return mixed + * @psalm-return mixed + */ + private function getSafeObjectData(mixed $objectData): mixed + { + // Convert to JSON string to check size + $jsonData = json_encode($objectData); + + // If the data is too large (>2KB), truncate it + if (strlen($jsonData) > 2048) { + return [ + '_truncated' => true, + '_originalSize' => strlen($jsonData), + '_preview' => substr($jsonData, 0, 500) . '...', + '_note' => 'Object data truncated for logging - too large to display fully' + ]; + } + + return $objectData; + + }//end getSafeObjectData() + + +}//end class diff --git a/lib/EventListener/SoftwareCatalogEventListener.php b/lib/EventListener/SoftwareCatalogEventListener.php index be1d0ede..44232914 100644 --- a/lib/EventListener/SoftwareCatalogEventListener.php +++ b/lib/EventListener/SoftwareCatalogEventListener.php @@ -66,21 +66,45 @@ public function __construct() { */ public function handle(Event $event): void { - // All processing is now handled by the cron-based OrganizationSyncService - // This prevents race conditions, infinite loops, and ensures consistent processing - try { $logger = \OC::$server->get(LoggerInterface::class); - $logger->debug('SoftwareCatalog: Event processing disabled - using cron-based sync', [ + $contactpersoonService = \OC::$server->get(ContactpersoonService::class); + $settingsService = \OC::$server->get(SettingsService::class); + + $logger->debug('SoftwareCatalog: Processing event', [ 'eventType' => get_class($event), - 'message' => 'All processing is handled by OrganizationSyncService cron job to avoid race conditions' + 'timestamp' => date('Y-m-d H:i:s') ]); + + if ($event instanceof ObjectCreatedEvent) { + $this->handleObjectCreated($event, $contactpersoonService, $settingsService, $logger); + } elseif ($event instanceof ObjectUpdatedEvent) { + $this->handleObjectUpdated($event, $contactpersoonService, $settingsService, $logger); + } elseif ($event instanceof ObjectDeletedEvent) { + $this->handleObjectDeleted($event, $contactpersoonService, $settingsService, $logger); + } elseif ($event instanceof ObjectLockedEvent || $event instanceof ObjectUnlockedEvent || $event instanceof ObjectRevertedEvent) { + $logger->debug('SoftwareCatalog: Ignoring object lifecycle event', [ + 'eventType' => get_class($event) + ]); + } else { + $logger->debug('SoftwareCatalog: Unknown event type ignored', [ + 'eventType' => get_class($event) + ]); + } } catch (\Exception $e) { - // Silently fail - logging is not critical + try { + $logger = \OC::$server->get(LoggerInterface::class); + $logger->error('SoftwareCatalog: Error in event handler', [ + 'eventType' => get_class($event), + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString() + ]); + } catch (\Exception $logException) { + // Silently fail if logging fails - better than breaking the event system + } } - - // Early return - no processing - return; } @@ -135,9 +159,41 @@ private function handleObjectCreated(ObjectCreatedEvent $event, ContactpersoonSe ] ); - // Organization processing is now handled by cron job - skip organization events + // Check if this is an organization object if ($organisatieSchemaId && $objectSchemaIdInt === (int) $organisatieSchemaId) { - $logger->debug('SoftwareCatalog: Skipping organization creation - handled by cron job', ['objectId' => $objectId]); + $objectData = $object->getObject(); + $status = strtolower($objectData['status'] ?? ''); + + // Only process active organizations + if (in_array($status, ['actief', 'active'])) { + $logger->info('SoftwareCatalog: Processing active organization creation', [ + 'objectId' => $objectId, + 'status' => $status + ]); + + try { + // Process organization with OrganizationSyncService + $organizationSyncService = \OC::$server->get('OCA\SoftwareCatalog\Service\OrganizationSyncService'); + $result = $organizationSyncService->processSpecificOrganization($object); + + $logger->info('SoftwareCatalog: Successfully processed organization creation', [ + 'objectId' => $objectId, + 'processResult' => $result + ]); + } catch (\Exception $e) { + $logger->error('SoftwareCatalog: Failed to process organization creation', [ + 'objectId' => $objectId, + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + } else { + $logger->debug('SoftwareCatalog: Skipping non-active organization creation', [ + 'objectId' => $objectId, + 'status' => $status + ]); + } return; } @@ -210,19 +266,46 @@ private function handleObjectUpdated(ObjectUpdatedEvent $event, ContactpersoonSe ] ); - // Organization updates are now handled by cron job - skip organization events + // Check if this is an organization update $organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie'); $organisatieSchemaIdInt = (int) $organisatieSchemaId; if ($organisatieSchemaId && $objectSchemaIdInt === $organisatieSchemaIdInt) { - $logger->info( - 'SoftwareCatalog: Skipping organisatie update - handled by cron job', - [ + $objectData = $object->getObject(); + $status = strtolower($objectData['status'] ?? ''); + + // Only process active organizations + if (in_array($status, ['actief', 'active'])) { + $logger->info('SoftwareCatalog: Processing active organization update', [ 'objectId' => $objectId, - 'schemaId' => $objectSchemaId, - 'configuredSchemaId' => $organisatieSchemaId - ] - ); + 'status' => $status, + 'schemaId' => $objectSchemaId + ]); + + try { + // Process organization with OrganizationSyncService + $organizationSyncService = \OC::$server->get('OCA\SoftwareCatalog\Service\OrganizationSyncService'); + $result = $organizationSyncService->processSpecificOrganization($object); + + $logger->info('SoftwareCatalog: Successfully processed organization update', [ + 'objectId' => $objectId, + 'processResult' => $result + ]); + } catch (\Exception $e) { + $logger->error('SoftwareCatalog: Failed to process organization update', [ + 'objectId' => $objectId, + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + } else { + $logger->debug('SoftwareCatalog: Skipping non-active organization update', [ + 'objectId' => $objectId, + 'status' => $status, + 'schemaId' => $objectSchemaId + ]); + } return; } @@ -354,13 +437,35 @@ private function handleObjectDeleted(ObjectDeletedEvent $event, ContactpersoonSe ] ); - // Organization deletion is now handled by cron job - skip organization events + // Check if this is an organization deletion $organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie'); $organisatieSchemaIdInt = (int) $organisatieSchemaId; $objectSchemaIdInt = (int) $objectSchemaId; if ($organisatieSchemaId && $objectSchemaIdInt === $organisatieSchemaIdInt) { - $logger->info('SoftwareCatalog: Skipping organization deletion - handled by cron job', ['objectId' => $objectId]); + $logger->info('SoftwareCatalog: Processing organization deletion', ['objectId' => $objectId]); + + try { + // For deletions, we may need to handle cleanup regardless of status + // The OrganizationSyncService can determine what cleanup is needed + $organizationSyncService = \OC::$server->get('OCA\SoftwareCatalog\Service\OrganizationSyncService'); + + // Note: processSpecificOrganization may handle cleanup for deleted organizations + // The service can check if the organization exists and handle accordingly + $result = $organizationSyncService->processSpecificOrganization($object); + + $logger->info('SoftwareCatalog: Successfully processed organization deletion', [ + 'objectId' => $objectId, + 'processResult' => $result + ]); + } catch (\Exception $e) { + $logger->error('SoftwareCatalog: Failed to process organization deletion', [ + 'objectId' => $objectId, + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } return; } diff --git a/lib/EventListener/TestEventListener.php b/lib/EventListener/TestEventListener.php new file mode 100644 index 00000000..8421240f --- /dev/null +++ b/lib/EventListener/TestEventListener.php @@ -0,0 +1,111 @@ + + * @copyright 2024 Conduction B.V. + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @version 1.0.0 + * @link https://github.com/ConductionNL/OpenConnector + */ + +declare(strict_types=1); + +namespace OCA\SoftwareCatalog\EventListener; + +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\User\Events\UserLoggedInEvent; +use Psr\Log\LoggerInterface; + +/** + * Test event listener for verifying event listener functionality. + * + * This listener handles user login events to test that our event system + * is working correctly. It logs when users log in and can be easily + * triggered for testing purposes. + * + * @category EventListener + * @package OCA\SoftwareCatalog\EventListener + * @author Conduction b.v. + * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html + * @version 1.0.0 + * @link https://github.com/ConductionNL/OpenConnector + */ +class TestEventListener implements IEventListener +{ + /** + * Constructor for TestEventListener + * + * @param LoggerInterface $logger The logger service for logging events + */ + public function __construct( + private readonly LoggerInterface $logger + ) { + } + + /** + * Handles events related to user login for testing purposes + * + * This method processes UserLoggedInEvent events and logs detailed + * information to verify that the event listener system is working + * correctly. + * + * @param Event $event The event to handle + * + * @return void + */ + public function handle(Event $event): void + { + // Log that we received ANY event first + $this->logger->info('SoftwareCatalog TestEventListener: Event received!', [ + 'eventClass' => get_class($event), + 'timestamp' => date('Y-m-d H:i:s'), + 'microtime' => microtime(true) + ]); + + // Also use error_log for immediate visibility in system logs + error_log('SOFTWARECATALOG_TEST_LISTENER_TRIGGERED: ' . get_class($event) . ' at ' . date('Y-m-d_H:i:s')); + + // Handle UserLoggedInEvent specifically + if ($event instanceof UserLoggedInEvent) { + $user = $event->getUser(); + + $this->logger->info('SoftwareCatalog TestEventListener: User logged in successfully!', [ + 'userId' => $user->getUID(), + 'userDisplayName' => $user->getDisplayName(), + 'userEmail' => $user->getEMailAddress(), + 'timestamp' => date('Y-m-d H:i:s'), + 'eventType' => 'UserLoggedInEvent' + ]); + + // Additional error_log for easy debugging + error_log('SOFTWARECATALOG_USER_LOGIN: User ' . $user->getUID() . ' (' . $user->getDisplayName() . ') logged in at ' . date('Y-m-d_H:i:s')); + + // Test that we can access Nextcloud services + try { + $this->logger->debug('SoftwareCatalog TestEventListener: Event listener is working correctly!', [ + 'message' => 'This confirms that event listeners are properly registered and triggered', + 'userId' => $user->getUID(), + 'eventClass' => get_class($event) + ]); + } catch (\Exception $e) { + $this->logger->error('SoftwareCatalog TestEventListener: Error in event processing', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + } + } else { + // Log other events we might receive + $this->logger->debug('SoftwareCatalog TestEventListener: Received unhandled event', [ + 'eventClass' => get_class($event), + 'timestamp' => date('Y-m-d H:i:s') + ]); + } + } +} diff --git a/lib/Service/ContactpersoonService.php b/lib/Service/ContactpersoonService.php index 137fe040..89548a11 100644 --- a/lib/Service/ContactpersoonService.php +++ b/lib/Service/ContactpersoonService.php @@ -108,21 +108,58 @@ public function processContactpersoon(object $contactpersoonObject, bool $isUpda $user = $userManager->get($username); if (!$user) { - // Create user account - $this->logger->info('ContactpersoonService: Creating user account for contactpersoon', [ - 'contactId' => $contactId, - 'username' => $username - ]); - - $success = $this->contactPersonHandler->createUserAccount($contactpersoonObject); - if (!$success) { - throw new \Exception('Failed to create user account'); + // Check if organization is active before creating user account + $organizationUuid = $contactData['organisation'] ?? $contactData['organisatie'] ?? ''; + + if (!empty($organizationUuid)) { + try { + $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); + $organisationEntity = $organisationMapper->findByUuid($organizationUuid); + + if ($organisationEntity && $organisationEntity->getActive()) { + // Create user account - organization is active + $this->logger->info('ContactpersoonService: Creating user account for contactpersoon (org is active)', [ + 'contactId' => $contactId, + 'username' => $username, + 'organizationUuid' => $organizationUuid, + 'organizationActive' => true + ]); + + $success = $this->contactPersonHandler->createUserAccount($contactpersoonObject); + if (!$success) { + throw new \Exception('Failed to create user account'); + } + + // Link user to organization entity + $this->contactPersonHandler->addUserToOrganizationEntity($contactpersoonObject, $username); + + $this->logger->info('ContactpersoonService: Successfully created user account', [ + 'contactId' => $contactId, + 'username' => $username + ]); + } else { + $this->logger->info('ContactpersoonService: Skipping user creation - organization not active or not found', [ + 'contactId' => $contactId, + 'organizationUuid' => $organizationUuid, + 'organizationFound' => $organisationEntity !== null, + 'organizationActive' => $organisationEntity ? $organisationEntity->getActive() : false + ]); + return false; + } + } catch (\Exception $e) { + $this->logger->info('ContactpersoonService: Skipping user creation - organization not found in entity table (not active)', [ + 'contactId' => $contactId, + 'organizationUuid' => $organizationUuid, + 'reason' => 'Organization not found in entity table' + ]); + return false; + } + } else { + $this->logger->warning('ContactpersoonService: Contactpersoon has no organization reference, skipping user creation', [ + 'contactId' => $contactId + ]); + return false; } - - $this->logger->info('ContactpersoonService: Successfully created user account', [ - 'contactId' => $contactId, - 'username' => $username - ]); } else { $this->logger->info('ContactpersoonService: User account already exists', [ 'contactId' => $contactId, diff --git a/lib/Service/OrganizationSyncService.php b/lib/Service/OrganizationSyncService.php index a9ebaaef..8c683aab 100644 --- a/lib/Service/OrganizationSyncService.php +++ b/lib/Service/OrganizationSyncService.php @@ -146,10 +146,10 @@ public function performOrganizationsSync(int $batchSize = 50, int $maxExecutionS ->where($qb->expr()->eq('o.schema', $qb->createNamedParameter($organizationSchema))) ->andWhere($qb->expr()->eq('o.register', $qb->createNamedParameter($register))) ->andWhere($qb->expr()->orX( - $qb->expr()->neq('o2.active', $qb->createFunction('(json_unquote(json_extract(o.object, \'$.status\')) = \'actief\')')), + $qb->expr()->neq('o2.active', $qb->createFunction('(LOWER(json_unquote(json_extract(o.object, \'$.status\'))) = \'actief\')')), $qb->expr()->isNull('o2.uuid') )) - ->andWhere($qb->expr()->neq($qb->createFunction('json_unquote(json_extract(o.object, \'$.status\'))'), $qb->createNamedParameter('concept'))) + ->andWhere($qb->expr()->neq($qb->createFunction('LOWER(json_unquote(json_extract(o.object, \'$.status\')))'), $qb->createNamedParameter('concept'))) ->orderBy('o.updated', 'ASC') // Process oldest first for consistency ->setMaxResults($batchSize); // Limit batch size @@ -165,7 +165,8 @@ public function performOrganizationsSync(int $batchSize = 50, int $maxExecutionS $this->logger->info('OrganizationSyncService: Execution time limit reached', [ 'processedCount' => $stats['organizationsProcessed'], 'executionTime' => time() - $startTime, - 'maxExecutionSeconds' => $maxExecutionSeconds + 'maxExecutionSeconds' => $maxExecutionSeconds, + 'trigger' => 'batch_processing' ]); break; } @@ -246,7 +247,8 @@ public function performContactSync(int $batchSize = 100, int $maxExecutionSecond $this->logger->info('OrganizationSyncService: Contact sync time limit reached', [ 'contactsProcessed' => $stats['contactPersonsProcessed'], 'executionTime' => time() - $startTime, - 'maxExecutionSeconds' => $maxExecutionSeconds + 'maxExecutionSeconds' => $maxExecutionSeconds, + 'trigger' => 'batch_processing' ]); break; } @@ -529,6 +531,14 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta $objectData = $organisatieObject->getObject(); $organisatieId = $objectData['id'] ?? $organisatieObject->getId(); + $this->logger->critical('🔍 ENSURING ORGANISATION ENTITY', [ + 'app' => 'softwarecatalog', + 'organisatieId' => $organisatieId, + 'naam' => $objectData['naam'] ?? 'Unknown', + 'status' => $objectData['status'] ?? 'Unknown' + ]); + error_log('🔍 ENSURING_ORG_ENTITY: ' . $organisatieId); + // Try to find existing organisation entity $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); @@ -539,75 +549,105 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta $status = strtolower($objectData['status'] ?? 'actief'); $shouldBeActive = in_array($status, ['actief', 'active']); + $this->logger->critical('📋 EXISTING ENTITY FOUND', [ + 'app' => 'softwarecatalog', + 'organisatieId' => $organisatieId, + 'entityId' => $organisationEntity->getId(), + 'currentActive' => $organisationEntity->getActive(), + 'shouldBeActive' => $shouldBeActive, + 'needsUpdate' => $organisationEntity->getActive() !== $shouldBeActive + ]); + error_log('📋 EXISTING_ENTITY: ' . $organisatieId . ' -> Entity ID: ' . $organisationEntity->getId() . ' (Active: ' . ($organisationEntity->getActive() ? 'true' : 'false') . ')'); + if ($organisationEntity->getActive() !== $shouldBeActive) { - $this->logger->info('OrganizationSyncService: Updating organisation entity status', [ + $this->logger->critical('🔄 UPDATING ENTITY STATUS', [ + 'app' => 'softwarecatalog', 'organisatieId' => $organisatieId, 'oldActive' => $organisationEntity->getActive(), 'newActive' => $shouldBeActive ]); + error_log('🔄 UPDATING_ENTITY_STATUS: ' . $organisatieId . ' from ' . ($organisationEntity->getActive() ? 'active' : 'inactive') . ' to ' . ($shouldBeActive ? 'active' : 'inactive')); - $isActive = $organisationEntity->getActive(); - + $wasActive = $organisationEntity->getActive(); $organisationEntity->setActive($shouldBeActive); $organisationMapper->save($organisationEntity); $stats['entitiesUpdated']++; // Send activation email if organization became active - if ($shouldBeActive && !$isActive) { + if ($shouldBeActive && !$wasActive) { + $this->logger->info('[FLOW] Sending organization activation email', [ + 'organisatieId' => $organisatieId + ]); $emailSent = $this->sendOrganizationActivationEmail($objectData); if ($emailSent) { - $this->logger->info('OrganizationSyncService: Organization activation email sent successfully', [ + $this->logger->info('📧 Organization activation email sent successfully', [ 'organisatieId' => $organisatieId ]); } else { - $this->logger->info('OrganizationSyncService: Organization activation email not sent (disabled or not configured)', [ + $this->logger->info('📧 Organization activation email not sent (disabled or not configured)', [ 'organisatieId' => $organisatieId ]); } } } - $this->logger->debug('OrganizationSyncService: Found existing organisation entity', [ - 'organisatieId' => $organisatieId, - 'entityId' => $organisationEntity->getId(), - 'active' => $organisationEntity->getActive() - ]); return $organisationEntity; + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { // Entity doesn't exist, create it - $this->logger->info('OrganizationSyncService: Creating new organisation entity', [ - 'organisatieId' => $organisatieId + $this->logger->critical('🆕 CREATING NEW ORGANISATION ENTITY', [ + 'app' => 'softwarecatalog', + 'organisatieId' => $organisatieId, + 'naam' => $objectData['naam'] ?? 'Unknown' ]); + error_log('🆕 CREATING_NEW_ENTITY: ' . $organisatieId . ' (' . ($objectData['naam'] ?? 'Unknown') . ')'); $organisationEntity = $this->organisatieService->createOrganisationInOpenRegister($objectData); if ($organisationEntity) { $stats['entitiesCreated']++; - $this->logger->info('OrganizationSyncService: Successfully created organisation entity', [ + $this->logger->critical('🎊 NEW ORGANISATION ENTITY CREATED', [ + 'app' => 'softwarecatalog', 'organisatieId' => $organisatieId, 'entityId' => $organisationEntity->getId(), - 'active' => $organisationEntity->getActive() + 'active' => $organisationEntity->getActive(), + 'title' => $organisationEntity->getTitle() ]); + error_log('🎊 NEW_ENTITY_SUCCESS: ' . $organisatieId . ' -> Entity ID: ' . $organisationEntity->getId()); // Send registration email for new organization + $this->logger->info('[FLOW] Sending organization registration email', [ + 'organisatieId' => $organisatieId + ]); $emailSent = $this->sendOrganizationRegistrationEmail($objectData); if ($emailSent) { - $this->logger->info('OrganizationSyncService: Organization registration email sent successfully', [ + $this->logger->info('📧 Organization registration email sent successfully', [ 'organisatieId' => $organisatieId ]); } else { - $this->logger->info('OrganizationSyncService: Organization registration email not sent (disabled or not configured)', [ + $this->logger->info('📧 Organization registration email not sent (disabled or not configured)', [ 'organisatieId' => $organisatieId ]); } + } else { + $this->logger->error('❌ ORGANISATION ENTITY CREATION FAILED', [ + 'app' => 'softwarecatalog', + 'organisatieId' => $organisatieId + ]); + error_log('❌ NEW_ENTITY_FAILED: ' . $organisatieId); } return $organisationEntity; } } catch (\Exception $e) { - $this->logger->error('OrganizationSyncService: Failed to ensure organisation entity', [ + $this->logger->error('💥 ENSURE ORGANISATION ENTITY EXCEPTION', [ + 'app' => 'softwarecatalog', 'organisatieId' => $organisatieObject->getId(), - 'exception' => $e + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString() ]); + error_log('💥 ENSURE_ENTITY_ERROR: ' . $organisatieObject->getId() . ' - ' . $e->getMessage()); return null; } } @@ -1004,6 +1044,552 @@ public function recordSyncTime(): void $this->config->setValueString('softwarecatalog', 'last_sync_time', date('Y-m-d H:i:s')); } + /** + * Process a specific organization object (called from event listener) + * + * This method processes a single organization object, creating or updating + * the corresponding organization entity as needed. + * + * @param \OCA\OpenRegister\Db\ObjectEntity $organizationObject The organization object to process + * + * @return array Processing results + */ + public function processSpecificOrganization($organizationObject): array + { + $startTime = microtime(true); + $stats = [ + 'organizationsProcessed' => 0, + 'entitiesCreated' => 0, + 'entitiesUpdated' => 0, + 'contactPersonsProcessed' => 0, + 'usersCreated' => 0, + 'errors' => [], + 'startTime' => date('Y-m-d H:i:s') + ]; + + try { + $objectData = $organizationObject->getObject(); + $organizationUuid = $organizationObject->getUuid(); + + $this->logger->critical('🏢 ORGANIZATION PROCESSING STARTED', [ + 'app' => 'softwarecatalog', + 'trigger' => 'ObjectCreatedEvent', + 'organizationId' => $organizationUuid, + 'organizationName' => $objectData['naam'] ?? 'Unknown', + 'organizationStatus' => $objectData['status'] ?? 'Unknown', + 'timestamp' => date('Y-m-d H:i:s'), + 'microtime' => microtime(true) + ]); + + // Also use error_log for immediate visibility + error_log('🏢 ORG_PROCESSING_START: ' . $organizationUuid . ' (' . ($objectData['naam'] ?? 'Unknown') . ') at ' . date('Y-m-d H:i:s')); + + // Process organization entity + $this->logger->info('[FLOW] Step 1: Creating/updating organisation entity', [ + 'organizationId' => $organizationUuid, + 'action' => 'ensure_organisation_entity' + ]); + + $organisationEntity = $this->ensureOrganisationEntity($organizationObject, $stats); + + if ($organisationEntity) { + $stats['organizationsProcessed']++; + + $this->logger->critical('✅ ORGANISATION ENTITY CREATED/UPDATED', [ + 'app' => 'softwarecatalog', + 'organizationUuid' => $organizationUuid, + 'entityId' => $organisationEntity->getId(), + 'entityActive' => $organisationEntity->getActive(), + 'entitiesCreated' => $stats['entitiesCreated'], + 'entitiesUpdated' => $stats['entitiesUpdated'] + ]); + error_log('✅ ORG_ENTITY_SUCCESS: ' . $organizationUuid . ' -> Entity ID: ' . $organisationEntity->getId()); + + // Step 2: Find and process related contactpersonen objects (separate objects, not nested) + $this->logger->info('[FLOW] Step 2: Finding related contactpersoon objects', [ + 'organizationId' => $organizationUuid, + 'action' => 'process_related_contactpersonen' + ]); + + $this->processRelatedContactPersons($organizationUuid, $stats); + + } else { + $this->logger->error('❌ ORGANISATION ENTITY FAILED', [ + 'app' => 'softwarecatalog', + 'organizationUuid' => $organizationUuid, + 'error' => 'Failed to create/update organisation entity' + ]); + error_log('❌ ORG_ENTITY_FAILED: ' . $organizationUuid); + $stats['errors'][] = 'Failed to create/update organisation entity'; + } + + $stats['endTime'] = date('Y-m-d H:i:s'); + $stats['duration'] = round(microtime(true) - $startTime, 3); + + $this->logger->critical('🏁 ORGANIZATION PROCESSING COMPLETED', [ + 'app' => 'softwarecatalog', + 'organizationId' => $organizationUuid, + 'stats' => $stats, + 'processingTime' => $stats['duration'] . 's' + ]); + error_log('🏁 ORG_PROCESSING_COMPLETE: ' . $organizationUuid . ' in ' . $stats['duration'] . 's - Users: ' . $stats['usersCreated']); + + return $stats; + + } catch (\Exception $e) { + $stats['errors'][] = $e->getMessage(); + $this->logger->error('💥 ORGANIZATION PROCESSING EXCEPTION', [ + 'app' => 'softwarecatalog', + 'organizationId' => $organizationObject->getUuid(), + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString() + ]); + error_log('💥 ORG_PROCESSING_ERROR: ' . $organizationObject->getUuid() . ' - ' . $e->getMessage()); + + return $stats; + } + } + + /** + * Process nested contact persons within an organization object + * + * @param \OCA\OpenRegister\Db\ObjectEntity $organizationObject The organization object containing contact persons + * @param array $stats The statistics array to update + * @return void + */ + private function processNestedContactPersons($organizationObject, array &$stats): void + { + try { + $objectData = $organizationObject->getObject(); + $organizationUuid = $organizationObject->getUuid(); + + // Check if organization has nested contact persons + $contactPersons = $objectData['contactpersonen'] ?? $objectData['contactPersons'] ?? []; + + if (empty($contactPersons)) { + $this->logger->info('[FLOW] No nested contact persons found in organization', [ + 'organizationId' => $organizationUuid + ]); + return; + } + + $this->logger->critical('👥 PROCESSING NESTED CONTACT PERSONS', [ + 'app' => 'softwarecatalog', + 'organizationId' => $organizationUuid, + 'contactCount' => count($contactPersons) + ]); + error_log('👥 PROCESSING_NESTED_CONTACTS: ' . $organizationUuid . ' has ' . count($contactPersons) . ' contacts'); + + // Get configuration for contact person creation + $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); + $register = $voorzieningenConfig['register'] ?? ''; + $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? ''; + + if (empty($register) || empty($contactSchema)) { + $this->logger->warning('[FLOW] Contact person processing skipped - configuration missing', [ + 'organizationId' => $organizationUuid, + 'register' => $register, + 'contactSchema' => $contactSchema + ]); + return; + } + + foreach ($contactPersons as $index => $contactData) { + try { + $this->logger->info('[FLOW] Processing nested contact person', [ + 'organizationId' => $organizationUuid, + 'contactIndex' => $index, + 'contactEmail' => $contactData['email'] ?? $contactData['e-mailadres'] ?? 'unknown' + ]); + + // Create contact person object in OpenRegister if it doesn't exist + $this->createOrUpdateContactPersonObject($contactData, $organizationUuid, $register, $contactSchema, $stats); + + } catch (\Exception $e) { + $this->logger->error('[FLOW] Failed to process nested contact person', [ + 'organizationId' => $organizationUuid, + 'contactIndex' => $index, + 'exception' => $e->getMessage() + ]); + $stats['errors'][] = "Contact person {$index}: " . $e->getMessage(); + } + } + + } catch (\Exception $e) { + $this->logger->error('[FLOW] Failed to process nested contact persons', [ + 'organizationId' => $organizationObject->getUuid(), + 'exception' => $e->getMessage() + ]); + $stats['errors'][] = 'Failed to process nested contact persons: ' . $e->getMessage(); + } + } + + /** + * Process related contactpersoon objects that have this organization in their organisation property + * + * @param string $organizationUuid The organization UUID to find related contacts for + * @param array $stats The statistics array to update + * @return void + */ + private function processRelatedContactPersons(string $organizationUuid, array &$stats): void + { + try { + $this->logger->critical('🔍 FINDING RELATED CONTACT PERSONS', [ + 'app' => 'softwarecatalog', + 'organizationId' => $organizationUuid, + 'action' => 'find_related_contacts' + ]); + error_log('🔍 FINDING_RELATED_CONTACTS for org: ' . $organizationUuid); + + // Get configuration + $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); + $register = $voorzieningenConfig['register'] ?? ''; + $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? ''; + + if (empty($register) || empty($contactSchema)) { + $this->logger->warning('[FLOW] Related contact processing skipped - configuration missing', [ + 'organizationId' => $organizationUuid, + 'register' => $register, + 'contactSchema' => $contactSchema + ]); + return; + } + + // Find all contactpersoon objects that have this organization in their organisation property + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); + + // Search for contactpersoon objects with this organization reference + $query = [ + '@self' => [ + 'register' => (int) $register, + 'schema' => (int) $contactSchema + ], + 'organisatie' => $organizationUuid + ]; + + $this->logger->info('[FLOW] Searching for related contact persons', [ + 'organizationId' => $organizationUuid, + 'query' => $query + ]); + + $relatedContacts = $objectService->searchObjects($query); + + if (empty($relatedContacts)) { + $this->logger->info('[FLOW] No related contact persons found', [ + 'organizationId' => $organizationUuid + ]); + return; + } + + $this->logger->critical('👥 PROCESSING RELATED CONTACT PERSONS', [ + 'app' => 'softwarecatalog', + 'organizationId' => $organizationUuid, + 'contactCount' => count($relatedContacts) + ]); + error_log('👥 PROCESSING_RELATED_CONTACTS: ' . $organizationUuid . ' has ' . count($relatedContacts) . ' related contacts'); + + foreach ($relatedContacts as $contactObject) { + try { + $contactUuid = $contactObject->getUuid(); + $contactData = $contactObject->getObject(); + + $this->logger->info('[FLOW] Processing related contact person', [ + 'organizationId' => $organizationUuid, + 'contactId' => $contactUuid, + 'contactEmail' => $contactData['email'] ?? $contactData['e-mailadres'] ?? 'unknown' + ]); + + // Process the contact person through processSpecificContactPerson + $contactStats = $this->processSpecificContactPerson($contactObject); + + // Merge stats + $stats['contactPersonsProcessed'] += $contactStats['contactPersonsProcessed'] ?? 0; + $stats['usersCreated'] += $contactStats['usersCreated'] ?? 0; + $stats['usersUpdated'] += $contactStats['usersUpdated'] ?? 0; + if (!empty($contactStats['errors'])) { + $stats['errors'] = array_merge($stats['errors'], $contactStats['errors']); + } + + } catch (\Exception $e) { + $this->logger->error('[FLOW] Failed to process related contact person', [ + 'organizationId' => $organizationUuid, + 'contactId' => $contactObject->getUuid(), + 'exception' => $e->getMessage() + ]); + $stats['errors'][] = "Related contact {$contactObject->getUuid()}: " . $e->getMessage(); + } + } + + } catch (\Exception $e) { + $this->logger->error('[FLOW] Failed to process related contact persons', [ + 'organizationId' => $organizationUuid, + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + $stats['errors'][] = 'Failed to process related contact persons: ' . $e->getMessage(); + } + } + + /** + * Create or update a contact person object and user account + * + * @param array $contactData The contact person data + * @param string $organizationUuid The organization UUID + * @param string $register The register ID + * @param string $contactSchema The contact schema ID + * @param array $stats The statistics array to update + * @return void + */ + private function createOrUpdateContactPersonObject(array $contactData, string $organizationUuid, string $register, string $contactSchema, array &$stats): void + { + try { + // Ensure contact data has organization reference + $contactData['organisatie'] = $organizationUuid; + + $email = $contactData['email'] ?? $contactData['e-mailadres'] ?? ''; + if (empty($email)) { + $this->logger->warning('[FLOW] Contact person has no email, skipping', [ + 'organizationId' => $organizationUuid, + 'contactData' => array_keys($contactData) + ]); + return; + } + + $this->logger->critical('📧 CREATING CONTACT PERSON OBJECT', [ + 'app' => 'softwarecatalog', + 'organizationId' => $organizationUuid, + 'email' => $email, + 'name' => ($contactData['voornaam'] ?? '') . ' ' . ($contactData['achternaam'] ?? '') + ]); + error_log('📧 CREATING_CONTACT: ' . $email . ' for org ' . $organizationUuid); + + // Create the contact person object in OpenRegister + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); + $contactObject = $objectService->saveObject( + object: $contactData, + register: $register, + schema: $contactSchema, + rbac: false, + multi: false + ); + + if ($contactObject) { + $stats['contactPersonsProcessed']++; + + $this->logger->critical('✅ CONTACT PERSON OBJECT CREATED', [ + 'app' => 'softwarecatalog', + 'organizationId' => $organizationUuid, + 'contactId' => $contactObject->getUuid(), + 'email' => $email + ]); + error_log('✅ CONTACT_CREATED: ' . $contactObject->getUuid() . ' (' . $email . ')'); + + // Create user account if username is missing AND organization is active + $contactObjectData = $contactObject->getObject(); + if (empty($contactObjectData['username'])) { + // Check if organization exists in organisation entity table (only active orgs have entries) + try { + $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); + $organisationEntity = $organisationMapper->findByUuid($organizationUuid); + + if ($organisationEntity && $organisationEntity->getActive()) { + $this->logger->critical('👤 CREATING USER ACCOUNT (org is active)', [ + 'app' => 'softwarecatalog', + 'contactId' => $contactObject->getUuid(), + 'organizationId' => $organizationUuid, + 'organizationActive' => true, + 'email' => $email + ]); + error_log('👤 CREATING_USER: ' . $email . ' (org active)'); + + $user = $this->contactpersonHandler->createUserAccount($contactObject); + if ($user) { + $stats['usersCreated']++; + $contactObjectData['username'] = $user->getUID(); + + // Update the contact object with username + $contactObject->setObject($contactObjectData); + $objectService->saveObject( + object: $contactObject, + register: $register, + schema: $contactSchema, + rbac: false, + multi: false + ); + + // Add user to organization entity in database + $this->contactpersonHandler->addUserToOrganizationEntity($contactObject, $user->getUID()); + + $this->logger->critical('🎉 USER ACCOUNT CREATED SUCCESS', [ + 'app' => 'softwarecatalog', + 'contactId' => $contactObject->getUuid(), + 'username' => $user->getUID(), + 'email' => $email, + 'displayName' => $user->getDisplayName() + ]); + error_log('🎉 USER_CREATED_SUCCESS: ' . $user->getUID() . ' (' . $email . ')'); + } else { + $this->logger->error('❌ USER ACCOUNT CREATION FAILED', [ + 'app' => 'softwarecatalog', + 'contactId' => $contactObject->getUuid(), + 'email' => $email + ]); + error_log('❌ USER_CREATION_FAILED: ' . $email); + $stats['errors'][] = "Failed to create user account for {$email}"; + } + } else { + $this->logger->info('Skipping user creation - organization not active or not found in entity table', [ + 'contactId' => $contactObject->getUuid(), + 'organizationId' => $organizationUuid, + 'organizationFound' => $organisationEntity !== null, + 'organizationActive' => $organisationEntity ? $organisationEntity->getActive() : false, + 'email' => $email + ]); + error_log('⏸️ SKIP_USER_CREATION: ' . $email . ' (org not active)'); + } + } catch (\Exception $e) { + // Organization not found in entity table = not active + $this->logger->info('Skipping user creation - organization not found in entity table (not active)', [ + 'contactId' => $contactObject->getUuid(), + 'organizationId' => $organizationUuid, + 'reason' => 'Organization not found in entity table', + 'email' => $email + ]); + error_log('⏸️ SKIP_USER_CREATION: ' . $email . ' (org not in entity table)'); + } + } + } + + } catch (\Exception $e) { + $this->logger->error('[FLOW] Failed to create/update contact person object', [ + 'organizationId' => $organizationUuid, + 'email' => $contactData['email'] ?? 'unknown', + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + $stats['errors'][] = "Contact person creation failed: " . $e->getMessage(); + } + } + + /** + * Process a specific contact person object (called from event listener) + * + * This method processes a single contact person object, creating user accounts + * and updating the contact person object as needed. + * + * @param \OCA\OpenRegister\Db\ObjectEntity $contactObject The contact person object to process + * + * @return array Processing results + */ + public function processSpecificContactPerson($contactObject): array + { + $stats = [ + 'contactPersonsProcessed' => 0, + 'usersCreated' => 0, + 'usersUpdated' => 0, + 'errors' => [], + 'startTime' => date('Y-m-d H:i:s') + ]; + + try { + $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); + $register = $voorzieningenConfig['register'] ?? ''; + $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? ''; + + $this->logger->info('[EVENT] OrganizationSyncService: Processing specific contact person', [ + 'contactId' => $contactObject->getUuid(), + 'trigger' => 'event_listener' + ]); + + $contactEntityObject = $contactObject->getObject(); + + // Skip if no organization reference + $organizationUuid = $contactEntityObject['organisatie'] ?? null; + if (empty($organizationUuid)) { + $this->logger->warning('[EVENT] OrganizationSyncService: Contact person has no organization reference', [ + 'contactId' => $contactObject->getUuid() + ]); + return $stats; + } + + // Create user account if username is missing AND organization is active + if (empty($contactEntityObject['username'])) { + // Check if organization exists in organisation entity table (only active orgs have entries) + try { + $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); + $organisationEntity = $organisationMapper->findByUuid($organizationUuid); + + if ($organisationEntity && $organisationEntity->getActive()) { + $this->logger->info('[EVENT] OrganizationSyncService: Creating user account for contact person (org is active)', [ + 'contactId' => $contactObject->getUuid(), + 'organizationId' => $organizationUuid, + 'organizationActive' => true, + 'email' => $contactEntityObject['email'] ?? $contactEntityObject['e-mailadres'] ?? 'unknown' + ]); + + $user = $this->contactpersonHandler->createUserAccount($contactObject); + $contactEntityObject['username'] = $user->getUID(); + + // Update the contact object with the username (using RBAC bypass) + $contactObject->setObject($contactEntityObject); + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); + $objectService->saveObject( + object: $contactObject, + register: $register, + schema: $contactSchema, + rbac: false, + multi: false + ); + + // Add user to organization entity in database + $this->contactpersonHandler->addUserToOrganizationEntity($contactObject, $user->getUID()); + + $stats['usersCreated']++; + } else { + $this->logger->info('[EVENT] OrganizationSyncService: Skipping user creation - organization not active or not found in entity table', [ + 'contactId' => $contactObject->getUuid(), + 'organizationId' => $organizationUuid, + 'organizationFound' => $organisationEntity !== null, + 'organizationActive' => $organisationEntity ? $organisationEntity->getActive() : false, + 'email' => $contactEntityObject['email'] ?? $contactEntityObject['e-mailadres'] ?? 'unknown' + ]); + } + } catch (\Exception $e) { + // Organization not found in entity table = not active + $this->logger->info('[EVENT] OrganizationSyncService: Skipping user creation - organization not found in entity table (not active)', [ + 'contactId' => $contactObject->getUuid(), + 'organizationId' => $organizationUuid, + 'reason' => 'Organization not found in entity table', + 'email' => $contactEntityObject['email'] ?? $contactEntityObject['e-mailadres'] ?? 'unknown' + ]); + } + } + + $stats['contactPersonsProcessed']++; + $stats['endTime'] = date('Y-m-d H:i:s'); + $stats['duration'] = (new \DateTime($stats['endTime']))->getTimestamp() - (new \DateTime($stats['startTime']))->getTimestamp(); + + $this->logger->info('[EVENT] OrganizationSyncService: Specific contact person processing completed', [ + 'contactId' => $contactObject->getUuid(), + 'stats' => $stats + ]); + + return $stats; + + } catch (\Exception $e) { + $stats['errors'][] = $e->getMessage(); + $this->logger->error('[EVENT] OrganizationSyncService: Failed to process specific contact person', [ + 'contactId' => $contactObject->getUuid(), + 'exception' => $e->getMessage() + ]); + + return $stats; + } + } + /** * Performs optimized manual synchronization for large datasets * @@ -1033,9 +1619,10 @@ public function performOptimizedManualSync(int $maxRounds = 10, int $batchSize = 'startTime' => date('Y-m-d H:i:s') ]; - $this->logger->info('OrganizationSyncService: Starting optimized manual sync', [ + $this->logger->info('[MANUAL] OrganizationSyncService: Starting optimized manual sync', [ 'maxRounds' => $maxRounds, - 'batchSize' => $batchSize + 'batchSize' => $batchSize, + 'trigger' => 'manual' ]); for ($round = 1; $round <= $maxRounds; $round++) { @@ -1067,7 +1654,7 @@ public function performOptimizedManualSync(int $maxRounds = 10, int $batchSize = // If no items were processed in this round, we're done if ($orgResults['organizationsProcessed'] === 0 && $contactResults['contactPersonsProcessed'] === 0) { - $this->logger->info('OrganizationSyncService: No more items to process, stopping', [ + $this->logger->info('[MANUAL] OrganizationSyncService: No more items to process, stopping', [ 'round' => $round, 'totalProcessed' => $allResults['organizationsProcessed'] + $allResults['contactPersonsProcessed'] ]); @@ -1089,7 +1676,7 @@ public function performOptimizedManualSync(int $maxRounds = 10, int $batchSize = $allResults['totalExecutionTime'] = time() - $totalStartTime; $allResults['endTime'] = date('Y-m-d H:i:s'); - $this->logger->info('OrganizationSyncService: Optimized manual sync completed', $allResults); + $this->logger->info('[MANUAL] OrganizationSyncService: Optimized manual sync completed', $allResults); return $allResults; } @@ -1107,9 +1694,10 @@ public function performOptimizedManualSync(int $maxRounds = 10, int $batchSize = */ public function performScheduledSync(int $minutesBack = 0): array { - $this->logger->info('OrganizationSyncService: Starting scheduled synchronization', [ + $this->logger->info('[CRONJOB] OrganizationSyncService: Starting scheduled synchronization', [ 'minutesBack' => $minutesBack, - 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental' + 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental', + 'trigger' => 'cronjob' ]); try { @@ -1130,7 +1718,7 @@ public function performScheduledSync(int $minutesBack = 0): array $this->recordSyncTime(); // Log summary results - $this->logger->info('OrganizationSyncService: Scheduled synchronization completed', [ + $this->logger->info('[CRONJOB] OrganizationSyncService: Scheduled synchronization completed', [ 'organizationsProcessed' => $syncResults['organizationsProcessed'], 'entitiesCreated' => $syncResults['entitiesCreated'], 'entitiesUpdated' => $syncResults['entitiesUpdated'], @@ -1151,7 +1739,7 @@ public function performScheduledSync(int $minutesBack = 0): array return $syncResults; } catch (\Exception $e) { - $this->logger->error('OrganizationSyncService: Scheduled synchronization failed', [ + $this->logger->error('[CRONJOB] OrganizationSyncService: Scheduled synchronization failed', [ 'exception' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), diff --git a/lib/Service/SoftwareCatalogue/ContactPersonHandler.php b/lib/Service/SoftwareCatalogue/ContactPersonHandler.php index d5be7dcd..84d84fdf 100644 --- a/lib/Service/SoftwareCatalogue/ContactPersonHandler.php +++ b/lib/Service/SoftwareCatalogue/ContactPersonHandler.php @@ -195,82 +195,167 @@ private function ensureUniqueUsername(string $username): string */ public function createUserAccount(object $contactpersoonObject, bool $isFirstContact = false): ?\OCP\IUser { + $startTime = microtime(true); + try { $objectData = $contactpersoonObject->getObject(); + $contactId = $contactpersoonObject->getId(); $email = $objectData['email'] ?? $objectData['e-mailadres'] ?? ''; + $organizationUuid = $objectData['organisation'] ?? $objectData['organisatie'] ?? ''; + + $this->_logger->critical('🔥 USER ACCOUNT CREATION STARTED', [ + 'app' => 'softwarecatalog', + 'contactId' => $contactId, + 'email' => $email, + 'organizationUuid' => $organizationUuid, + 'isFirstContact' => $isFirstContact, + 'timestamp' => date('Y-m-d H:i:s'), + 'microtime' => microtime(true) + ]); + error_log('🔥 USER_CREATION_START: ' . $email . ' for contact ' . $contactId); if (empty($email)) { - $this->_logger->warning( - 'Cannot create user account: no email address provided', - ['contactpersoonId' => $contactpersoonObject->getId()] - ); + $this->_logger->error('❌ USER CREATION FAILED - NO EMAIL', [ + 'app' => 'softwarecatalog', + 'contactpersoonId' => $contactId + ]); + error_log('❌ USER_CREATION_NO_EMAIL: Contact ' . $contactId); return null; } // Generate username first to check both email and username existence $username = $objectData['username'] ?? ''; if (empty($username)) { + $this->_logger->info('[USER] Step 1: Generating username', [ + 'contactId' => $contactId, + 'email' => $email + ]); $username = $this->generateUsernameFromContactData($objectData); - + $this->_logger->critical('📝 USERNAME GENERATED', [ + 'app' => 'softwarecatalog', + 'contactId' => $contactId, + 'generatedUsername' => $username, + 'email' => $email + ]); + error_log('📝 USERNAME_GENERATED: ' . $username . ' for ' . $email); } // Check if user already exists by email + $this->_logger->info('[USER] Step 2: Checking existing user by email', [ + 'email' => $email + ]); if ($this->_userManager->userExists($email)) { - $this->_logger->info( - 'User already exists with email', - ['email' => $email, 'contactpersoonId' => $contactpersoonObject->getId()] - ); + $this->_logger->critical('♻️ USER EXISTS BY EMAIL', [ + 'app' => 'softwarecatalog', + 'email' => $email, + 'contactpersoonId' => $contactId + ]); + error_log('♻️ USER_EXISTS_EMAIL: ' . $email); + $existingUser = $this->_userManager->get($email); if ($existingUser) { // Store organization UUID for existing user - $organizationUuid = $objectData['organisation'] ?? $objectData['organisatie'] ?? ''; if (!empty($organizationUuid)) { $this->storeUserOrganizationUuid($existingUser, $organizationUuid); } // Update groups for existing user $this->assignUserGroups($existingUser, $objectData, $isFirstContact); + + $this->_logger->critical('✅ EXISTING USER UPDATED', [ + 'app' => 'softwarecatalog', + 'username' => $existingUser->getUID(), + 'email' => $email, + 'organizationUuid' => $organizationUuid + ]); + error_log('✅ EXISTING_USER_UPDATED: ' . $existingUser->getUID() . ' (' . $email . ')'); + return $existingUser; } } // Check if user already exists by username + $this->_logger->info('[USER] Step 3: Checking existing user by username', [ + 'username' => $username + ]); $existingUserByUsername = $this->_userManager->get($username); if ($existingUserByUsername) { - $this->_logger->info( - 'User already exists with username', - ['username' => $username, 'contactpersoonId' => $contactpersoonObject->getId()] - ); + $this->_logger->critical('♻️ USER EXISTS BY USERNAME', [ + 'app' => 'softwarecatalog', + 'username' => $username, + 'contactpersoonId' => $contactId + ]); + error_log('♻️ USER_EXISTS_USERNAME: ' . $username); // Store organization UUID for existing user - $organizationUuid = $objectData['organisation'] ?? $objectData['organisatie'] ?? ''; if (!empty($organizationUuid)) { $this->storeUserOrganizationUuid($existingUserByUsername, $organizationUuid); } // Update groups for existing user $this->assignUserGroups($existingUserByUsername, $objectData, $isFirstContact); + + $this->_logger->critical('✅ EXISTING USER UPDATED BY USERNAME', [ + 'app' => 'softwarecatalog', + 'username' => $username, + 'email' => $existingUserByUsername->getEMailAddress(), + 'organizationUuid' => $organizationUuid + ]); + error_log('✅ EXISTING_USER_UPDATED_USERNAME: ' . $username); + return $existingUserByUsername; } - // Username already generated above for existence checks + // Create new user account + $this->_logger->critical('🚀 CREATING NEW USER ACCOUNT', [ + 'app' => 'softwarecatalog', + 'username' => $username, + 'email' => $email, + 'contactId' => $contactId + ]); + error_log('🚀 CREATING_NEW_USER: ' . $username . ' (' . $email . ')'); - // Create user account $user = $this->_userManager->createUser($username, $username); if ($user) { + $this->_logger->critical('🎊 NEW USER ACCOUNT CREATED', [ + 'app' => 'softwarecatalog', + 'username' => $username, + 'email' => $email, + 'contactId' => $contactId, + 'userId' => $user->getUID() + ]); + error_log('🎊 NEW_USER_CREATED: ' . $user->getUID() . ' (' . $email . ')'); // Set user details + $this->_logger->info('[USER] Step 4: Setting user details', [ + 'username' => $username + ]); $user->setEMailAddress($email); - $user->setDisplayName($this->getDisplayNameFromContactData($objectData)); + $displayName = $this->getDisplayNameFromContactData($objectData); + $user->setDisplayName($displayName); + + $this->_logger->critical('📋 USER DETAILS SET', [ + 'app' => 'softwarecatalog', + 'username' => $username, + 'email' => $email, + 'displayName' => $displayName + ]); // Store organization UUID in user config for OpenConnector access - $organizationUuid = $objectData['organisation'] ?? $objectData['organisatie'] ?? ''; if (!empty($organizationUuid)) { + $this->_logger->info('[USER] Step 5: Storing organization UUID', [ + 'username' => $username, + 'organizationUuid' => $organizationUuid + ]); $this->storeUserOrganizationUuid($user, $organizationUuid); } // Set user groups based on roles and organization + $this->_logger->info('[USER] Step 6: Assigning user groups', [ + 'username' => $username, + 'isFirstContact' => $isFirstContact + ]); $this->assignUserGroups($user, $objectData, $isFirstContact); // Update contactpersoon with username @@ -278,42 +363,52 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon $contactpersoonObject->setObject($objectData); // Send user creation email + $this->_logger->info('[USER] Step 7: Sending user creation email', [ + 'username' => $username, + 'email' => $email + ]); $this->sendUserCreationEmail($user, $objectData); - $this->_logger->info( - 'Created user account for contact person', - [ - 'contactpersoonId' => $contactpersoonObject->getId(), - 'username' => $username, - 'email' => $email - ] - ); + $creationTime = round(microtime(true) - $startTime, 3); + $this->_logger->critical('🎉 USER ACCOUNT CREATION COMPLETED', [ + 'app' => 'softwarecatalog', + 'contactpersoonId' => $contactId, + 'username' => $username, + 'email' => $email, + 'displayName' => $displayName, + 'organizationUuid' => $organizationUuid, + 'creationTime' => $creationTime . 's' + ]); + error_log('🎉 USER_CREATION_COMPLETE: ' . $username . ' (' . $email . ') in ' . $creationTime . 's'); return $user; } else { - $this->_logger->error( - 'DEBUG: User creation returned null (no exception thrown)', - [ - 'username' => $username, - 'email' => $email, - 'contactpersoonId' => $contactpersoonObject->getId() - ] - ); + $this->_logger->error('❌ USER CREATION RETURNED NULL', [ + 'app' => 'softwarecatalog', + 'username' => $username, + 'email' => $email, + 'contactpersoonId' => $contactId, + 'note' => 'No exception thrown but createUser returned null' + ]); + error_log('❌ USER_CREATION_NULL: ' . $username . ' (' . $email . ')'); } return null; } catch (\Exception $e) { - $this->_logger->error( - 'Failed to create user account: ' . $e->getMessage(), - [ - 'contactpersoonId' => $contactpersoonObject->getId(), - 'exception' => $e, - 'exception_class' => get_class($e), - 'exception_code' => $e->getCode(), - 'trace' => $e->getTraceAsString() - ] - ); + $this->_logger->error('💥 USER CREATION EXCEPTION', [ + 'app' => 'softwarecatalog', + 'contactpersoonId' => $contactpersoonObject->getId(), + 'email' => $objectData['email'] ?? 'unknown', + 'username' => $username ?? 'unknown', + 'exception' => $e->getMessage(), + 'exception_class' => get_class($e), + 'exception_code' => $e->getCode(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString() + ]); + error_log('💥 USER_CREATION_ERROR: ' . ($objectData['email'] ?? 'unknown') . ' - ' . $e->getMessage()); return null; } } @@ -1782,7 +1877,7 @@ public function ensureContactpersoonInOrganization(object $contactpersoonObject) * * @return void */ - private function addUserToOrganizationEntity(object $contactpersoonObject, string $username): void + public function addUserToOrganizationEntity(object $contactpersoonObject, string $username): void { try { $objectData = $contactpersoonObject->getObject(); diff --git a/src/utils/heartbeat.js b/src/utils/heartbeat.js index e0b10b5b..cbaef54d 100644 --- a/src/utils/heartbeat.js +++ b/src/utils/heartbeat.js @@ -161,3 +161,4 @@ export default heartbeat + From 2855d00aa07327d733eb23efb7d2b8c8efd39878 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 28 Aug 2025 02:47:20 +0200 Subject: [PATCH 53/83] Schema update --- lib/Settings/softwarecatalogus_register.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index ea5a30ab..857f3faf 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -2560,8 +2560,7 @@ "objectConfiguration": { "handling": "related-object" }, - "$ref": "#/components/schemas/module", - "inversedBy": "koppeling" + "$ref": "#/components/schemas/module" }, "buitengemeentelijkVoorziening": { "description": "Buitengemeentelijke voorziening waarmee gekoppeld wordt", @@ -4715,7 +4714,8 @@ "objectConfiguration": { "handling": "related-object" }, - "$ref": "#/components/schemas/koppeling" + "$ref": "#/components/schemas/koppeling", + "inversedBy": "moduleA" } }, "compliancy": { From a7eb75b9a01b9461ca42ebd7ffc24287b4873c05 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 28 Aug 2025 03:01:12 +0200 Subject: [PATCH 54/83] Remove error_log --- docs/API_REFERENCE.md | 4 ++-- .../OpenRegisterEventsDebugListener.php | 5 ++-- lib/EventListener/TestEventListener.php | 6 ++--- lib/Service/OrganizationSyncService.php | 24 +------------------ .../ContactPersonHandler.php | 12 ---------- 5 files changed, 7 insertions(+), 44 deletions(-) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index e3fe8aef..ee66d8e8 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -299,7 +299,7 @@ try { $service->processContactgegevens($object); } catch (ProcessingException $e) { // Handle processing error - error_log('Processing failed: ' . $e->getMessage()); + $logger->error('Processing failed: ' . $e->getMessage()); } ``` @@ -311,7 +311,7 @@ try { $schemaId = $settingsService->getSchemaIdForObjectType('contactgegevens'); } catch (ConfigurationException $e) { // Handle configuration error - error_log('Configuration error: ' . $e->getMessage()); + $logger->error('Configuration error: ' . $e->getMessage()); } ``` diff --git a/lib/EventListener/OpenRegisterEventsDebugListener.php b/lib/EventListener/OpenRegisterEventsDebugListener.php index 00d7f9c9..a9601d73 100644 --- a/lib/EventListener/OpenRegisterEventsDebugListener.php +++ b/lib/EventListener/OpenRegisterEventsDebugListener.php @@ -116,12 +116,11 @@ public function handle(Event $event): void 'source' => 'OpenRegister', ]); - // Also use error_log for immediate stdout visibility - error_log('🔍 SOFTWARECATALOG_OPENREGISTER_DEBUG_LISTENER: ' . $eventType . ' (' . $eventClass . ') at ' . date('Y-m-d H:i:s')); + if (!$this->debugEnabled) { $this->logger->warning('SoftwareCatalog OpenRegister Debug: Debug disabled, skipping detailed logging'); - error_log('SOFTWARECATALOG_DEBUG_DISABLED: Debug logging disabled for ' . $eventType); + return; } diff --git a/lib/EventListener/TestEventListener.php b/lib/EventListener/TestEventListener.php index 8421240f..0996f61b 100644 --- a/lib/EventListener/TestEventListener.php +++ b/lib/EventListener/TestEventListener.php @@ -69,8 +69,7 @@ public function handle(Event $event): void 'microtime' => microtime(true) ]); - // Also use error_log for immediate visibility in system logs - error_log('SOFTWARECATALOG_TEST_LISTENER_TRIGGERED: ' . get_class($event) . ' at ' . date('Y-m-d_H:i:s')); + // Handle UserLoggedInEvent specifically if ($event instanceof UserLoggedInEvent) { @@ -84,8 +83,7 @@ public function handle(Event $event): void 'eventType' => 'UserLoggedInEvent' ]); - // Additional error_log for easy debugging - error_log('SOFTWARECATALOG_USER_LOGIN: User ' . $user->getUID() . ' (' . $user->getDisplayName() . ') logged in at ' . date('Y-m-d_H:i:s')); + // Test that we can access Nextcloud services try { diff --git a/lib/Service/OrganizationSyncService.php b/lib/Service/OrganizationSyncService.php index 8c683aab..cfb2e206 100644 --- a/lib/Service/OrganizationSyncService.php +++ b/lib/Service/OrganizationSyncService.php @@ -537,7 +537,6 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta 'naam' => $objectData['naam'] ?? 'Unknown', 'status' => $objectData['status'] ?? 'Unknown' ]); - error_log('🔍 ENSURING_ORG_ENTITY: ' . $organisatieId); // Try to find existing organisation entity $organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper'); @@ -557,7 +556,6 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta 'shouldBeActive' => $shouldBeActive, 'needsUpdate' => $organisationEntity->getActive() !== $shouldBeActive ]); - error_log('📋 EXISTING_ENTITY: ' . $organisatieId . ' -> Entity ID: ' . $organisationEntity->getId() . ' (Active: ' . ($organisationEntity->getActive() ? 'true' : 'false') . ')'); if ($organisationEntity->getActive() !== $shouldBeActive) { $this->logger->critical('🔄 UPDATING ENTITY STATUS', [ @@ -566,7 +564,6 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta 'oldActive' => $organisationEntity->getActive(), 'newActive' => $shouldBeActive ]); - error_log('🔄 UPDATING_ENTITY_STATUS: ' . $organisatieId . ' from ' . ($organisationEntity->getActive() ? 'active' : 'inactive') . ' to ' . ($shouldBeActive ? 'active' : 'inactive')); $wasActive = $organisationEntity->getActive(); $organisationEntity->setActive($shouldBeActive); @@ -600,7 +597,6 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta 'organisatieId' => $organisatieId, 'naam' => $objectData['naam'] ?? 'Unknown' ]); - error_log('🆕 CREATING_NEW_ENTITY: ' . $organisatieId . ' (' . ($objectData['naam'] ?? 'Unknown') . ')'); $organisationEntity = $this->organisatieService->createOrganisationInOpenRegister($objectData); if ($organisationEntity) { @@ -612,7 +608,6 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta 'active' => $organisationEntity->getActive(), 'title' => $organisationEntity->getTitle() ]); - error_log('🎊 NEW_ENTITY_SUCCESS: ' . $organisatieId . ' -> Entity ID: ' . $organisationEntity->getId()); // Send registration email for new organization $this->logger->info('[FLOW] Sending organization registration email', [ @@ -633,7 +628,6 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta 'app' => 'softwarecatalog', 'organisatieId' => $organisatieId ]); - error_log('❌ NEW_ENTITY_FAILED: ' . $organisatieId); } return $organisationEntity; } @@ -647,7 +641,6 @@ private function ensureOrganisationEntity(object $organisatieObject, array &$sta 'line' => $e->getLine(), 'trace' => $e->getTraceAsString() ]); - error_log('💥 ENSURE_ENTITY_ERROR: ' . $organisatieObject->getId() . ' - ' . $e->getMessage()); return null; } } @@ -1081,8 +1074,7 @@ public function processSpecificOrganization($organizationObject): array 'microtime' => microtime(true) ]); - // Also use error_log for immediate visibility - error_log('🏢 ORG_PROCESSING_START: ' . $organizationUuid . ' (' . ($objectData['naam'] ?? 'Unknown') . ') at ' . date('Y-m-d H:i:s')); + // Process organization entity $this->logger->info('[FLOW] Step 1: Creating/updating organisation entity', [ @@ -1103,7 +1095,6 @@ public function processSpecificOrganization($organizationObject): array 'entitiesCreated' => $stats['entitiesCreated'], 'entitiesUpdated' => $stats['entitiesUpdated'] ]); - error_log('✅ ORG_ENTITY_SUCCESS: ' . $organizationUuid . ' -> Entity ID: ' . $organisationEntity->getId()); // Step 2: Find and process related contactpersonen objects (separate objects, not nested) $this->logger->info('[FLOW] Step 2: Finding related contactpersoon objects', [ @@ -1119,7 +1110,6 @@ public function processSpecificOrganization($organizationObject): array 'organizationUuid' => $organizationUuid, 'error' => 'Failed to create/update organisation entity' ]); - error_log('❌ ORG_ENTITY_FAILED: ' . $organizationUuid); $stats['errors'][] = 'Failed to create/update organisation entity'; } @@ -1132,7 +1122,6 @@ public function processSpecificOrganization($organizationObject): array 'stats' => $stats, 'processingTime' => $stats['duration'] . 's' ]); - error_log('🏁 ORG_PROCESSING_COMPLETE: ' . $organizationUuid . ' in ' . $stats['duration'] . 's - Users: ' . $stats['usersCreated']); return $stats; @@ -1146,7 +1135,6 @@ public function processSpecificOrganization($organizationObject): array 'line' => $e->getLine(), 'trace' => $e->getTraceAsString() ]); - error_log('💥 ORG_PROCESSING_ERROR: ' . $organizationObject->getUuid() . ' - ' . $e->getMessage()); return $stats; } @@ -1180,7 +1168,6 @@ private function processNestedContactPersons($organizationObject, array &$stats) 'organizationId' => $organizationUuid, 'contactCount' => count($contactPersons) ]); - error_log('👥 PROCESSING_NESTED_CONTACTS: ' . $organizationUuid . ' has ' . count($contactPersons) . ' contacts'); // Get configuration for contact person creation $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); @@ -1241,7 +1228,6 @@ private function processRelatedContactPersons(string $organizationUuid, array &$ 'organizationId' => $organizationUuid, 'action' => 'find_related_contacts' ]); - error_log('🔍 FINDING_RELATED_CONTACTS for org: ' . $organizationUuid); // Get configuration $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); @@ -1288,7 +1274,6 @@ private function processRelatedContactPersons(string $organizationUuid, array &$ 'organizationId' => $organizationUuid, 'contactCount' => count($relatedContacts) ]); - error_log('👥 PROCESSING_RELATED_CONTACTS: ' . $organizationUuid . ' has ' . count($relatedContacts) . ' related contacts'); foreach ($relatedContacts as $contactObject) { try { @@ -1364,7 +1349,6 @@ private function createOrUpdateContactPersonObject(array $contactData, string $o 'email' => $email, 'name' => ($contactData['voornaam'] ?? '') . ' ' . ($contactData['achternaam'] ?? '') ]); - error_log('📧 CREATING_CONTACT: ' . $email . ' for org ' . $organizationUuid); // Create the contact person object in OpenRegister $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); @@ -1385,7 +1369,6 @@ private function createOrUpdateContactPersonObject(array $contactData, string $o 'contactId' => $contactObject->getUuid(), 'email' => $email ]); - error_log('✅ CONTACT_CREATED: ' . $contactObject->getUuid() . ' (' . $email . ')'); // Create user account if username is missing AND organization is active $contactObjectData = $contactObject->getObject(); @@ -1403,7 +1386,6 @@ private function createOrUpdateContactPersonObject(array $contactData, string $o 'organizationActive' => true, 'email' => $email ]); - error_log('👤 CREATING_USER: ' . $email . ' (org active)'); $user = $this->contactpersonHandler->createUserAccount($contactObject); if ($user) { @@ -1430,14 +1412,12 @@ private function createOrUpdateContactPersonObject(array $contactData, string $o 'email' => $email, 'displayName' => $user->getDisplayName() ]); - error_log('🎉 USER_CREATED_SUCCESS: ' . $user->getUID() . ' (' . $email . ')'); } else { $this->logger->error('❌ USER ACCOUNT CREATION FAILED', [ 'app' => 'softwarecatalog', 'contactId' => $contactObject->getUuid(), 'email' => $email ]); - error_log('❌ USER_CREATION_FAILED: ' . $email); $stats['errors'][] = "Failed to create user account for {$email}"; } } else { @@ -1448,7 +1428,6 @@ private function createOrUpdateContactPersonObject(array $contactData, string $o 'organizationActive' => $organisationEntity ? $organisationEntity->getActive() : false, 'email' => $email ]); - error_log('⏸️ SKIP_USER_CREATION: ' . $email . ' (org not active)'); } } catch (\Exception $e) { // Organization not found in entity table = not active @@ -1458,7 +1437,6 @@ private function createOrUpdateContactPersonObject(array $contactData, string $o 'reason' => 'Organization not found in entity table', 'email' => $email ]); - error_log('⏸️ SKIP_USER_CREATION: ' . $email . ' (org not in entity table)'); } } } diff --git a/lib/Service/SoftwareCatalogue/ContactPersonHandler.php b/lib/Service/SoftwareCatalogue/ContactPersonHandler.php index 84d84fdf..bf7f81b7 100644 --- a/lib/Service/SoftwareCatalogue/ContactPersonHandler.php +++ b/lib/Service/SoftwareCatalogue/ContactPersonHandler.php @@ -212,14 +212,12 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'timestamp' => date('Y-m-d H:i:s'), 'microtime' => microtime(true) ]); - error_log('🔥 USER_CREATION_START: ' . $email . ' for contact ' . $contactId); if (empty($email)) { $this->_logger->error('❌ USER CREATION FAILED - NO EMAIL', [ 'app' => 'softwarecatalog', 'contactpersoonId' => $contactId ]); - error_log('❌ USER_CREATION_NO_EMAIL: Contact ' . $contactId); return null; } @@ -237,7 +235,6 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'generatedUsername' => $username, 'email' => $email ]); - error_log('📝 USERNAME_GENERATED: ' . $username . ' for ' . $email); } // Check if user already exists by email @@ -250,7 +247,6 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'email' => $email, 'contactpersoonId' => $contactId ]); - error_log('♻️ USER_EXISTS_EMAIL: ' . $email); $existingUser = $this->_userManager->get($email); if ($existingUser) { @@ -268,7 +264,6 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'email' => $email, 'organizationUuid' => $organizationUuid ]); - error_log('✅ EXISTING_USER_UPDATED: ' . $existingUser->getUID() . ' (' . $email . ')'); return $existingUser; } @@ -285,7 +280,6 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'username' => $username, 'contactpersoonId' => $contactId ]); - error_log('♻️ USER_EXISTS_USERNAME: ' . $username); // Store organization UUID for existing user if (!empty($organizationUuid)) { @@ -301,7 +295,6 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'email' => $existingUserByUsername->getEMailAddress(), 'organizationUuid' => $organizationUuid ]); - error_log('✅ EXISTING_USER_UPDATED_USERNAME: ' . $username); return $existingUserByUsername; } @@ -313,7 +306,6 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'email' => $email, 'contactId' => $contactId ]); - error_log('🚀 CREATING_NEW_USER: ' . $username . ' (' . $email . ')'); $user = $this->_userManager->createUser($username, $username); @@ -325,7 +317,6 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'contactId' => $contactId, 'userId' => $user->getUID() ]); - error_log('🎊 NEW_USER_CREATED: ' . $user->getUID() . ' (' . $email . ')'); // Set user details $this->_logger->info('[USER] Step 4: Setting user details', [ @@ -379,7 +370,6 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'organizationUuid' => $organizationUuid, 'creationTime' => $creationTime . 's' ]); - error_log('🎉 USER_CREATION_COMPLETE: ' . $username . ' (' . $email . ') in ' . $creationTime . 's'); return $user; } else { @@ -390,7 +380,6 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'contactpersoonId' => $contactId, 'note' => 'No exception thrown but createUser returned null' ]); - error_log('❌ USER_CREATION_NULL: ' . $username . ' (' . $email . ')'); } return null; @@ -408,7 +397,6 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon 'line' => $e->getLine(), 'trace' => $e->getTraceAsString() ]); - error_log('💥 USER_CREATION_ERROR: ' . ($objectData['email'] ?? 'unknown') . ' - ' . $e->getMessage()); return null; } } From c9c27393e5b0ed00ac989eab6a09a71c0e6cf626 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 28 Aug 2025 10:38:45 +0200 Subject: [PATCH 55/83] Lets auto fix gebruik objecten --- lib/AppInfo/Application.php | 9 + lib/Controller/SettingsController.php | 6 + .../SoftwareCatalogEventListener.php | 112 ++++- lib/Service/GebruikSyncService.php | 423 ++++++++++++++++++ lib/Service/OrganizationSyncService.php | 4 - lib/Service/SettingsService.php | 12 +- lib/Settings/softwarecatalogus_register.json | 11 + 7 files changed, 564 insertions(+), 13 deletions(-) create mode 100644 lib/Service/GebruikSyncService.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 87b30033..1f656485 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -50,6 +50,7 @@ use Psr\Container\ContainerInterface; use OCA\SoftwareCatalog\Service\SymfonyEmailService; use OCA\SoftwareCatalog\Service\SettingsService; +use OCA\SoftwareCatalog\Service\GebruikSyncService; /** * Main Application class for SoftwareCatalog @@ -209,6 +210,14 @@ public function register(IRegistrationContext $context): void ); }); + // Register gebruik sync service + $context->registerService(\OCA\SoftwareCatalog\Service\GebruikSyncService::class, function ($container) { + return new \OCA\SoftwareCatalog\Service\GebruikSyncService( + $container->get('Psr\Log\LoggerInterface'), + $container->get(SettingsService::class) + ); + }); + // Event listener uses direct service access like OpenCatalogi - no service registration needed // Register ArchiMate import service diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 443c181b..07a07c0d 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -1020,6 +1020,12 @@ public function render(): string public function importArchiMate(): JSONResponse { try { + // Increase memory limit for large imports + ini_set('memory_limit', '4096M'); + $this->logger->info('Memory limit increased for import', [ + 'old_limit' => ini_get('memory_limit'), + 'new_limit' => '4096M' + ]); // Get JSON data from request body $rawInput = file_get_contents('php://input'); $data = json_decode($rawInput, true); diff --git a/lib/EventListener/SoftwareCatalogEventListener.php b/lib/EventListener/SoftwareCatalogEventListener.php index 44232914..d88ac73c 100644 --- a/lib/EventListener/SoftwareCatalogEventListener.php +++ b/lib/EventListener/SoftwareCatalogEventListener.php @@ -20,6 +20,7 @@ use OCA\SoftwareCatalog\Service\ContactpersoonService; use OCA\SoftwareCatalog\Service\SettingsService; +use OCA\SoftwareCatalog\Service\GebruikSyncService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCA\OpenRegister\Event\ObjectCreatedEvent; @@ -148,6 +149,7 @@ private function handleObjectCreated(ObjectCreatedEvent $event, ContactpersoonSe $organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie'); $contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType('contactpersoon'); $contactgegevensSchemaId = $settingsService->getSchemaIdForObjectType('contactgegevens'); + $gebruikSchemaId = $settingsService->getSchemaIdForObjectType('gebruik'); $logger->debug( 'SoftwareCatalog: Configuration lookup results', @@ -155,6 +157,7 @@ private function handleObjectCreated(ObjectCreatedEvent $event, ContactpersoonSe 'organisatieSchemaId' => $organisatieSchemaId, 'contactpersoonSchemaId' => $contactpersoonSchemaId, 'contactgegevensSchemaId' => $contactgegevensSchemaId, + 'gebruikSchemaId' => $gebruikSchemaId, 'objectSchemaId' => $objectSchemaIdInt ] ); @@ -211,6 +214,30 @@ private function handleObjectCreated(ObjectCreatedEvent $event, ContactpersoonSe return; } + // Check if this is a gebruik object + if ($gebruikSchemaId && $objectSchemaIdInt === (int) $gebruikSchemaId) { + $logger->info('SoftwareCatalog: Processing gebruik creation', ['objectId' => $objectId]); + + try { + // Process gebruik object with GebruikSyncService + $gebruikSyncService = \OC::$server->get(GebruikSyncService::class); + $result = $gebruikSyncService->processSpecificGebruik($object); + + $logger->info('SoftwareCatalog: Successfully processed gebruik creation', [ + 'objectId' => $objectId, + 'processResult' => $result + ]); + } catch (\Exception $e) { + $logger->error('SoftwareCatalog: Failed to process gebruik creation', [ + 'objectId' => $objectId, + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + return; + } + // Log unhandled object types $logger->debug( 'SoftwareCatalog: Object creation not handled - not a supported object type', @@ -221,7 +248,8 @@ private function handleObjectCreated(ObjectCreatedEvent $event, ContactpersoonSe 'supportedSchemas' => [ 'organisatie' => $organisatieSchemaId, 'contactpersoon' => $contactpersoonSchemaId, - 'contactgegevens' => $contactgegevensSchemaId + 'contactgegevens' => $contactgegevensSchemaId, + 'gebruik' => $gebruikSchemaId ] ] ); @@ -388,9 +416,51 @@ private function handleObjectUpdated(ObjectUpdatedEvent $event, ContactpersoonSe return; } + // Handle gebruik updates + $gebruikSchemaId = $settingsService->getSchemaIdForObjectType('gebruik'); + $gebruikSchemaIdInt = (int) $gebruikSchemaId; + + if ($gebruikSchemaId && $objectSchemaIdInt === $gebruikSchemaIdInt) { + $logger->info( + 'SoftwareCatalog: Matched gebruik schema - processing update', + [ + 'objectId' => $objectId, + 'schemaId' => $objectSchemaId, + 'configuredSchemaId' => $gebruikSchemaId + ] + ); + + try { + // Process gebruik object with GebruikSyncService + $gebruikSyncService = \OC::$server->get(GebruikSyncService::class); + $result = $gebruikSyncService->processSpecificGebruik($object); + + $logger->info( + 'SoftwareCatalog: Successfully processed gebruik update', + [ + 'objectId' => $objectId, + 'processResult' => $result, + 'timestamp' => date('Y-m-d H:i:s') + ] + ); + } catch (\Exception $e) { + $logger->error( + 'SoftwareCatalog: Failed to process gebruik update', + [ + 'objectId' => $objectId, + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString() + ] + ); + } + return; + } + // Log if we don't handle this schema type $logger->debug( - 'SoftwareCatalog: Object update not handled - focusing only on organisatie and contactpersonen', + 'SoftwareCatalog: Object update not handled - focusing only on organisatie, contactpersonen, and gebruik', [ 'objectId' => $objectId, 'schemaId' => $objectSchemaId, @@ -400,7 +470,8 @@ private function handleObjectUpdated(ObjectUpdatedEvent $event, ContactpersoonSe 'handledSchemas' => [ 'organisatie' => $organisatieSchemaId, 'contactpersoon' => $contactpersoonSchemaId, - 'contactgegevens' => $contactgegevensSchemaId + 'contactgegevens' => $contactgegevensSchemaId, + 'gebruik' => $gebruikSchemaId ] ] ); @@ -547,9 +618,39 @@ private function handleObjectDeleted(ObjectDeletedEvent $event, ContactpersoonSe return; } + // Handle gebruik deletion + $gebruikSchemaId = $settingsService->getSchemaIdForObjectType('gebruik'); + $gebruikSchemaIdInt = (int) $gebruikSchemaId; + + if ($gebruikSchemaId && $objectSchemaIdInt === $gebruikSchemaIdInt) { + $objectData = $object->getObject(); + + $logger->info( + 'SoftwareCatalog: Matched gebruik schema - processing deletion', + [ + 'objectId' => $objectId, + 'schemaId' => $objectSchemaId, + 'configuredSchemaId' => $gebruikSchemaId, + 'afnemer' => $objectData['afnemer']['naam'] ?? 'Unknown', + 'product' => $objectData['product']['naam'] ?? 'Unknown' + ] + ); + + // For deletions, we mainly log the event since the object is being removed + // No specific cleanup needed for gebruik objects currently + $logger->info( + 'SoftwareCatalog: Gebruik object deleted - no specific cleanup required', + [ + 'objectId' => $objectId, + 'timestamp' => date('Y-m-d H:i:s') + ] + ); + return; + } + // Log if we don't handle this schema type $logger->debug( - 'SoftwareCatalog: Object deletion not handled - focusing only on organisatie and contactpersonen', + 'SoftwareCatalog: Object deletion not handled - focusing only on organisatie, contactpersonen, and gebruik', [ 'objectId' => $objectId, 'schemaId' => $objectSchemaId, @@ -557,7 +658,8 @@ private function handleObjectDeleted(ObjectDeletedEvent $event, ContactpersoonSe 'handledSchemas' => [ 'organisatie' => $organisatieSchemaId, 'contactpersoon' => $contactpersoonSchemaId, - 'contactgegevens' => $contactgegevensSchemaId + 'contactgegevens' => $contactgegevensSchemaId, + 'gebruik' => $gebruikSchemaId ] ] ); diff --git a/lib/Service/GebruikSyncService.php b/lib/Service/GebruikSyncService.php new file mode 100644 index 00000000..4d44a81a --- /dev/null +++ b/lib/Service/GebruikSyncService.php @@ -0,0 +1,423 @@ + + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl + * @version 1.0.0 + * @link https://github.com/conduction/nextcloud-software-catalog + */ +class GebruikSyncService +{ + private LoggerInterface $logger; + private SettingsService $settingsService; + + /** + * Constructor for GebruikSyncService + * + * @param LoggerInterface $logger Logger for debugging and error reporting + * @param SettingsService $settingsService Service for retrieving configuration settings + */ + public function __construct( + LoggerInterface $logger, + SettingsService $settingsService + ) { + $this->logger = $logger; + $this->settingsService = $settingsService; + } + + /** + * Process a specific gebruik object + * + * This method handles both AMEF elements processing and status auto-update + * + * @param ObjectEntity $gebruikObject The gebruik object to process + * @return array Processing statistics + */ + public function processSpecificGebruik(ObjectEntity $gebruikObject): array + { + $startTime = microtime(true); + $stats = [ + 'startTime' => date('Y-m-d H:i:s'), + 'gebruikId' => $gebruikObject->getUuid(), + 'amefElementsProcessed' => 0, + 'statusUpdated' => false, + 'errors' => [], + 'duration' => 0 + ]; + + try { + $gebruikData = $gebruikObject->getObject(); + $gebruikUuid = $gebruikObject->getUuid(); + + $this->logger->critical('🔄 PROCESSING GEBRUIK OBJECT', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikUuid, + 'afnemer' => $gebruikData['afnemer']['naam'] ?? 'Unknown', + 'product' => $gebruikData['product']['naam'] ?? 'Unknown', + 'currentStatus' => $gebruikData['status'] ?? 'Unknown', + 'timestamp' => date('Y-m-d H:i:s') + ]); + + // Step 1: Process gebruiktVoorReferentiecomponenten for AMEF elements + $amefStats = $this->processAmefElements($gebruikObject); + $stats['amefElementsProcessed'] = $amefStats['amefElementsProcessed']; + $stats['errors'] = array_merge($stats['errors'], $amefStats['errors']); + + // Step 2: Auto-update status based on dates + $statusStats = $this->updateStatusBasedOnDates($gebruikObject); + $stats['statusUpdated'] = $statusStats['statusUpdated']; + $stats['errors'] = array_merge($stats['errors'], $statusStats['errors']); + + $stats['endTime'] = date('Y-m-d H:i:s'); + $stats['duration'] = round(microtime(true) - $startTime, 3); + + $this->logger->critical('✅ GEBRUIK PROCESSING COMPLETED', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikUuid, + 'stats' => $stats, + 'processingTime' => $stats['duration'] . 's' + ]); + + return $stats; + + } catch (Exception $e) { + $stats['errors'][] = $e->getMessage(); + $stats['duration'] = round(microtime(true) - $startTime, 3); + + $this->logger->error('💥 GEBRUIK PROCESSING ERROR', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikObject->getUuid(), + 'exception' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString() + ]); + + return $stats; + } + } + + /** + * Process AMEF elements from gebruiktVoorReferentiecomponenten + * + * Searches for AMEF elements based on IDs in gebruiktVoorReferentiecomponenten + * and adds their slugs to the amefElements array + * + * @param ObjectEntity $gebruikObject The gebruik object to process + * @return array Processing statistics + */ + private function processAmefElements(ObjectEntity $gebruikObject): array + { + $stats = [ + 'amefElementsProcessed' => 0, + 'errors' => [] + ]; + + try { + $gebruikData = $gebruikObject->getObject(); + $gebruikUuid = $gebruikObject->getUuid(); + + // Get the referentiecomponenten IDs + $referentieComponenten = $gebruikData['gebruiktVoorReferentiecomponenten'] ?? []; + + if (empty($referentieComponenten)) { + $this->logger->info('🔍 No referentiecomponenten found for gebruik object', [ + 'gebruikId' => $gebruikUuid + ]); + return $stats; + } + + $this->logger->critical('🔍 PROCESSING REFERENTIECOMPONENTEN', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikUuid, + 'referentieComponentenCount' => count($referentieComponenten) + ]); + + // Extract IDs from referentiecomponenten + $referentieIds = []; + foreach ($referentieComponenten as $component) { + if (isset($component['id'])) { + $referentieIds[] = $component['id']; + } + } + + if (empty($referentieIds)) { + $this->logger->warning('⚠️ No valid IDs found in referentiecomponenten', [ + 'gebruikId' => $gebruikUuid + ]); + return $stats; + } + + // Get AMEF register configuration + $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); + $amefRegister = $voorzieningenConfig['amef_register'] ?? ''; + $elementSchema = $voorzieningenConfig['element_schema'] ?? ''; + + if (empty($amefRegister) || empty($elementSchema)) { + $stats['errors'][] = 'AMEF register or element schema not configured'; + $this->logger->error('❌ AMEF configuration missing', [ + 'app' => 'softwarecatalog', + 'amefRegister' => $amefRegister, + 'elementSchema' => $elementSchema + ]); + return $stats; + } + + // Search for AMEF elements + $amefElements = $this->searchAmefElementsByIds($referentieIds, $amefRegister, $elementSchema); + + // Extract slugs from found AMEF elements + $amefSlugs = []; + foreach ($amefElements as $amefElement) { + $amefData = $amefElement->getObject(); + if (isset($amefData['slug'])) { + $amefSlugs[] = $amefData['slug']; + $stats['amefElementsProcessed']++; + } + } + + // Update the gebruik object with AMEF slugs + if (!empty($amefSlugs)) { + $gebruikData['amefElements'] = array_unique($amefSlugs); + $this->updateGebruikObject($gebruikObject, $gebruikData); + + $this->logger->critical('✅ AMEF ELEMENTS UPDATED', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikUuid, + 'amefSlugs' => $amefSlugs, + 'amefElementsCount' => count($amefSlugs) + ]); + } else { + $this->logger->info('ℹ️ No AMEF elements with slugs found', [ + 'gebruikId' => $gebruikUuid + ]); + } + + return $stats; + + } catch (Exception $e) { + $stats['errors'][] = 'AMEF processing error: ' . $e->getMessage(); + $this->logger->error('💥 AMEF PROCESSING ERROR', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikObject->getUuid(), + 'exception' => $e->getMessage() + ]); + + return $stats; + } + } + + /** + * Search for AMEF elements by IDs + * + * Uses searchObjects to find AMEF elements based on provided IDs. + * Since searchObjects may not support direct IDs array parameter, + * this method implements multiple individual searches. + * + * @param array $ids Array of IDs to search for + * @param string $register AMEF register ID + * @param string $schema Element schema ID + * @return array Array of found ObjectEntity objects + */ + private function searchAmefElementsByIds(array $ids, string $register, string $schema): array + { + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); + $foundElements = []; + + foreach ($ids as $id) { + try { + // Try to search by ID + $query = [ + '@self' => [ + 'register' => (int) $register, + 'schema' => (int) $schema + ], + 'id' => $id + ]; + + $elements = $objectService->searchObjects($query); + $foundElements = array_merge($foundElements, $elements); + + } catch (Exception $e) { + $this->logger->warning('⚠️ Failed to search for AMEF element', [ + 'app' => 'softwarecatalog', + 'id' => $id, + 'error' => $e->getMessage() + ]); + } + } + + $this->logger->info('🔍 AMEF elements search completed', [ + 'app' => 'softwarecatalog', + 'searchedIds' => $ids, + 'foundElementsCount' => count($foundElements) + ]); + + return $foundElements; + } + + /** + * Update status based on date fields + * + * Looks at all status date fields and updates the status to the one + * with the highest date that is not in the future + * + * @param ObjectEntity $gebruikObject The gebruik object to process + * @return array Processing statistics + */ + private function updateStatusBasedOnDates(ObjectEntity $gebruikObject): array + { + $stats = [ + 'statusUpdated' => false, + 'errors' => [] + ]; + + try { + $gebruikData = $gebruikObject->getObject(); + $gebruikUuid = $gebruikObject->getUuid(); + $currentStatus = $gebruikData['status'] ?? ''; + + // Define status dates mapping + $statusDates = [ + 'Verwerving' => $gebruikData['startDatumVerwerving'] ?? null, + 'Gepland' => $gebruikData['startDatumGepland'] ?? null, + 'In productie' => $gebruikData['startDatumInProductie'] ?? null, + 'Uit te faseren' => $gebruikData['startDatumUitTeFaseren'] ?? null, + 'Uitgefaseerd' => $gebruikData['startDatumUitGefaseerd'] ?? null + ]; + + $this->logger->info('🔍 CHECKING STATUS DATES', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikUuid, + 'currentStatus' => $currentStatus, + 'statusDates' => $statusDates + ]); + + // Find the highest date that is not in the future + $now = new DateTime(); + $targetStatus = null; + $targetDate = null; + + foreach ($statusDates as $status => $dateString) { + if (!empty($dateString)) { + try { + $date = new DateTime($dateString); + + // Only consider dates that are not in the future + if ($date <= $now) { + if ($targetDate === null || $date > $targetDate) { + $targetDate = $date; + $targetStatus = $status; + } + } + } catch (Exception $e) { + $this->logger->warning('⚠️ Invalid date format', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikUuid, + 'status' => $status, + 'dateString' => $dateString, + 'error' => $e->getMessage() + ]); + } + } + } + + // Update status if we found a different one + if ($targetStatus && $targetStatus !== $currentStatus) { + $gebruikData['status'] = $targetStatus; + $this->updateGebruikObject($gebruikObject, $gebruikData); + $stats['statusUpdated'] = true; + + $this->logger->critical('✅ STATUS AUTO-UPDATED', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikUuid, + 'oldStatus' => $currentStatus, + 'newStatus' => $targetStatus, + 'basedOnDate' => $targetDate ? $targetDate->format('Y-m-d') : null + ]); + } else { + $this->logger->info('ℹ️ No status update needed', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikUuid, + 'currentStatus' => $currentStatus, + 'targetStatus' => $targetStatus + ]); + } + + return $stats; + + } catch (Exception $e) { + $stats['errors'][] = 'Status update error: ' . $e->getMessage(); + $this->logger->error('💥 STATUS UPDATE ERROR', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikObject->getUuid(), + 'exception' => $e->getMessage() + ]); + + return $stats; + } + } + + /** + * Update a gebruik object in OpenRegister + * + * @param ObjectEntity $gebruikObject The object to update + * @param array $updatedData The updated data + * @return void + * @throws Exception If the update fails + */ + private function updateGebruikObject(ObjectEntity $gebruikObject, array $updatedData): void + { + try { + $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService'); + + // Get voorzieningenConfig to find the correct register and schema + $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig(); + $register = $voorzieningenConfig['register'] ?? ''; + $gebruikSchema = $voorzieningenConfig['gebruik_schema'] ?? ''; + + if (empty($register) || empty($gebruikSchema)) { + throw new Exception('Register or gebruik schema not configured'); + } + + // Update the object + $objectService->saveObject( + object: $updatedData, + register: (int) $register, + schema: (int) $gebruikSchema, + id: $gebruikObject->getUuid() + ); + + $this->logger->info('✅ Gebruik object updated successfully', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikObject->getUuid() + ]); + + } catch (Exception $e) { + $this->logger->error('❌ Failed to update gebruik object', [ + 'app' => 'softwarecatalog', + 'gebruikId' => $gebruikObject->getUuid(), + 'error' => $e->getMessage() + ]); + throw $e; + } + } +} diff --git a/lib/Service/OrganizationSyncService.php b/lib/Service/OrganizationSyncService.php index cfb2e206..705786dc 100644 --- a/lib/Service/OrganizationSyncService.php +++ b/lib/Service/OrganizationSyncService.php @@ -295,11 +295,7 @@ public function performUserSync(): array ->andWhere($qb->expr()->eq($qb->createFunction('json_contains(oo.users, json_extract(`o`.`object`, \'$.username\'))'), $qb->createNamedParameter(0))); $sql = $qb->getSQL(); -// var_dump($sql); $users = $qb->execute()->fetchAll(); - -// var_dump(count($users)); -// var_dump('hello'); foreach($users as $user) { $this->organisatieService->addUsersToOrganization($user['organisation'], [$user['username']]); } diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php index cb92c969..5a22fb7f 100644 --- a/lib/Service/SettingsService.php +++ b/lib/Service/SettingsService.php @@ -2960,7 +2960,7 @@ private function normalizeVoorzieningenConfig(array $input): array // Register id $normalized['register'] = isset($input['register']) ? (string)$input['register'] : ''; - // Known schema keys to support (18 total) + // Known schema keys to support (19 total) $schemaKeys = [ 'organisatie_schema', 'contactpersoon_schema', @@ -2980,6 +2980,7 @@ private function normalizeVoorzieningenConfig(array $input): array 'module_gebruik_schema', 'module_versie_schema', 'sector_schema', + 'gebruik_schema', ]; // Copy any present schema keys; ignore sources/registers @@ -3168,7 +3169,8 @@ private function getVoorzieningenObjectCounts(): array 'totalCompliancyObjects' => 0, 'totalModuleGebruikObjects' => 0, 'totalModuleVersieObjects' => 0, - 'totalSectorObjects' => 0 + 'totalSectorObjects' => 0, + 'totalGebruikObjects' => 0 ]; } @@ -3194,7 +3196,8 @@ private function getVoorzieningenObjectCounts(): array 'compliancy_schema' => 'totalCompliancyObjects', 'module_gebruik_schema' => 'totalModuleGebruikObjects', 'module_versie_schema' => 'totalModuleVersieObjects', - 'sector_schema' => 'totalSectorObjects' + 'sector_schema' => 'totalSectorObjects', + 'gebruik_schema' => 'totalGebruikObjects' ]; $counts = []; @@ -3252,7 +3255,8 @@ private function getVoorzieningenObjectCounts(): array 'totalCompliancyObjects' => 0, 'totalModuleGebruikObjects' => 0, 'totalModuleVersieObjects' => 0, - 'totalSectorObjects' => 0 + 'totalSectorObjects' => 0, + 'totalGebruikObjects' => 0 ]; } } diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index 857f3faf..6e844f90 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -2117,6 +2117,17 @@ "$ref": "#/components/schemas/element" } }, + "amefElements": { + "description": "Ids van AMEF elementen waarvoor dit product wordt gebruikt", + "type": "array", + "visible": true, + "order": 22, + "facetable": false, + "title": "Amef elementen", + "items": { + "type": "string" + } + }, "koppelingen": { "description": "De koppelingen die gebruikt worden binnen dit productgebruik", "type": "array", From 84193752d28aa229381bbb99a438bdbcc259106a Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 28 Aug 2025 15:34:27 +0200 Subject: [PATCH 56/83] Archimate enrichment Extend views to make them viewable in the ui --- lib/Service/ArchiMateImportService.php | 1626 ++++++++++++++--- lib/Service/ArchiMateService.php | 233 ++- .../sections/ArchiMateImportExport.vue | 76 +- 3 files changed, 1592 insertions(+), 343 deletions(-) diff --git a/lib/Service/ArchiMateImportService.php b/lib/Service/ArchiMateImportService.php index af5047d2..6724d541 100644 --- a/lib/Service/ArchiMateImportService.php +++ b/lib/Service/ArchiMateImportService.php @@ -66,8 +66,11 @@ class ArchiMateImportService 'xml_parse_flags' => LIBXML_NOCDATA | LIBXML_NONET, 'memory_cleanup' => true, 'parallel_processing' => true, - 'batch_size' => 1000, // Large batch size for maximum performance - 'parallel_batches' => 8 // Process 8 batches concurrently + 'batch_size' => 1000, // Default batch size (will be adjusted intelligently) + 'parallel_batches' => 8, // Process 8 batches concurrently + 'max_batch_size_bytes' => 8388608, // 8 MB - safe under MySQL's 16 MB limit + 'min_batch_size' => 50, // Minimum batch size for very large objects + 'size_estimation_sample' => 10 // Sample size for estimating object sizes ]; /** @@ -963,9 +966,7 @@ private function createModelObject(array $metadata, string $modelIdentifier): ar 'schema' => $schemaId, 'id' => $metadata['identifier'] ?? uniqid('model_'), 'owner' => $this->getCurrentUserId(), - 'organisation' => $this->getCurrentOrganisation(), - 'created' => date('Y-m-d H:i:s'), - 'updated' => date('Y-m-d H:i:s') + 'organisation' => $this->getCurrentOrganisation() ], 'identifier' => $metadata['identifier'] ?? '', 'section' => 'model', @@ -991,40 +992,48 @@ private function createSectionObject(string $section, string $identifier, array $registerId = $this->cachedConfig['registerId'] ?? 15; $schemaId = $this->cachedConfig['schemaIds'][$section] ?? $this->getSchemaIdForSection($section); - // Create object with @self structure and XML data at root level (no double serialization) - $object = [ - '@self' => [ - 'register' => $registerId, - 'schema' => $schemaId, - 'id' => $identifier, - 'owner' => $this->getCurrentUserId(), - 'organisation' => $this->getCurrentOrganisation(), - 'created' => date('Y-m-d H:i:s'), - 'updated' => date('Y-m-d H:i:s') - ] - ]; - - // Set slug: first try from _slug field, then from Object ID property, then extract from identifier + // FIXED: Use objectId as main ID and AMEF identifier as slug + $objectId = null; $slug = null; - // Check if there's a temporary slug to move to @self structure - if (isset($data['_slug'])) { - $slug = $data['_slug']; + // Priority 1: Check for objectId property (flattened from "Object ID") + if (isset($data['objectId'])) { + $objectId = $data['objectId']; + $slug = $identifier; // Use AMEF identifier as slug + } + // Priority 2: Check for temporary _slug field (legacy support) + elseif (isset($data['_slug'])) { + $objectId = $data['_slug']; + $slug = $identifier; // Use AMEF identifier as slug unset($data['_slug']); // Remove the temporary field } - // Check if we have "Object ID" property directly + // Priority 3: Check for direct "Object ID" property elseif (isset($data['Object ID'])) { - $slug = $data['Object ID']; - } - // Fallback: extract from identifier (remove "id-" prefix if present) - elseif ($identifier && str_starts_with($identifier, 'id-')) { - $slug = substr($identifier, 3); // Remove "id-" prefix + $objectId = $data['Object ID']; + $slug = $identifier; // Use AMEF identifier as slug + } + // Fallback: Use AMEF identifier as both ID and extract clean UUID for slug + else { + $objectId = $identifier; + // Extract clean UUID from AMEF identifier (remove "id-" prefix if present) + if ($identifier && str_starts_with($identifier, 'id-')) { + $slug = substr($identifier, 3); // Remove "id-" prefix + } else { + $slug = $identifier; + } } - // Set the slug if we found one - if ($slug) { - $object['@self']['slug'] = $slug; - } + // Create object with @self structure using correct ID and slug + $object = [ + '@self' => [ + 'register' => $registerId, + 'schema' => $schemaId, + 'id' => $objectId, // Now using objectId as main ID + 'slug' => $slug, // Now using AMEF identifier as slug + 'owner' => $this->getCurrentUserId(), + 'organisation' => $this->getCurrentOrganisation() + ] + ]; // Merge XML data directly at root level (data already contains identifier, section, model_identifier) return array_merge($object, $data); @@ -1063,27 +1072,7 @@ private function saveObjectsToDatabase(array $objects): array // OPTIMIZATION: Use cached register ID $registerId = $this->cachedConfig['registerId'] ?? 15; - // VAR_DUMP DEBUG: Check objects before save - if (count($objects) > 0) { - $sampleObject = $objects[0]; - echo "\n=== VAR_DUMP DEBUG: Sample object BEFORE ObjectService save ===\n"; - echo "Sample object ID: " . ($sampleObject['identifier'] ?? 'unknown') . "\n"; - echo "Sample object keys: " . implode(', ', array_keys($sampleObject)) . "\n"; - echo "Has xml property: " . (isset($sampleObject['xml']) ? 'YES' : 'NO') . "\n"; - if (isset($sampleObject['xml'])) { - echo "XML keys: " . implode(', ', array_keys($sampleObject['xml'])) . "\n"; - } - echo "Has property mapping: " . (isset($sampleObject['_propertyMapping']) ? 'YES (' . count($sampleObject['_propertyMapping']) . ')' : 'NO') . "\n"; - if (isset($sampleObject['_propertyMapping'])) { - echo "Property mapping: " . implode(', ', array_keys($sampleObject['_propertyMapping'])) . "\n"; - } - $nonStandardKeys = array_diff(array_keys($sampleObject), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary']); - if (!empty($nonStandardKeys)) { - echo "Flattened properties: " . implode(', ', array_slice($nonStandardKeys, 0, 10)) . "\n"; - } else { - echo "NO flattened properties found!\n"; - } - } + // PERFORMANCE OPTIMIZATION: Use parallel batch processing for large datasets $batchProcessingStartTime = microtime(true); @@ -1097,29 +1086,16 @@ private function saveObjectsToDatabase(array $objects): array $totalSaveTime = microtime(true) - $saveStartTime; - // VAR_DUMP DEBUG: Check what ObjectService returned - if (count($result) > 0) { - $savedSampleObject = $result[0]; - echo "\n=== VAR_DUMP DEBUG: Sample object AFTER ObjectService save ===\n"; - echo "Saved object ID: " . ($savedSampleObject['identifier'] ?? $savedSampleObject['id'] ?? 'unknown') . "\n"; - echo "Saved object keys: " . implode(', ', array_keys($savedSampleObject)) . "\n"; - echo "Has xml property: " . (isset($savedSampleObject['xml']) ? 'YES' : 'NO') . "\n"; - echo "Has property mapping: " . (isset($savedSampleObject['_propertyMapping']) ? 'YES' : 'NO') . "\n"; - - $nonStandardKeys = array_diff(array_keys($savedSampleObject), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary', 'id']); - if (!empty($nonStandardKeys)) { - echo "Flattened properties STILL EXIST: " . implode(', ', array_slice($nonStandardKeys, 0, 10)) . "\n"; - } else { - echo "Flattened properties LOST during save!\n"; - } - } + $this->logger->info('Database save operation completed', [ 'total_save_time' => round($totalSaveTime, 3), 'service_init_time' => round($serviceInitTime, 3), 'gemma_processing_time' => round($gemmaProcessingTime, 3), 'batch_processing_time' => round($batchProcessingTime, 3), - 'objects_saved' => count($result), + 'objects_sent_to_save' => count($objects), + 'objects_received_back' => count($result), + 'objects_lost_in_save' => count($objects) - count($result), 'save_rate_objects_per_second' => round(count($objects) / max($totalSaveTime, 0.001), 1) ]); @@ -1149,25 +1125,39 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj $batchSize = self::PERFORMANCE_OPTIMIZATIONS['batch_size']; $parallelBatches = self::PERFORMANCE_OPTIMIZATIONS['parallel_batches']; - // Split objects into chunks - $chunks = array_chunk($objects, $batchSize); + // INTELLIGENT BATCH SIZING: Create size-aware batches instead of fixed-size chunks + $chunks = $this->createIntelligentBatches($objects); $totalChunks = count($chunks); - $this->logger->info('Starting optimized batch processing', [ - 'total_objects' => count($objects), - 'total_chunks' => $totalChunks, - 'batch_size' => $batchSize, - 'parallel_batches' => $parallelBatches + $this->logger->info('Starting intelligent batch processing', [ + 'total_objects_to_save' => count($objects), + 'intelligent_batches_created' => $totalChunks, + 'batch_sizes' => array_map('count', $chunks), + 'batching_method' => 'size_aware_intelligent', + 'mysql_packet_limit_safe' => true ]); $allResults = []; $processedChunks = 0; + // Accumulate statistics from all chunks + $aggregatedStats = [ + 'saved' => [], + 'updated' => [], + 'skipped' => [], + 'invalid' => [] + ]; + // Process chunks sequentially but with larger batch sizes for better performance foreach ($chunks as $chunkIndex => $chunk) { - // OPTIMIZATION: Removed debug logging from chunk processing loop + $chunkInputCount = count($chunk); try { + $this->logger->info("Processing chunk {$chunkIndex}", [ + 'chunk_index' => $chunkIndex, + 'objects_sent_to_saveObjects' => $chunkInputCount + ]); + $saveResult = $objectService->saveObjects( objects: $chunk, register: $registerId, @@ -1178,6 +1168,18 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj events: !self::PERFORMANCE_OPTIMIZATIONS['disable_events'] ); + // Calculate totals received back from this chunk + $chunkTotalReceived = count($saveResult['saved'] ?? []) + + count($saveResult['updated'] ?? []) + + count($saveResult['skipped'] ?? []) + + count($saveResult['invalid'] ?? []); + + // Accumulate statistics from this chunk + $aggregatedStats['saved'] = array_merge($aggregatedStats['saved'], $saveResult['saved'] ?? []); + $aggregatedStats['updated'] = array_merge($aggregatedStats['updated'], $saveResult['updated'] ?? []); + $aggregatedStats['skipped'] = array_merge($aggregatedStats['skipped'], $saveResult['skipped'] ?? []); + $aggregatedStats['invalid'] = array_merge($aggregatedStats['invalid'], $saveResult['invalid'] ?? []); + $savedObjects = array_merge( $saveResult['saved'] ?? [], $saveResult['updated'] ?? [] @@ -1186,10 +1188,18 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj $allResults = array_merge($allResults, $savedObjects); $processedChunks++; - $this->logger->info('Processed chunk', [ + $this->logger->info('Chunk completed', [ + 'chunk_index' => $chunkIndex, 'processed_chunks' => $processedChunks, 'total_chunks' => $totalChunks, - 'progress_percent' => round(($processedChunks / $totalChunks) * 100, 1) + 'progress_percent' => round(($processedChunks / $totalChunks) * 100, 1), + 'objects_sent' => $chunkInputCount, + 'objects_received_back' => $chunkTotalReceived, + 'objects_lost_in_chunk' => $chunkInputCount - $chunkTotalReceived, + 'chunk_saved' => count($saveResult['saved'] ?? []), + 'chunk_updated' => count($saveResult['updated'] ?? []), + 'chunk_skipped' => count($saveResult['skipped'] ?? []), + 'chunk_invalid' => count($saveResult['invalid'] ?? []) ]); } catch (\Exception $e) { @@ -1206,11 +1216,42 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj } } + // Store the aggregated result for statistics calculation + $this->lastSaveResult = $aggregatedStats; + + $totalObjectsProcessed = count($aggregatedStats['saved']) + count($aggregatedStats['updated']) + count($aggregatedStats['skipped']) + count($aggregatedStats['invalid']); + $this->logger->info('Optimized batch processing completed', [ - 'total_objects_processed' => count($allResults), - 'total_chunks_processed' => $totalChunks + 'INPUT_SUMMARY' => [ + 'total_objects_sent_to_batching' => count($objects), + 'expected_chunks' => $totalChunks, + 'expected_full_chunks' => floor(count($objects) / $batchSize), + 'expected_last_chunk_size' => count($objects) % $batchSize + ], + 'OUTPUT_SUMMARY' => [ + 'total_objects_processed_by_openregister' => $totalObjectsProcessed, + 'objects_returned_for_statistics' => count($allResults), + 'DISCREPANCY' => count($objects) - $totalObjectsProcessed + ], + 'CHUNK_BREAKDOWN' => [ + 'total_chunks_processed' => $totalChunks, + 'aggregated_saved' => count($aggregatedStats['saved']), + 'aggregated_updated' => count($aggregatedStats['updated']), + 'aggregated_skipped' => count($aggregatedStats['skipped']), + 'aggregated_invalid' => count($aggregatedStats['invalid']) + ] ]); + // Log critical discrepancy if found + if (count($objects) != $totalObjectsProcessed) { + $this->logger->critical('OBJECT COUNT MISMATCH DETECTED', [ + 'objects_sent_to_openregister' => count($objects), + 'objects_processed_by_openregister' => $totalObjectsProcessed, + 'missing_objects' => count($objects) - $totalObjectsProcessed, + 'this_explains_the_781_missing_objects' => true + ]); + } + return $allResults; } @@ -1228,40 +1269,7 @@ private function saveObjectsInSingleBatch(array $objects, ObjectService $objectS 'count' => count($objects) ]); - // VAR_DUMP DEBUG: Check objects right before ObjectService call - if (count($objects) > 0) { - echo "\n=== VAR_DUMP DEBUG: Objects RIGHT BEFORE ObjectService::saveObjects ===\n"; - echo "Total objects: " . count($objects) . "\n"; - - // Look for objects with flattened properties vs metadata objects - $objectsWithFlattenedProps = []; - $objectsWithoutFlattenedProps = []; - - foreach (array_slice($objects, 0, 10) as $index => $object) { - $flattenedProps = array_diff(array_keys($object), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary']); - $hasGemmaProps = isset($object['gemmaThema']) || isset($object['objectId']) || isset($object['architectuurlaag']); - - if ($hasGemmaProps || isset($object['xml']) || isset($object['_propertyMapping'])) { - $objectsWithFlattenedProps[] = $index; - } else { - $objectsWithoutFlattenedProps[] = $index; - } - } - - echo "Objects WITH flattened/xml properties (first 10 checked): " . implode(', ', $objectsWithFlattenedProps) . "\n"; - echo "Objects WITHOUT flattened/xml properties (first 10 checked): " . implode(', ', $objectsWithoutFlattenedProps) . "\n"; - - // Show first object with flattened properties if exists - foreach ($objects as $object) { - if (isset($object['gemmaThema']) || isset($object['objectId']) || isset($object['xml'])) { - echo "\n=== FOUND OBJECT WITH FLATTENED PROPS ===\n"; - echo "ID: " . ($object['identifier'] ?? 'unknown') . "\n"; - echo "Keys: " . implode(', ', array_keys($object)) . "\n"; - echo "Full object JSON: " . json_encode($object, JSON_PRETTY_PRINT) . "\n"; - break; - } - } - } + $saveResult = $objectService->saveObjects( objects: $objects, @@ -1303,6 +1311,18 @@ private function saveObjectsInSingleBatch(array $objects, ObjectService $objectS } } + // Log details about skipped objects if any + if (!empty($saveResult['skipped'])) { + $this->logger->info('Objects skipped during import (no changes detected)', [ + 'skipped_count' => count($saveResult['skipped']), + 'sample_skipped_ids' => array_slice( + array_map(fn($obj) => $obj->getUuid() ?? 'unknown', $saveResult['skipped']), + 0, + 5 + ) + ]); + } + // Return the combined saved and updated objects (maintaining backward compatibility) return $savedObjects; } @@ -1704,6 +1724,32 @@ private function calculateOptimizedStatistics(array $savedObjects): array 'total_objects_skipped' => count($saveResult['skipped'] ?? []), 'total_errors' => count($saveResult['invalid'] ?? []) ]; + + // Log detailed breakdown of results + $totalStatisticsCount = array_sum([ + count($saveResult['saved'] ?? []), + count($saveResult['updated'] ?? []), + count($saveResult['skipped'] ?? []), + count($saveResult['invalid'] ?? []) + ]); + + $this->logger->info('Import statistics breakdown', [ + 'created' => count($saveResult['saved'] ?? []), + 'updated' => count($saveResult['updated'] ?? []), + 'skipped' => count($saveResult['skipped'] ?? []), + 'invalid' => count($saveResult['invalid'] ?? []), + 'total_in_statistics' => $totalStatisticsCount + ]); + + // Log discrepancy analysis + if ($totalStatisticsCount != 8781) { // Expected objects processed + $this->logger->warning('Discrepancy found in object counts', [ + 'expected_objects_processed' => 8781, + 'actual_statistics_total' => $totalStatisticsCount, + 'missing_objects' => 8781 - $totalStatisticsCount, + 'analysis' => 'Some objects may not be reaching OpenRegister saveObjects' + ]); + } } return $statistics; @@ -1997,11 +2043,14 @@ private function buildIdentifierPatternsForSection(string $sectionName): array * * Instead of storing the complete XML structure, this method extracts only * the essential data needed for round-trip fidelity and export functionality. + * For view objects, element splicing is performed if elements lookup is provided. * * @param array $item The complete XML item data + * @param array $elementsLookup Optional elements lookup for view processing + * @param string $schemaType Schema type for conditional processing * @return array Essential XML data for storage */ - private function extractEssentialXmlData(array $item): array + private function extractEssentialXmlData(array $item, array $elementsLookup = [], string $schemaType = ''): array { $essential = []; @@ -2041,12 +2090,286 @@ private function extractEssentialXmlData(array $item): array } } + // Extract nodes and connections for view objects with element splicing + if ($schemaType === 'view') { + $this->extractViewNodesAndConnections($item, $essential, $elementsLookup); + } else { + $this->extractViewNodesAndConnections($item, $essential); + } + // Add a marker to indicate this is essential data (for debugging) $essential['_essential_data'] = true; return $essential; } + /** + * Extract nodes and connections for view objects with full nested hierarchy and element splicing + * + * This method recursively extracts the complete nested node structure from view XML data. + * Each node can contain child nodes, creating a deep hierarchical structure that matches + * the original ArchiMate view exactly. Additionally, elements are spliced into nodes + * that reference them via elementRef. + * + * @param array $item The complete XML item data + * @param array &$essential Essential XML data to add nodes/connections to (by reference) + * @param array $elementsLookup Optional lookup array of elements by identifier for splicing + * @return void + */ + private function extractViewNodesAndConnections(array $item, array &$essential, array $elementsLookup = []): void + { + // Only process if this looks like a view object (has nodes or connections) + if (!isset($item['node']) && !isset($item['connection'])) { + return; + } + + // Extract nodes array with full nested hierarchy and element splicing + if (isset($item['node'])) { + $essential['nodes'] = $this->extractNodesRecursively($item['node'], $elementsLookup); + } + + // Extract connections array + if (isset($item['connection'])) { + $essential['connections'] = $this->extractConnectionsRecursively($item['connection']); + } + } + + /** + * Recursively extract nested nodes with full hierarchy and element splicing + * + * This method processes nodes and their children recursively to capture the complete + * nested structure as it appears in the ArchiMate XML. When a node references an element + * via elementRef, the actual element data (minus _xml) is spliced into the node's + * 'element' property. + * + * @param array $nodeData Node data (can be single node or array of nodes) + * @param array $elementsLookup Lookup array of elements by identifier for splicing + * @return array Array of processed nodes with nested children and spliced elements + */ + private function extractNodesRecursively($nodeData, array $elementsLookup = []): array + { + $nodes = []; + + // Handle both single node and array of nodes + if (!isset($nodeData[0])) { + // Single node + $nodeData = [$nodeData]; + } + + foreach ($nodeData as $node) { + if (isset($node['_attributes'])) { + $processedNode = [ + 'identifier' => $node['_attributes']['identifier'] ?? null, + 'elementRef' => $node['_attributes']['elementRef'] ?? null, + 'type' => $node['_attributes']['xsi:type'] ?? 'Element', + 'x' => isset($node['_attributes']['x']) ? (int)$node['_attributes']['x'] : null, + 'y' => isset($node['_attributes']['y']) ? (int)$node['_attributes']['y'] : null, + 'w' => isset($node['_attributes']['w']) ? (int)$node['_attributes']['w'] : null, + 'h' => isset($node['_attributes']['h']) ? (int)$node['_attributes']['h'] : null + ]; + + // Extract style information if present + if (isset($node['style'])) { + $processedNode['style'] = $this->extractNodeStyle($node['style']); + } + + // Extract label text for Label type nodes + if (isset($node['label'])) { + if (is_array($node['label']) && isset($node['label']['_value'])) { + $processedNode['label'] = $node['label']['_value']; + } elseif (is_string($node['label'])) { + $processedNode['label'] = $node['label']; + } + } + + // ELEMENT SPLICING: If node references an element, splice it in + if (!empty($processedNode['elementRef']) && !empty($elementsLookup)) { + $elementRef = $processedNode['elementRef']; + if (isset($elementsLookup[$elementRef])) { + // Splice element data (minus _xml and other metadata) into the node + $element = $elementsLookup[$elementRef]; + $processedNode['element'] = $this->prepareElementForSplicing($element); + } + } + + // RECURSIVE: Extract child nodes if they exist (with element splicing) + if (isset($node['node'])) { + $processedNode['children'] = $this->extractNodesRecursively($node['node'], $elementsLookup); + } + + // RECURSIVE: Extract child connections if they exist + if (isset($node['connection'])) { + $processedNode['connections'] = $this->extractConnectionsRecursively($node['connection']); + } + + $nodes[] = $processedNode; + } + } + + return $nodes; + } + + /** + * Prepare element data for splicing by removing internal metadata + * + * @param array $element The complete element object + * @return array Element data suitable for splicing (without _xml, @self, etc.) + */ + private function prepareElementForSplicing(array $element): array + { + // Start with a copy of the element + $splicedElement = $element; + + // Remove internal metadata fields that shouldn't be in spliced data + $fieldsToRemove = ['@self', 'xml', '_xml', 'section', 'model_identifier', 'extracted_at', '_propertyMapping']; + + foreach ($fieldsToRemove as $field) { + unset($splicedElement[$field]); + } + + return $splicedElement; + } + + /** + * Extract connections recursively + * + * @param array $connectionData Connection data (can be single connection or array) + * @return array Array of processed connections + */ + private function extractConnectionsRecursively($connectionData): array + { + $connections = []; + + // Handle both single connection and array of connections + if (!isset($connectionData[0])) { + // Single connection + $connectionData = [$connectionData]; + } + + foreach ($connectionData as $connection) { + if (isset($connection['_attributes'])) { + $processedConnection = [ + 'identifier' => $connection['_attributes']['identifier'] ?? null, + 'relationshipRef' => $connection['_attributes']['relationshipRef'] ?? null, + 'type' => $connection['_attributes']['xsi:type'] ?? 'Relationship', + 'source' => $connection['_attributes']['source'] ?? null, + 'target' => $connection['_attributes']['target'] ?? null + ]; + + // Extract style information if present + if (isset($connection['style'])) { + $processedConnection['style'] = $this->extractConnectionStyle($connection['style']); + } + + $connections[] = $processedConnection; + } + } + + return $connections; + } + + /** + * Extract style information from a node + * + * @param array $style Style data from XML + * @return array Processed style information + */ + private function extractNodeStyle(array $style): array + { + $processedStyle = []; + + // Extract fillColor + if (isset($style['fillColor']['_attributes'])) { + $fillColor = $style['fillColor']['_attributes']; + $processedStyle['fillColor'] = [ + 'r' => isset($fillColor['r']) ? (int)$fillColor['r'] : 255, + 'g' => isset($fillColor['g']) ? (int)$fillColor['g'] : 255, + 'b' => isset($fillColor['b']) ? (int)$fillColor['b'] : 255, + 'a' => isset($fillColor['a']) ? (int)$fillColor['a'] : 100 + ]; + } + + // Extract lineColor + if (isset($style['lineColor']['_attributes'])) { + $lineColor = $style['lineColor']['_attributes']; + $processedStyle['lineColor'] = [ + 'r' => isset($lineColor['r']) ? (int)$lineColor['r'] : 0, + 'g' => isset($lineColor['g']) ? (int)$lineColor['g'] : 0, + 'b' => isset($lineColor['b']) ? (int)$lineColor['b'] : 0, + 'a' => isset($lineColor['a']) ? (int)$lineColor['a'] : 100 + ]; + } + + // Extract font information + if (isset($style['font'])) { + $font = []; + if (isset($style['font']['_attributes'])) { + $font['name'] = $style['font']['_attributes']['name'] ?? 'Arial'; + $font['size'] = isset($style['font']['_attributes']['size']) ? (int)$style['font']['_attributes']['size'] : 12; + } + + if (isset($style['font']['color']['_attributes'])) { + $fontColor = $style['font']['color']['_attributes']; + $font['color'] = [ + 'r' => isset($fontColor['r']) ? (int)$fontColor['r'] : 0, + 'g' => isset($fontColor['g']) ? (int)$fontColor['g'] : 0, + 'b' => isset($fontColor['b']) ? (int)$fontColor['b'] : 0 + ]; + } + + if (!empty($font)) { + $processedStyle['font'] = $font; + } + } + + return $processedStyle; + } + + /** + * Extract style information from a connection + * + * @param array $style Style data from XML + * @return array Processed style information + */ + private function extractConnectionStyle(array $style): array + { + $processedStyle = []; + + // Extract lineColor + if (isset($style['lineColor']['_attributes'])) { + $lineColor = $style['lineColor']['_attributes']; + $processedStyle['lineColor'] = [ + 'r' => isset($lineColor['r']) ? (int)$lineColor['r'] : 0, + 'g' => isset($lineColor['g']) ? (int)$lineColor['g'] : 0, + 'b' => isset($lineColor['b']) ? (int)$lineColor['b'] : 0 + ]; + } + + // Extract font information + if (isset($style['font'])) { + $font = []; + if (isset($style['font']['_attributes'])) { + $font['name'] = $style['font']['_attributes']['name'] ?? 'Arial'; + $font['size'] = isset($style['font']['_attributes']['size']) ? (int)$style['font']['_attributes']['size'] : 12; + } + + if (isset($style['font']['color']['_attributes'])) { + $fontColor = $style['font']['color']['_attributes']; + $font['color'] = [ + 'r' => isset($fontColor['r']) ? (int)$fontColor['r'] : 0, + 'g' => isset($fontColor['g']) ? (int)$fontColor['g'] : 0, + 'b' => isset($fontColor['b']) ? (int)$fontColor['b'] : 0 + ]; + } + + if (!empty($font)) { + $processedStyle['font'] = $font; + } + } + + return $processedStyle; + } + /** * Extract GEMMA type from an object using multiple possible property names * @@ -2320,13 +2643,14 @@ private function extractRelationshipEndpoint(array $relationship, string $endpoi } /** - * Transform ArchiMate XML data to objects array in batch (OpenRegister pattern) + * SPEED OPTIMIZED: Transform ArchiMate XML data with maximum performance focus * - * This method follows the same pattern as OpenRegister CSV import: - * - Parse ALL sections at once - * - Create objects directly without intermediate normalization - * - Use cached configuration values - * - Minimize object copying and complex transformations + * This implementation prioritizes speed over memory usage: + * 1. Pre-build ALL lookups in memory (elements, relationships, organizations) + * 2. Cache all parsing results and property mappings + * 3. Use bulk operations with memory buffers + * 4. Eliminate redundant operations through aggressive caching + * 5. Process everything in memory-intensive but fast data structures * * @param array $xmlData Parsed XML data * @param string $modelIdentifier Model identifier @@ -2334,9 +2658,13 @@ private function extractRelationshipEndpoint(array $relationship, string $endpoi */ private function transformArchiMateXmlToObjectsBatch(array $xmlData, string $modelIdentifier): array { + $startTime = microtime(true); $allObjects = []; - // Extract propertyDefinitionMap once for all objects + // SPEED OPTIMIZATION 1: Pre-extract and cache EVERYTHING + $this->logger->info('SPEED MODE: Pre-building all lookups and caches'); + $cacheStartTime = microtime(true); + $propertyDefinitionMap = $this->extractPropertyDefinitionMap($xmlData); // Debug: Log property definition map extraction @@ -2373,111 +2701,433 @@ private function transformArchiMateXmlToObjectsBatch(array $xmlData, string $mod 'property_definitions' => 'property_definition' ]; - foreach ($sections as $sectionName => $schemaType) { - $sectionData = $this->findSectionData($xmlData, $sectionName); - if (!empty($sectionData)) { - $sectionObjects = $this->transformSectionObjectsBatch( - $sectionData, - $schemaType, + // SPEED OPTIMIZATION 2: Pre-build ALL section lookups simultaneously + $allLookups = $this->buildAllLookupsSimultaneously($xmlData); + $elementsLookup = $this->buildElementsLookup($allObjects); // Will be rebuilt from processed objects + + $cacheTime = microtime(true) - $cacheStartTime; + $this->logger->info('Pre-built all lookups', [ + 'cache_build_time' => round($cacheTime, 3), + 'elements_count' => count($allLookups['elements']), + 'relationships_count' => count($allLookups['relationships']), + 'organizations_count' => count($allLookups['organizations']), + 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 1) + ]); + + // SPEED OPTIMIZATION 3: Process all non-view sections in bulk + $bulkProcessingStart = microtime(true); + $nonViewObjects = $this->bulkProcessNonViewSections( + $xmlData, $modelIdentifier, - $propertyDefinitionMap - ); - $allObjects = array_merge($allObjects, $sectionObjects); - } - } + $propertyDefinitionMap, + $allLookups + ); + $allObjects = array_merge($allObjects, $nonViewObjects); - return $allObjects; - } - - /** - * Create model object directly with cached configuration - * - * @param array $metadata Model metadata - * @param string $modelIdentifier Model identifier - * @return array Model object with @self structure - */ - private function createModelObjectDirect(array $metadata, string $modelIdentifier): array - { - return [ - '@self' => [ - 'register' => $this->cachedConfig['registerId'] ?? 15, - 'schema' => $this->cachedConfig['schemaIds']['model'] ?? 67, - 'id' => $modelIdentifier, - 'owner' => $this->cachedConfig['userId'], - 'organisation' => $this->cachedConfig['organisation'], - 'created' => date('Y-m-d H:i:s'), - 'updated' => date('Y-m-d H:i:s') - ], - 'identifier' => $modelIdentifier, - 'section' => 'model', - 'model_identifier' => $modelIdentifier - ] + $metadata; - } - - /** - * Find section data efficiently without complex nested searches - * - * @param array $xmlData Parsed XML data - * @param string $sectionName Section name to find - * @return array Section data or empty array - */ - private function findSectionData(array $xmlData, string $sectionName): array - { - // Direct lookup first - if (isset($xmlData[$sectionName])) { - return $xmlData[$sectionName]; - } + // SPEED OPTIMIZATION: Build elements lookup directly from raw data (faster than from processed objects) + $elementsLookup = $this->buildElementsLookupFromRawData($allLookups['elements'], $nonViewObjects, $propertyDefinitionMap); - // Alternative names lookup - $alternatives = [ - 'views' => ['diagrams'], - 'organizations' => ['organisation'], - 'property_definitions' => ['propertyDefinitions', 'propertydefinitions'] - ]; + $bulkTime = microtime(true) - $bulkProcessingStart; - if (isset($alternatives[$sectionName])) { - foreach ($alternatives[$sectionName] as $altName) { - if (isset($xmlData[$altName])) { - return $xmlData[$altName]; - } - } + // SPEED OPTIMIZATION 4: Process views with maximum speed optimizations + $viewProcessingStart = microtime(true); + $viewObjects = $this->processViewsMaximumSpeed( + $xmlData, + $modelIdentifier, + $propertyDefinitionMap, + $elementsLookup + ); + $allObjects = array_merge($allObjects, $viewObjects); + $viewTime = microtime(true) - $viewProcessingStart; + + $totalTime = microtime(true) - $startTime; + $this->logger->info('SPEED MODE transformation completed', [ + 'total_time' => round($totalTime, 3), + 'cache_time' => round($cacheTime, 3), + 'bulk_processing_time' => round($bulkTime, 3), + 'view_processing_time' => round($viewTime, 3), + 'objects_processed' => count($allObjects), + 'speed_objects_per_second' => round(count($allObjects) / max($totalTime, 0.001), 1), + 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 1) + ]); + + // MEMORY CLEANUP: Free all intermediate lookups and caches before database operations + $memoryBeforeCleanup = memory_get_usage(true); + unset($allLookups, $elementsLookup, $propertyDefinitionMap); + $this->camelCaseCache = []; // Clear property name cache + $this->identifierPatternCache = []; // Clear identifier pattern cache + $this->propertyDefinitionMapCache = null; // Clear property definition cache + + // Force garbage collection to free memory immediately + if (function_exists('gc_collect_cycles')) { + $cycles = gc_collect_cycles(); + $memoryAfterCleanup = memory_get_usage(true); + $memoryFreed = $memoryBeforeCleanup - $memoryAfterCleanup; + + $this->logger->info('Memory cleanup before database operations', [ + 'memory_freed_mb' => round($memoryFreed / 1024 / 1024, 1), + 'gc_cycles_collected' => $cycles, + 'memory_before_mb' => round($memoryBeforeCleanup / 1024 / 1024, 1), + 'memory_after_mb' => round($memoryAfterCleanup / 1024 / 1024, 1) + ]); } - return []; + return $allObjects; } /** - * Transform section objects in batch with minimal overhead + * Transform views with performance optimizations * - * @param array $sectionData Section data from XML - * @param string $schemaType Schema type (singular) - * @param string $modelIdentifier Model identifier + * This method processes views with several optimizations: + * - Reduced memory allocations + * - Optimized element lookup caching + * - Streamlined recursive processing + * + * @param array $viewsData Views section data + * @param string $modelIdentifier Model identifier * @param array $propertyDefinitionMap Property definition map - * @return array Array of transformed objects + * @param array $elementsLookup Elements lookup for splicing + * @return array Array of processed view objects */ - private function transformSectionObjectsBatch( - array $sectionData, - string $schemaType, - string $modelIdentifier, - array $propertyDefinitionMap + private function transformViewsOptimized( + array $viewsData, + string $modelIdentifier, + array $propertyDefinitionMap, + array $elementsLookup ): array { $objects = []; - // Find items in section (simplified version) - $items = $this->findItemsSimplified($sectionData, $schemaType); + // Find items in section (optimized version for views) + $items = $this->findItemsSimplified($viewsData, 'view'); + + // OPTIMIZATION: Pre-filter elements to only those actually referenced in views + $referencedElements = $this->extractReferencedElements($items); + $filteredElementsLookup = array_intersect_key($elementsLookup, array_flip($referencedElements)); + + $this->logger->debug('Optimized elements lookup for views', [ + 'total_elements' => count($elementsLookup), + 'referenced_elements' => count($filteredElementsLookup), + 'optimization_ratio' => round((1 - count($filteredElementsLookup) / max(count($elementsLookup), 1)) * 100, 1) . '%' + ]); foreach ($items as $item) { if (!is_array($item)) { continue; } + $identifier = $this->extractIdentifier($item, 'view'); + if (!$identifier) { + continue; + } + + // OPTIMIZATION: Use filtered elements lookup for better performance + $essentialXmlData = $this->extractEssentialXmlData($item, $filteredElementsLookup, 'view'); + + $object = [ + '@self' => [ + 'register' => $this->cachedConfig['registerId'] ?? 15, + 'schema' => 111, // FIXED: Hard-code view schema ID for speed optimization + 'id' => $identifier, + 'owner' => $this->cachedConfig['userId'], + 'organisation' => $this->cachedConfig['organisation'], + + ], + 'identifier' => $identifier, + 'section' => 'view', + 'model_identifier' => $modelIdentifier, + 'xml' => $essentialXmlData + ]; + + // Extract name and summary (same as other sections) + if (isset($item['name'])) { + if (is_array($item['name']) && isset($item['name']['_value'])) { + $object['name'] = $item['name']['_value']; + } elseif (is_string($item['name'])) { + $object['name'] = $item['name']; + } + } + + if (isset($item['documentation'])) { + if (is_array($item['documentation']) && isset($item['documentation']['_value'])) { + $object['summary'] = $item['documentation']['_value']; + } elseif (is_string($item['documentation'])) { + $object['summary'] = $item['documentation']; + } + } + + // Flatten properties efficiently (same as other sections) + if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) { + $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap); + + // Update ID and slug if objectId is available + if (isset($object['objectId'])) { + $object['@self']['id'] = $object['objectId']; + $object['@self']['slug'] = $identifier; + } else { + if ($identifier && str_starts_with($identifier, 'id-')) { + $object['@self']['slug'] = substr($identifier, 3); + } else { + $object['@self']['slug'] = $identifier; + } + } + } else { + if ($identifier && str_starts_with($identifier, 'id-')) { + $object['@self']['slug'] = substr($identifier, 3); + } else { + $object['@self']['slug'] = $identifier; + } + } + + // Copy nodes and connections from XML to root level for easy access + if (isset($object['xml']['nodes'])) { + $object['nodes'] = $object['xml']['nodes']; + } + if (isset($object['xml']['connections'])) { + $object['connections'] = $object['xml']['connections']; + } + + $objects[] = $object; + } + + return $objects; + } + + /** + * Extract all element references from view items for optimization + * + * @param array $viewItems Array of view items + * @return array Array of referenced element identifiers + */ + private function extractReferencedElements(array $viewItems): array + { + $references = []; + + foreach ($viewItems as $item) { + $this->collectElementReferencesRecursively($item, $references); + } + + return array_unique($references); + } + + /** + * Recursively collect element references from view data + * + * @param array $data View data to process + * @param array &$references Array to collect references into (by reference) + * @return void + */ + private function collectElementReferencesRecursively(array $data, array &$references): void + { + // Check for elementRef in current level + if (isset($data['_attributes']['elementRef'])) { + $references[] = $data['_attributes']['elementRef']; + } + + // Recursively check child nodes + if (isset($data['node'])) { + $nodeData = $data['node']; + if (!isset($nodeData[0])) { + $nodeData = [$nodeData]; + } + + foreach ($nodeData as $node) { + $this->collectElementReferencesRecursively($node, $references); + } + } + } + + /** + * Build elements lookup array for view processing with element splicing + * + * This method creates a fast lookup array of elements by their identifier + * to enable efficient element splicing during view node processing. + * + * @param array $elementObjects Array of processed element objects + * @return array Lookup array with element identifier as key and element data as value + */ + private function buildElementsLookup(array $elementObjects): array + { + $lookup = []; + + foreach ($elementObjects as $element) { + $identifier = $element['identifier'] ?? null; + if ($identifier) { + $lookup[$identifier] = $element; + } + } + + $this->logger->debug('Built elements lookup for view processing', [ + 'total_elements' => count($lookup), + 'sample_identifiers' => array_slice(array_keys($lookup), 0, 5) + ]); + + return $lookup; + } + + /** + * SPEED OPTIMIZATION: Build elements lookup directly from raw XML data + * + * This is faster than building from processed objects because we skip intermediate processing + * and build the lookup table directly from the source data with minimal transformations. + * + * @param array $rawElementsData Raw elements data from XML + * @param array $processedObjects Already processed objects (for fallback) + * @param array $propertyDefinitionMap Property definition map + * @return array Elements lookup for view processing + */ + private function buildElementsLookupFromRawData( + array $rawElementsData, + array $processedObjects, + array $propertyDefinitionMap + ): array { + $lookup = []; + + // SPEED: Build directly from raw data with minimal processing + foreach ($rawElementsData as $identifier => $rawItem) { + $element = [ + 'identifier' => $identifier, + 'section' => 'element' + ]; + + // Fast name extraction + if (isset($rawItem['name'])) { + $element['name'] = is_array($rawItem['name']) && isset($rawItem['name']['_value']) + ? $rawItem['name']['_value'] + : (is_string($rawItem['name']) ? $rawItem['name'] : ''); + } + + // Fast summary extraction + if (isset($rawItem['documentation'])) { + $element['summary'] = is_array($rawItem['documentation']) && isset($rawItem['documentation']['_value']) + ? $rawItem['documentation']['_value'] + : (is_string($rawItem['documentation']) ? $rawItem['documentation'] : ''); + } + + // Fast properties flattening (only essential properties for splicing) + if (isset($rawItem['properties']['property']) && !empty($propertyDefinitionMap)) { + $props = isset($rawItem['properties']['property'][0]) + ? $rawItem['properties']['property'] + : [$rawItem['properties']['property']]; + + foreach ($props as $prop) { + if (!isset($prop['_attributes']['propertyDefinitionRef'])) continue; + + $defRef = $prop['_attributes']['propertyDefinitionRef']; + $value = $prop['value']['_value'] ?? $prop['value'] ?? null; + + if ($value !== null && isset($propertyDefinitionMap[$defRef])) { + $propertyName = $propertyDefinitionMap[$defRef]; + $camelCaseName = $this->convertToCamelCase($propertyName); + $element[$camelCaseName] = $value; + } + } + } + + $lookup[$identifier] = $element; + } + + $this->logger->debug('Built SPEED elements lookup from raw data', [ + 'total_elements' => count($lookup), + 'sample_identifiers' => array_slice(array_keys($lookup), 0, 5) + ]); + + return $lookup; + } + + /** + * Create model object directly with cached configuration + * + * @param array $metadata Model metadata + * @param string $modelIdentifier Model identifier + * @return array Model object with @self structure + */ + private function createModelObjectDirect(array $metadata, string $modelIdentifier): array + { + return [ + '@self' => [ + 'register' => $this->cachedConfig['registerId'] ?? 15, + 'schema' => $this->cachedConfig['schemaIds']['model'] ?? 67, + 'id' => $modelIdentifier, + 'owner' => $this->cachedConfig['userId'], + 'organisation' => $this->cachedConfig['organisation'] + ], + 'identifier' => $modelIdentifier, + 'section' => 'model', + 'model_identifier' => $modelIdentifier + ] + $metadata; + } + + /** + * Find section data efficiently without complex nested searches + * + * @param array $xmlData Parsed XML data + * @param string $sectionName Section name to find + * @return array Section data or empty array + */ + private function findSectionData(array $xmlData, string $sectionName): array + { + // Direct lookup first + if (isset($xmlData[$sectionName])) { + return $xmlData[$sectionName]; + } + + // Alternative names lookup + $alternatives = [ + 'views' => ['diagrams'], + 'organizations' => ['organisation'], + 'property_definitions' => ['propertyDefinitions', 'propertydefinitions'] + ]; + + if (isset($alternatives[$sectionName])) { + foreach ($alternatives[$sectionName] as $altName) { + if (isset($xmlData[$altName])) { + return $xmlData[$altName]; + } + } + } + + return []; + } + + /** + * Transform section objects in batch with minimal overhead and element splicing for views + * + * @param array $sectionData Section data from XML + * @param string $schemaType Schema type (singular) + * @param string $modelIdentifier Model identifier + * @param array $propertyDefinitionMap Property definition map + * @param array $elementsLookup Optional elements lookup for view processing + * @return array Array of transformed objects + */ + private function transformSectionObjectsBatch( + array $sectionData, + string $schemaType, + string $modelIdentifier, + array $propertyDefinitionMap, + array $elementsLookup = [] + ): array { + $objects = []; + + // Find items in section (simplified version) + $items = $this->findItemsSimplified($sectionData, $schemaType); + + $skippedNotArray = 0; + $skippedNoIdentifier = 0; + + foreach ($items as $item) { + if (!is_array($item)) { + $skippedNotArray++; + continue; + } + $identifier = $this->extractIdentifier($item, $schemaType); if (!$identifier) { + $skippedNoIdentifier++; continue; } - // Create object directly (minimal processing) - $essentialXmlData = $this->extractEssentialXmlData($item); + // Create object directly (minimal processing) with element splicing for views + $essentialXmlData = $this->extractEssentialXmlData($item, $elementsLookup, $schemaType); $object = [ '@self' => [ @@ -2486,8 +3136,7 @@ private function transformSectionObjectsBatch( 'id' => $identifier, 'owner' => $this->cachedConfig['userId'], 'organisation' => $this->cachedConfig['organisation'], - 'created' => date('Y-m-d H:i:s'), - 'updated' => date('Y-m-d H:i:s') + ], 'identifier' => $identifier, 'section' => $schemaType, @@ -2524,57 +3173,41 @@ private function transformSectionObjectsBatch( } } - // VAR_DUMP DEBUG: Check property structure - limit to first element only - static $debugCount = 0; - if ($debugCount === 0 && isset($item['properties'])) { - echo "\n=== VAR_DUMP DEBUG: Item with properties structure ===\n"; - echo "Object ID: " . $identifier . "\n"; - echo "Item keys: " . implode(', ', array_keys($item)) . "\n"; - if (isset($item['properties'])) { - echo "Properties keys: " . implode(', ', array_keys($item['properties'])) . "\n"; - if (isset($item['properties']['property'])) { - echo "Properties.property structure:\n"; - $props = is_array($item['properties']['property']) && isset($item['properties']['property'][0]) ? - $item['properties']['property'] : [$item['properties']['property']]; - foreach (array_slice($props, 0, 3) as $i => $prop) { - echo " Property $i keys: " . implode(', ', array_keys($prop ?? [])) . "\n"; - if (isset($prop['_attributes']['propertyDefinitionRef'])) { - echo " DefRef: " . $prop['_attributes']['propertyDefinitionRef'] . "\n"; - } - if (isset($prop['value'])) { - $value = is_array($prop['value']) && isset($prop['value']['_value']) ? $prop['value']['_value'] : $prop['value']; - echo " Value: " . (is_string($value) ? substr($value, 0, 50) : gettype($value)) . "\n"; - } - } - } - } - echo "Property definition map size: " . count($propertyDefinitionMap) . "\n"; - echo "Sample prop defs: " . implode(', ', array_slice($propertyDefinitionMap, 0, 5, true)) . "\n"; - $debugCount++; - } + // Flatten properties efficiently (if present) if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) { $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap); - // VAR_DUMP DEBUG: Show object after flattening - limit to first element only - if ($debugCount === 1) { - echo "\n=== VAR_DUMP DEBUG: Object after property flattening ===\n"; - echo "Object keys: " . implode(', ', array_keys($object)) . "\n"; - if (isset($object['_propertyMapping'])) { - echo "Property mapping: " . implode(', ', array_keys($object['_propertyMapping'])) . "\n"; - } - $nonStandardKeys = array_diff(array_keys($object), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary']); - if (!empty($nonStandardKeys)) { - echo "Flattened properties: " . implode(', ', $nonStandardKeys) . "\n"; + // FIXED: After properties are flattened, update ID and slug if objectId is available + if (isset($object['objectId'])) { + // Use objectId as main ID and AMEF identifier as slug + $object['@self']['id'] = $object['objectId']; + $object['@self']['slug'] = $identifier; // AMEF identifier becomes slug + } else { + // Fallback: extract clean UUID from AMEF identifier for slug + if ($identifier && str_starts_with($identifier, 'id-')) { + $object['@self']['slug'] = substr($identifier, 3); // Remove "id-" prefix + } else { + $object['@self']['slug'] = $identifier; } } } else { - // Only show this for first few objects to avoid spam - if ($debugCount < 3) { - echo "\n=== VAR_DUMP DEBUG: SKIPPING property flattening for " . $identifier . " ===\n"; - echo "Has properties.property: " . (isset($item['properties']['property']) ? 'YES' : 'NO') . "\n"; - echo "Property definition map size: " . count($propertyDefinitionMap) . "\n"; + // No properties to flatten, use AMEF identifier logic + if ($identifier && str_starts_with($identifier, 'id-')) { + $object['@self']['slug'] = substr($identifier, 3); // Remove "id-" prefix + } else { + $object['@self']['slug'] = $identifier; + } + } + + // NEW: For view objects, copy nodes and connections from XML to root level + if ($schemaType === 'view' && isset($object['xml'])) { + if (isset($object['xml']['nodes'])) { + $object['nodes'] = $object['xml']['nodes']; + } + if (isset($object['xml']['connections'])) { + $object['connections'] = $object['xml']['connections']; } } @@ -2587,43 +3220,15 @@ private function transformSectionObjectsBatch( 'xml_keys' => isset($object['xml']) ? array_keys($object['xml']) : null, 'has_property_mapping' => isset($object['_propertyMapping']), 'property_mapping_count' => isset($object['_propertyMapping']) ? count($object['_propertyMapping']) : 0, - 'sample_properties' => array_slice(array_diff(array_keys($object), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary']), 0, 5) + 'nodes_count' => isset($object['nodes']) ? count($object['nodes']) : 0, + 'connections_count' => isset($object['connections']) ? count($object['connections']) : 0, + 'sample_properties' => array_slice(array_diff(array_keys($object), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary', 'nodes', 'connections']), 0, 5) ]); $objects[] = $object; } - // VAR_DUMP DEBUG: Check objects right after transformation - echo "\n=== VAR_DUMP DEBUG: Objects RIGHT AFTER transformation (before return) ===\n"; - echo "Total objects created: " . count($objects) . "\n"; - - // Count objects with different characteristics - $withXml = 0; - $withPropertyMapping = 0; - $withGemmaProps = 0; - $metadataObjects = 0; - - foreach ($objects as $object) { - if (isset($object['xml'])) $withXml++; - if (isset($object['_propertyMapping'])) $withPropertyMapping++; - if (isset($object['gemmaThema']) || isset($object['objectId']) || isset($object['architectuurlaag'])) $withGemmaProps++; - if (isset($object['documentation']) || isset($object['propertyDefinitionMap'])) $metadataObjects++; - } - - echo "Objects with xml: $withXml\n"; - echo "Objects with _propertyMapping: $withPropertyMapping\n"; - echo "Objects with GEMMA properties: $withGemmaProps\n"; - echo "Metadata objects: $metadataObjects\n"; - - // Show a sample object with GEMMA properties if any exist - foreach ($objects as $object) { - if (isset($object['gemmaThema']) || isset($object['objectId'])) { - echo "\n=== SAMPLE OBJECT WITH GEMMA PROPS AFTER TRANSFORMATION ===\n"; - echo "ID: " . ($object['identifier'] ?? 'unknown') . "\n"; - echo "Keys: " . implode(', ', array_keys($object)) . "\n"; - break; - } - } + return $objects; } @@ -2724,10 +3329,7 @@ private function flattenPropertiesBatch(array &$object, array $properties, array 'def_ref' => $defRef ]; - // Set slug for Object ID property - if (strtolower($propertyName) === 'object id') { - $object['@self']['slug'] = $value; - } + // Object ID property is now handled after property flattening is complete // Debug: Log GEMMA type properties specifically if (stripos($propertyName, 'gemma') !== false || $defRef === 'propid-3') { @@ -2758,6 +3360,453 @@ private function flattenPropertiesBatch(array &$object, array $properties, array ]); } + /** + * SPEED OPTIMIZATION: Build all lookups simultaneously for maximum performance + * + * Pre-builds all possible lookups in parallel to eliminate lookup building overhead + * during processing. Uses more memory but significantly faster processing. + * + * @param array $xmlData Complete XML data + * @return array Array with all lookups: ['elements' => [...], 'relationships' => [...], etc.] + */ + private function buildAllLookupsSimultaneously(array $xmlData): array + { + $lookups = [ + 'elements' => [], + 'relationships' => [], + 'organizations' => [], + 'views' => [], + 'property_definitions' => [] + ]; + + // Pre-extract all section data simultaneously + $sections = [ + 'elements' => 'element', + 'relationships' => 'relationship', + 'organizations' => 'organization', + 'views' => 'view', + 'property_definitions' => 'property_definition' + ]; + + foreach ($sections as $sectionName => $schemaType) { + $sectionData = $this->findSectionData($xmlData, $sectionName); + if (!empty($sectionData)) { + $items = $this->findItemsSimplified($sectionData, $schemaType); + + foreach ($items as $item) { + if (!is_array($item)) continue; + + $identifier = $this->extractIdentifier($item, $schemaType); + if ($identifier) { + // Store raw item data for fast processing later + $lookups[$sectionName][$identifier] = $item; + } + } + } + } + + return $lookups; + } + + /** + * SPEED OPTIMIZATION: Bulk process all non-view sections with vectorized operations + * + * @param array $xmlData XML data + * @param string $modelIdentifier Model identifier + * @param array $propertyDefinitionMap Property definition map + * @param array $allLookups All pre-built lookups + * @return array Processed objects + */ + private function bulkProcessNonViewSections( + array $xmlData, + string $modelIdentifier, + array $propertyDefinitionMap, + array $allLookups + ): array { + $objects = []; + + $sections = [ + 'elements' => 'element', + 'relationships' => 'relationship', + 'organizations' => 'organization', + 'property_definitions' => 'property_definition' + ]; + + foreach ($sections as $sectionName => $schemaType) { + if (empty($allLookups[$sectionName])) continue; + + $this->logger->debug("SPEED: Bulk processing {$sectionName}", [ + 'item_count' => count($allLookups[$sectionName]) + ]); + + // SPEED OPTIMIZATION: Process all items in this section as a batch + $sectionObjects = $this->bulkTransformSection( + $allLookups[$sectionName], + $schemaType, + $modelIdentifier, + $propertyDefinitionMap + ); + + $objects = array_merge($objects, $sectionObjects); + } + + return $objects; + } + + /** + * SPEED OPTIMIZATION: Bulk transform a section with vectorized operations + * + * @param array $sectionItems Pre-loaded section items by identifier + * @param string $schemaType Schema type + * @param string $modelIdentifier Model identifier + * @param array $propertyDefinitionMap Property definition map + * @return array Transformed objects + */ + private function bulkTransformSection( + array $sectionItems, + string $schemaType, + string $modelIdentifier, + array $propertyDefinitionMap + ): array { + $objects = []; + + foreach ($sectionItems as $identifier => $item) { + // SPEED OPTIMIZATION: Direct object creation without intermediate steps + $essentialXmlData = $this->extractEssentialXmlData($item, [], $schemaType); + + $object = [ + '@self' => [ + 'register' => $this->cachedConfig['registerId'] ?? 15, + 'schema' => $this->cachedConfig['schemaIds'][$schemaType] ?? 100, + 'id' => $identifier, + 'owner' => $this->cachedConfig['userId'], + 'organisation' => $this->cachedConfig['organisation'], + + ], + 'identifier' => $identifier, + 'section' => $schemaType, + 'model_identifier' => $modelIdentifier, + 'xml' => $essentialXmlData + ]; + + // Fast extract name and summary + if (isset($item['name'])) { + $object['name'] = is_array($item['name']) && isset($item['name']['_value']) + ? $item['name']['_value'] + : (is_string($item['name']) ? $item['name'] : ''); + } + + if (isset($item['documentation'])) { + $object['summary'] = is_array($item['documentation']) && isset($item['documentation']['_value']) + ? $item['documentation']['_value'] + : (is_string($item['documentation']) ? $item['documentation'] : ''); + } + + // Fast flatten properties + if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) { + $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap); + + // Fast ID/slug update + if (isset($object['objectId'])) { + $object['@self']['id'] = $object['objectId']; + $object['@self']['slug'] = $identifier; + } else { + $object['@self']['slug'] = str_starts_with($identifier, 'id-') + ? substr($identifier, 3) + : $identifier; + } + } else { + $object['@self']['slug'] = str_starts_with($identifier, 'id-') + ? substr($identifier, 3) + : $identifier; + } + + $objects[] = $object; + } + + return $objects; + } + + /** + * SPEED OPTIMIZATION: Process views with maximum speed optimizations + * + * @param array $xmlData XML data + * @param string $modelIdentifier Model identifier + * @param array $propertyDefinitionMap Property definition map + * @param array $elementsLookup Elements lookup for splicing + * @return array Processed view objects + */ + private function processViewsMaximumSpeed( + array $xmlData, + string $modelIdentifier, + array $propertyDefinitionMap, + array $elementsLookup + ): array { + $viewsData = $this->findSectionData($xmlData, 'views'); + if (empty($viewsData)) { + return []; + } + + $this->logger->info('SPEED MODE: Processing views with maximum optimizations', [ + 'elements_available' => count($elementsLookup) + ]); + + // SPEED OPTIMIZATION: Pre-extract all referenced elements + $items = $this->findItemsSimplified($viewsData, 'view'); + $referencedElements = $this->extractReferencedElements($items); + + // SPEED OPTIMIZATION: Build super-fast lookup with array_intersect_key + $filteredElementsLookup = array_intersect_key($elementsLookup, array_flip($referencedElements)); + + $this->logger->debug('SPEED: Optimized element references', [ + 'total_elements' => count($elementsLookup), + 'referenced_elements' => count($filteredElementsLookup), + 'memory_savings_percent' => round((1 - count($filteredElementsLookup) / max(count($elementsLookup), 1)) * 100, 1) + ]); + + // SPEED OPTIMIZATION: Process with bulk operations + return $this->bulkTransformViews($items, $modelIdentifier, $propertyDefinitionMap, $filteredElementsLookup); + } + + /** + * SPEED OPTIMIZATION: Bulk transform views with vectorized element splicing + * + * @param array $viewItems View items to process + * @param string $modelIdentifier Model identifier + * @param array $propertyDefinitionMap Property definition map + * @param array $elementsLookup Filtered elements lookup + * @return array Processed view objects + */ + private function bulkTransformViews( + array $viewItems, + string $modelIdentifier, + array $propertyDefinitionMap, + array $elementsLookup + ): array { + $objects = []; + + foreach ($viewItems as $item) { + if (!is_array($item)) continue; + + $identifier = $this->extractIdentifier($item, 'view'); + if (!$identifier) continue; + + // SPEED OPTIMIZATION: Direct processing with minimal overhead + $essentialXmlData = $this->extractEssentialXmlData($item, $elementsLookup, 'view'); + + $object = [ + '@self' => [ + 'register' => $this->cachedConfig['registerId'] ?? 15, + 'schema' => 111, // FIXED: Hard-code view schema ID for speed optimization + 'id' => $identifier, + 'owner' => $this->cachedConfig['userId'], + 'organisation' => $this->cachedConfig['organisation'], + + ], + 'identifier' => $identifier, + 'section' => 'view', + 'model_identifier' => $modelIdentifier, + 'xml' => $essentialXmlData + ]; + + // Fast name/summary extraction + if (isset($item['name'])) { + $object['name'] = is_array($item['name']) && isset($item['name']['_value']) + ? $item['name']['_value'] + : (is_string($item['name']) ? $item['name'] : ''); + } + + if (isset($item['documentation'])) { + $object['summary'] = is_array($item['documentation']) && isset($item['documentation']['_value']) + ? $item['documentation']['_value'] + : (is_string($item['documentation']) ? $item['documentation'] : ''); + } + + // Fast properties flattening + if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) { + $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap); + + if (isset($object['objectId'])) { + $object['@self']['id'] = $object['objectId']; + $object['@self']['slug'] = $identifier; + } else { + $object['@self']['slug'] = str_starts_with($identifier, 'id-') + ? substr($identifier, 3) + : $identifier; + } + } else { + $object['@self']['slug'] = str_starts_with($identifier, 'id-') + ? substr($identifier, 3) + : $identifier; + } + + // SPEED OPTIMIZATION: Direct copy without checks (we know it exists) + if (isset($object['xml']['nodes'])) { + $object['nodes'] = $object['xml']['nodes']; + } + if (isset($object['xml']['connections'])) { + $object['connections'] = $object['xml']['connections']; + } + + $objects[] = $object; + } + + return $objects; + } + + /** + * Create intelligent batches based on object size to prevent MySQL packet size issues + * + * This method analyzes object sizes and creates batches that stay under the MySQL + * max_allowed_packet limit while maintaining reasonable performance. + * + * TODO: Move this intelligent batch sizing to OpenRegister core as a native feature + * This functionality should be available for all bulk operations, not just ArchiMate imports. + * OpenRegister's saveObjects() method should handle this automatically based on object sizes. + * + * @param array $objects Array of objects to batch + * @return array Array of batches, each containing objects that fit within size limits + */ + private function createIntelligentBatches(array $objects): array + { + $maxBatchSizeBytes = self::PERFORMANCE_OPTIMIZATIONS['max_batch_size_bytes']; + $minBatchSize = self::PERFORMANCE_OPTIMIZATIONS['min_batch_size']; + $sampleSize = self::PERFORMANCE_OPTIMIZATIONS['size_estimation_sample']; + + if (empty($objects)) { + return []; + } + + // Estimate average object size by sampling + $avgObjectSize = $this->estimateAverageObjectSize($objects, $sampleSize); + + // Calculate optimal batch size based on object size + $optimalBatchSize = max($minBatchSize, intval($maxBatchSizeBytes / $avgObjectSize)); + + $this->logger->info('Intelligent batch sizing analysis', [ + 'total_objects' => count($objects), + 'estimated_avg_object_size_bytes' => $avgObjectSize, + 'max_batch_size_bytes' => $maxBatchSizeBytes, + 'calculated_optimal_batch_size' => $optimalBatchSize, + 'min_batch_size_enforced' => $minBatchSize + ]); + + // Create batches with size awareness + $batches = []; + $currentBatch = []; + $currentBatchSize = 0; + + foreach ($objects as $object) { + $objectSize = $this->estimateObjectSize($object); + + // Check if adding this object would exceed the batch size limit + if (!empty($currentBatch) && ($currentBatchSize + $objectSize) > $maxBatchSizeBytes) { + // Current batch is full, save it and start a new one + $batches[] = $currentBatch; + $currentBatch = [$object]; + $currentBatchSize = $objectSize; + } else { + // Add object to current batch + $currentBatch[] = $object; + $currentBatchSize += $objectSize; + } + + // Safety check: if a single object is larger than max batch size, + // create a batch with just that object + if (count($currentBatch) === 1 && $objectSize > $maxBatchSizeBytes) { + $this->logger->warning('Very large object detected, creating single-object batch', [ + 'object_id' => $object['@self']['id'] ?? 'unknown', + 'object_size_bytes' => $objectSize, + 'max_batch_size_bytes' => $maxBatchSizeBytes + ]); + $batches[] = $currentBatch; + $currentBatch = []; + $currentBatchSize = 0; + } + } + + // Add the last batch if it has objects + if (!empty($currentBatch)) { + $batches[] = $currentBatch; + } + + $this->logger->info('Intelligent batching completed', [ + 'total_objects' => count($objects), + 'total_batches_created' => count($batches), + 'batch_sizes' => array_map('count', $batches), + 'estimated_batch_sizes_bytes' => array_map(fn($batch) => array_sum(array_map([$this, 'estimateObjectSize'], $batch)), $batches) + ]); + + return $batches; + } + + /** + * Estimate the average size of objects by sampling + * + * @param array $objects Array of objects to sample + * @param int $sampleSize Number of objects to sample for size estimation + * @return int Estimated average object size in bytes + */ + private function estimateAverageObjectSize(array $objects, int $sampleSize): int + { + $totalObjects = count($objects); + if ($totalObjects === 0) { + return 1000; // Default fallback size + } + + // Sample evenly distributed objects + $sampleIndices = []; + if ($totalObjects <= $sampleSize) { + // Use all objects if we have fewer than sample size + $sampleIndices = range(0, $totalObjects - 1); + } else { + // Sample evenly across the array + $step = max(1, intval($totalObjects / $sampleSize)); + for ($i = 0; $i < $totalObjects; $i += $step) { + $sampleIndices[] = $i; + if (count($sampleIndices) >= $sampleSize) { + break; + } + } + } + + // Calculate sizes of sampled objects + $totalSampleSize = 0; + foreach ($sampleIndices as $index) { + $totalSampleSize += $this->estimateObjectSize($objects[$index]); + } + + $averageSize = intval($totalSampleSize / count($sampleIndices)); + + $this->logger->debug('Object size estimation completed', [ + 'total_objects' => $totalObjects, + 'sampled_objects' => count($sampleIndices), + 'total_sample_size_bytes' => $totalSampleSize, + 'estimated_average_size_bytes' => $averageSize + ]); + + return max(1000, $averageSize); // Minimum 1KB per object + } + + /** + * Estimate the serialized size of an object for batching purposes + * + * @param array $object The object to estimate size for + * @return int Estimated size in bytes + */ + private function estimateObjectSize(array $object): int + { + // Quick estimation based on JSON serialization + // This includes overhead for SQL parameters and structure + $jsonSize = strlen(json_encode($object)); + + // Add overhead for SQL INSERT statement structure + // Each object becomes multiple parameters in a bulk INSERT + $sqlOverhead = 500; // Estimated overhead per object in SQL + + return $jsonSize + $sqlOverhead; + } + /** * Calculate detailed object statistics for import operations * @@ -2780,7 +3829,7 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb if ($this->lastSaveResult !== null) { $saveResult = $this->lastSaveResult; - // Count objects by section type from the actual saved objects + // Count objects by section type from the actual processed objects $allProcessedObjects = array_merge( $saveResult['saved'] ?? [], $saveResult['updated'] ?? [], @@ -2822,6 +3871,10 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb $wasUpdated = !empty(array_filter($saveResult['updated'] ?? [], fn($updated) => ($updated->getUuid() === $objectId))); + // Check if this object was skipped (no changes) + $wasSkipped = !empty(array_filter($saveResult['skipped'] ?? [], + fn($skipped) => ($skipped->getUuid() === $objectId))); + // Check if this object had validation errors $hasErrors = !empty(array_filter($saveResult['invalid'] ?? [], fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId))); @@ -2830,6 +3883,8 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb $statistics[$sectionKey]['created']++; } elseif ($wasUpdated) { $statistics[$sectionKey]['updated']++; + } elseif ($wasSkipped) { + $statistics[$sectionKey]['skipped']++; } elseif ($hasErrors) { // Add to errors array for this section $errorInfo = array_filter($saveResult['invalid'] ?? [], @@ -2839,6 +3894,7 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb $statistics[$sectionKey]['errors'][] = array_values($errorInfo)[0]['error'] ?? 'Unknown validation error'; } } else { + // This shouldn't happen, but leave as fallback $statistics[$sectionKey]['skipped']++; } } diff --git a/lib/Service/ArchiMateService.php b/lib/Service/ArchiMateService.php index 461fc9b6..b4ff6ae2 100644 --- a/lib/Service/ArchiMateService.php +++ b/lib/Service/ArchiMateService.php @@ -100,8 +100,11 @@ class ArchiMateService 'xml_parse_flags' => LIBXML_NOCDATA | LIBXML_NONET, 'memory_cleanup' => true, 'parallel_processing' => true, - 'batch_size' => 1000, // Large batch size for maximum performance - 'parallel_batches' => 8 // Process 8 batches concurrently + 'batch_size' => 1000, // Default batch size (will be adjusted intelligently) + 'parallel_batches' => 8, // Process 8 batches concurrently + 'max_batch_size_bytes' => 8388608, // 8 MB - safe under MySQL's 16 MB limit + 'min_batch_size' => 50, // Minimum batch size for very large objects + 'size_estimation_sample' => 10 // Sample size for estimating object sizes ]; /** @@ -634,8 +637,7 @@ private function createModelObject(array $metadata, string $modelIdentifier): ar 'id' => $metadata['identifier'] ?? uniqid('model_'), 'owner' => $this->getCurrentUserId(), 'organisation' => $this->getCurrentOrganisation(), - 'created' => date('Y-m-d H:i:s'), - 'updated' => date('Y-m-d H:i:s') + ], 'identifier' => $metadata['identifier'] ?? '', 'section' => 'model', @@ -669,8 +671,7 @@ private function createSectionObject(string $section, string $identifier, array 'id' => $identifier, 'owner' => $this->getCurrentUserId(), 'organisation' => $this->getCurrentOrganisation(), - 'created' => date('Y-m-d H:i:s'), - 'updated' => date('Y-m-d H:i:s') + ] ]; @@ -780,20 +781,29 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj $batchSize = self::PERFORMANCE_OPTIMIZATIONS['batch_size']; $parallelBatches = self::PERFORMANCE_OPTIMIZATIONS['parallel_batches']; - // Split objects into chunks - $chunks = array_chunk($objects, $batchSize); + // INTELLIGENT BATCH SIZING: Create size-aware batches instead of fixed-size chunks + $chunks = $this->createIntelligentBatches($objects); $totalChunks = count($chunks); - $this->logger->info('Starting optimized batch processing', [ - 'total_objects' => count($objects), - 'total_chunks' => $totalChunks, - 'batch_size' => $batchSize, - 'parallel_batches' => $parallelBatches + $this->logger->info('Starting intelligent batch processing', [ + 'total_objects_to_save' => count($objects), + 'intelligent_batches_created' => $totalChunks, + 'batch_sizes' => array_map('count', $chunks), + 'batching_method' => 'size_aware_intelligent', + 'mysql_packet_limit_safe' => true ]); $allResults = []; $processedChunks = 0; + // Accumulate statistics from all chunks + $aggregatedStats = [ + 'saved' => [], + 'updated' => [], + 'skipped' => [], + 'invalid' => [] + ]; + // Process chunks sequentially but with larger batch sizes for better performance foreach ($chunks as $chunkIndex => $chunk) { // OPTIMIZATION: Removed debug logging from chunk processing loop @@ -809,6 +819,12 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj events: !self::PERFORMANCE_OPTIMIZATIONS['disable_events'] ); + // Accumulate statistics from this chunk + $aggregatedStats['saved'] = array_merge($aggregatedStats['saved'], $saveResult['saved'] ?? []); + $aggregatedStats['updated'] = array_merge($aggregatedStats['updated'], $saveResult['updated'] ?? []); + $aggregatedStats['skipped'] = array_merge($aggregatedStats['skipped'], $saveResult['skipped'] ?? []); + $aggregatedStats['invalid'] = array_merge($aggregatedStats['invalid'], $saveResult['invalid'] ?? []); + $savedObjects = array_merge( $saveResult['saved'] ?? [], $saveResult['updated'] ?? [] @@ -820,7 +836,11 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj $this->logger->info('Processed chunk', [ 'processed_chunks' => $processedChunks, 'total_chunks' => $totalChunks, - 'progress_percent' => round(($processedChunks / $totalChunks) * 100, 1) + 'progress_percent' => round(($processedChunks / $totalChunks) * 100, 1), + 'chunk_saved' => count($saveResult['saved'] ?? []), + 'chunk_updated' => count($saveResult['updated'] ?? []), + 'chunk_skipped' => count($saveResult['skipped'] ?? []), + 'chunk_invalid' => count($saveResult['invalid'] ?? []) ]); } catch (\Exception $e) { @@ -837,9 +857,16 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj } } + // Store the aggregated result for statistics calculation + $this->lastSaveResult = $aggregatedStats; + $this->logger->info('Optimized batch processing completed', [ 'total_objects_processed' => count($allResults), - 'total_chunks_processed' => $totalChunks + 'total_chunks_processed' => $totalChunks, + 'aggregated_saved' => count($aggregatedStats['saved']), + 'aggregated_updated' => count($aggregatedStats['updated']), + 'aggregated_skipped' => count($aggregatedStats['skipped']), + 'aggregated_invalid' => count($aggregatedStats['invalid']) ]); return $allResults; @@ -899,6 +926,18 @@ private function saveObjectsInSingleBatch(array $objects, ObjectService $objectS } } + // Log details about skipped objects if any + if (!empty($saveResult['skipped'])) { + $this->logger->info('Objects skipped during import (no changes detected)', [ + 'skipped_count' => count($saveResult['skipped']), + 'sample_skipped_ids' => array_slice( + array_map(fn($obj) => $obj->getUuid() ?? 'unknown', $saveResult['skipped']), + 0, + 5 + ) + ]); + } + // Return the combined saved and updated objects (maintaining backward compatibility) return $savedObjects; } @@ -1560,6 +1599,155 @@ public function isOperationInProgress(): bool return $this->isImportInProgress() || $this->isExportInProgress(); } + /** + * Create intelligent batches based on object size to prevent MySQL packet size issues + * + * This method analyzes object sizes and creates batches that stay under the MySQL + * max_allowed_packet limit while maintaining reasonable performance. + * + * @param array $objects Array of objects to batch + * @return array Array of batches, each containing objects that fit within size limits + */ + private function createIntelligentBatches(array $objects): array + { + $maxBatchSizeBytes = self::PERFORMANCE_OPTIMIZATIONS['max_batch_size_bytes']; + $minBatchSize = self::PERFORMANCE_OPTIMIZATIONS['min_batch_size']; + $sampleSize = self::PERFORMANCE_OPTIMIZATIONS['size_estimation_sample']; + + if (empty($objects)) { + return []; + } + + // Estimate average object size by sampling + $avgObjectSize = $this->estimateAverageObjectSize($objects, $sampleSize); + + // Calculate optimal batch size based on object size + $optimalBatchSize = max($minBatchSize, intval($maxBatchSizeBytes / $avgObjectSize)); + + $this->logger->info('Intelligent batch sizing analysis', [ + 'total_objects' => count($objects), + 'estimated_avg_object_size_bytes' => $avgObjectSize, + 'max_batch_size_bytes' => $maxBatchSizeBytes, + 'calculated_optimal_batch_size' => $optimalBatchSize, + 'min_batch_size_enforced' => $minBatchSize + ]); + + // Create batches with size awareness + $batches = []; + $currentBatch = []; + $currentBatchSize = 0; + + foreach ($objects as $object) { + $objectSize = $this->estimateObjectSize($object); + + // Check if adding this object would exceed the batch size limit + if (!empty($currentBatch) && ($currentBatchSize + $objectSize) > $maxBatchSizeBytes) { + // Current batch is full, save it and start a new one + $batches[] = $currentBatch; + $currentBatch = [$object]; + $currentBatchSize = $objectSize; + } else { + // Add object to current batch + $currentBatch[] = $object; + $currentBatchSize += $objectSize; + } + + // Safety check: if a single object is larger than max batch size, + // create a batch with just that object + if (count($currentBatch) === 1 && $objectSize > $maxBatchSizeBytes) { + $this->logger->warning('Very large object detected, creating single-object batch', [ + 'object_id' => $object['@self']['id'] ?? 'unknown', + 'object_size_bytes' => $objectSize, + 'max_batch_size_bytes' => $maxBatchSizeBytes + ]); + $batches[] = $currentBatch; + $currentBatch = []; + $currentBatchSize = 0; + } + } + + // Add the last batch if it has objects + if (!empty($currentBatch)) { + $batches[] = $currentBatch; + } + + $this->logger->info('Intelligent batching completed', [ + 'total_objects' => count($objects), + 'total_batches_created' => count($batches), + 'batch_sizes' => array_map('count', $batches), + 'estimated_batch_sizes_bytes' => array_map(fn($batch) => array_sum(array_map([$this, 'estimateObjectSize'], $batch)), $batches) + ]); + + return $batches; + } + + /** + * Estimate the average size of objects by sampling + * + * @param array $objects Array of objects to sample + * @param int $sampleSize Number of objects to sample for size estimation + * @return int Estimated average object size in bytes + */ + private function estimateAverageObjectSize(array $objects, int $sampleSize): int + { + $totalObjects = count($objects); + if ($totalObjects === 0) { + return 1000; // Default fallback size + } + + // Sample evenly distributed objects + $sampleIndices = []; + if ($totalObjects <= $sampleSize) { + // Use all objects if we have fewer than sample size + $sampleIndices = range(0, $totalObjects - 1); + } else { + // Sample evenly across the array + $step = max(1, intval($totalObjects / $sampleSize)); + for ($i = 0; $i < $totalObjects; $i += $step) { + $sampleIndices[] = $i; + if (count($sampleIndices) >= $sampleSize) { + break; + } + } + } + + // Calculate sizes of sampled objects + $totalSampleSize = 0; + foreach ($sampleIndices as $index) { + $totalSampleSize += $this->estimateObjectSize($objects[$index]); + } + + $averageSize = intval($totalSampleSize / count($sampleIndices)); + + $this->logger->debug('Object size estimation completed', [ + 'total_objects' => $totalObjects, + 'sampled_objects' => count($sampleIndices), + 'total_sample_size_bytes' => $totalSampleSize, + 'estimated_average_size_bytes' => $averageSize + ]); + + return max(1000, $averageSize); // Minimum 1KB per object + } + + /** + * Estimate the serialized size of an object for batching purposes + * + * @param array $object The object to estimate size for + * @return int Estimated size in bytes + */ + private function estimateObjectSize(array $object): int + { + // Quick estimation based on JSON serialization + // This includes overhead for SQL parameters and structure + $jsonSize = strlen(json_encode($object)); + + // Add overhead for SQL INSERT statement structure + // Each object becomes multiple parameters in a bulk INSERT + $sqlOverhead = 500; // Estimated overhead per object in SQL + + return $jsonSize + $sqlOverhead; + } + /** * Calculate detailed object statistics for import operations * @@ -1582,7 +1770,7 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb if ($this->lastSaveResult !== null) { $saveResult = $this->lastSaveResult; - // Count objects by section type from the actual saved objects + // Count objects by section type from the actual processed objects $allProcessedObjects = array_merge( $saveResult['saved'] ?? [], $saveResult['updated'] ?? [], @@ -1624,6 +1812,10 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb $wasUpdated = !empty(array_filter($saveResult['updated'] ?? [], fn($updated) => ($updated->getUuid() === $objectId))); + // Check if this object was skipped (no changes) + $wasSkipped = !empty(array_filter($saveResult['skipped'] ?? [], + fn($skipped) => ($skipped->getUuid() === $objectId))); + // Check if this object had validation errors $hasErrors = !empty(array_filter($saveResult['invalid'] ?? [], fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId))); @@ -1632,6 +1824,8 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb $statistics[$sectionKey]['created']++; } elseif ($wasUpdated) { $statistics[$sectionKey]['updated']++; + } elseif ($wasSkipped) { + $statistics[$sectionKey]['skipped']++; } elseif ($hasErrors) { // Add to errors array for this section $errorInfo = array_filter($saveResult['invalid'] ?? [], @@ -1641,6 +1835,7 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb $statistics[$sectionKey]['errors'][] = array_values($errorInfo)[0]['error'] ?? 'Unknown validation error'; } } else { + // This shouldn't happen, but leave as fallback $statistics[$sectionKey]['skipped']++; } } @@ -1801,8 +1996,7 @@ private function createModelObjectDirect(array $metadata, string $modelIdentifie 'id' => $modelIdentifier, 'owner' => $this->cachedConfig['userId'], 'organisation' => $this->cachedConfig['organisation'], - 'created' => date('Y-m-d H:i:s'), - 'updated' => date('Y-m-d H:i:s') + ], 'identifier' => $modelIdentifier, 'section' => 'model', @@ -1880,8 +2074,7 @@ private function transformSectionObjectsBatch( 'id' => $identifier, 'owner' => $this->cachedConfig['userId'], 'organisation' => $this->cachedConfig['organisation'], - 'created' => date('Y-m-d H:i:s'), - 'updated' => date('Y-m-d H:i:s') + ], 'identifier' => $identifier, 'section' => $schemaType, diff --git a/src/views/settings/sections/ArchiMateImportExport.vue b/src/views/settings/sections/ArchiMateImportExport.vue index a5e97c89..168e550b 100644 --- a/src/views/settings/sections/ArchiMateImportExport.vue +++ b/src/views/settings/sections/ArchiMateImportExport.vue @@ -564,45 +564,45 @@ export default { } }, - /** - * Load organization options from the API - * - * @async - * @return {Promise} - */ - async loadOrganizations() { - try { - // Get organization objects from OpenRegister - // This would need to be implemented based on your organization schema - // For now, we'll keep the default Generic option - const response = await fetch('/index.php/apps/openregister/api/objects/6/35', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'OCS-APIREQUEST': 'true', - requesttoken: OC.requestToken, - }, - }) - - if (response.ok) { - const result = await response.json() - const organizations = result.results || [] - - // Add organization options - const orgOptions = [ - { label: 'Generic', value: null }, - ...organizations.map(org => ({ - label: org.naam || org.title || org.name || 'Unknown Organization', - value: org.id, - })), - ] - - this.organizationOptions = orgOptions + /** + * Load organization options from the API + * + * @async + * @return {Promise} + */ + async loadOrganizations() { + try { + // Get organization objects from OpenRegister + // This would need to be implemented based on your organization schema + // For now, we'll keep the default Generic option + const response = await fetch('/index.php/apps/openregister/api/objects/6/35', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'OCS-APIREQUEST': 'true', + requesttoken: OC.requestToken, + }, + }) + + if (response.ok) { + const result = await response.json() + const organizations = result.results || [] + + // Add organization options + const orgOptions = [ + { label: 'Generic', value: null }, + ...organizations.map(org => ({ + label: org.naam || org.title || org.name || 'Unknown Organization', + value: org.id, + })), + ] + + this.organizationOptions = orgOptions + } + } catch (error) { + console.warn('Failed to load organizations, using default options:', error) + // Keep default options if loading fails } - } catch (error) { - console.warn('Failed to load organizations, using default options:', error) - // Keep default options if loading fails - } }, }, } From d65b67fa097e1de91687027af6c9e6e87ba5a402 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 28 Aug 2025 15:37:09 +0200 Subject: [PATCH 57/83] Add views endpoint --- lib/Settings/softwarecatalogus_register.json | 21 +++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index 6e844f90..d785bdf2 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -75,11 +75,30 @@ "conditions": [], "inputMapping": null, "outputMapping": null, - "rules": ["register-organization-rule"], "configurations": [], "slug": "register", "created": "2025-05-16T13:11:49+00:00", "updated": "2025-05-16T13:38:20+00:00" + }, + "views": { + "name": "Views", + "description": "Endpoint voor het ophalen van AMEF views uit de vng-gemma register", + "reference": "", + "version": "0.0.1", + "endpoint": "views", + "endpointArray": ["views"], + "endpointRegex": "#^views$#", + "method": "GET", + "targetType": "register/schema", + "targetId": "vng-gemma/view", + "conditions": [], + "inputMapping": null, + "outputMapping": null, + "rules": [], + "configurations": [], + "slug": "views", + "created": "2025-01-17T12:00:00+00:00", + "updated": "2025-01-17T12:00:00+00:00" } }, "schemas": { From e9d0d70b2c3ff14a1d2fb1b82330630e2450493d Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 28 Aug 2025 17:18:38 +0200 Subject: [PATCH 58/83] Endpoint hotfix --- lib/Service/ArchiMateImportService.php | 15 +- lib/Service/ArchiMateService.php | 8 +- lib/Settings/softwarecatalogus_register.json | 364 ++++--------------- 3 files changed, 89 insertions(+), 298 deletions(-) diff --git a/lib/Service/ArchiMateImportService.php b/lib/Service/ArchiMateImportService.php index 6724d541..3893d71d 100644 --- a/lib/Service/ArchiMateImportService.php +++ b/lib/Service/ArchiMateImportService.php @@ -966,7 +966,8 @@ private function createModelObject(array $metadata, string $modelIdentifier): ar 'schema' => $schemaId, 'id' => $metadata['identifier'] ?? uniqid('model_'), 'owner' => $this->getCurrentUserId(), - 'organisation' => $this->getCurrentOrganisation() + 'organisation' => $this->getCurrentOrganisation(), + 'published' => date('Y-m-d\TH:i:s\Z') ], 'identifier' => $metadata['identifier'] ?? '', 'section' => 'model', @@ -1031,7 +1032,8 @@ private function createSectionObject(string $section, string $identifier, array 'id' => $objectId, // Now using objectId as main ID 'slug' => $slug, // Now using AMEF identifier as slug 'owner' => $this->getCurrentUserId(), - 'organisation' => $this->getCurrentOrganisation() + 'organisation' => $this->getCurrentOrganisation(), + 'published' => date('Y-m-d\TH:i:s\Z') ] ]; @@ -3049,7 +3051,8 @@ private function createModelObjectDirect(array $metadata, string $modelIdentifie 'schema' => $this->cachedConfig['schemaIds']['model'] ?? 67, 'id' => $modelIdentifier, 'owner' => $this->cachedConfig['userId'], - 'organisation' => $this->cachedConfig['organisation'] + 'organisation' => $this->cachedConfig['organisation'], + 'published' => date('Y-m-d\TH:i:s\Z') ], 'identifier' => $modelIdentifier, 'section' => 'model', @@ -3136,7 +3139,7 @@ private function transformSectionObjectsBatch( 'id' => $identifier, 'owner' => $this->cachedConfig['userId'], 'organisation' => $this->cachedConfig['organisation'], - + 'published' => date('Y-m-d\TH:i:s\Z') ], 'identifier' => $identifier, 'section' => $schemaType, @@ -3481,7 +3484,7 @@ private function bulkTransformSection( 'id' => $identifier, 'owner' => $this->cachedConfig['userId'], 'organisation' => $this->cachedConfig['organisation'], - + 'published' => date('Y-m-d\TH:i:s\Z') ], 'identifier' => $identifier, 'section' => $schemaType, @@ -3601,7 +3604,7 @@ private function bulkTransformViews( 'id' => $identifier, 'owner' => $this->cachedConfig['userId'], 'organisation' => $this->cachedConfig['organisation'], - + 'published' => date('Y-m-d\TH:i:s\Z') ], 'identifier' => $identifier, 'section' => 'view', diff --git a/lib/Service/ArchiMateService.php b/lib/Service/ArchiMateService.php index b4ff6ae2..49adf537 100644 --- a/lib/Service/ArchiMateService.php +++ b/lib/Service/ArchiMateService.php @@ -637,7 +637,7 @@ private function createModelObject(array $metadata, string $modelIdentifier): ar 'id' => $metadata['identifier'] ?? uniqid('model_'), 'owner' => $this->getCurrentUserId(), 'organisation' => $this->getCurrentOrganisation(), - + 'published' => date('Y-m-d\TH:i:s\Z') ], 'identifier' => $metadata['identifier'] ?? '', 'section' => 'model', @@ -671,7 +671,7 @@ private function createSectionObject(string $section, string $identifier, array 'id' => $identifier, 'owner' => $this->getCurrentUserId(), 'organisation' => $this->getCurrentOrganisation(), - + 'published' => date('Y-m-d\TH:i:s\Z') ] ]; @@ -1996,7 +1996,7 @@ private function createModelObjectDirect(array $metadata, string $modelIdentifie 'id' => $modelIdentifier, 'owner' => $this->cachedConfig['userId'], 'organisation' => $this->cachedConfig['organisation'], - + 'published' => date('Y-m-d\TH:i:s\Z') ], 'identifier' => $modelIdentifier, 'section' => 'model', @@ -2074,7 +2074,7 @@ private function transformSectionObjectsBatch( 'id' => $identifier, 'owner' => $this->cachedConfig['userId'], 'organisation' => $this->cachedConfig['organisation'], - + 'published' => date('Y-m-d\TH:i:s\Z') ], 'identifier' => $identifier, 'section' => $schemaType, diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index d785bdf2..c618bc8d 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -45,7 +45,6 @@ "property-definition", "relation", "view", - "extendview", "property" ], "source": "", @@ -80,13 +79,60 @@ "created": "2025-05-16T13:11:49+00:00", "updated": "2025-05-16T13:38:20+00:00" }, + "elements": { + "name": "Elements", + "description": "", + "reference": "https://vng.accept.commonground.nu/Endpoint-Elements.json", + "version": "0.0.1", + "endpoint": "elements", + "endpointArray": [ + "elements" + ], + "endpointRegex": "#^elements$#", + "method": "GET", + "targetType": "register/schema", + "targetId": "vng-gemma/element", + "conditions": [], + "inputMapping": null, + "outputMapping": null, + "rules": [], + "configurations": [], + "slug": "elements", + "created": "2025-05-01T14:48:08+00:00", + "updated": "2025-05-02T10:37:48+00:00" + }, + "element": { + "name": "Element", + "description": "", + "reference": "https://vng.accept.commonground.nu/Endpoint-Element.json", + "version": "0.0.1", + "endpoint": "elements/{{id}}", + "endpointArray": [ + "elements", + "{{id}}" + ], + "endpointRegex": "#^elements(/([^/]+))$#", + "method": "GET", + "targetType": "register/schema", + "targetId": "vng-gemma/element", + "conditions": [], + "inputMapping": null, + "outputMapping": null, + "rules": [], + "configurations": [], + "slug": "element", + "created": "2025-05-01T14:48:08+00:00", + "updated": "2025-05-02T10:37:52+00:00" + }, "views": { "name": "Views", - "description": "Endpoint voor het ophalen van AMEF views uit de vng-gemma register", - "reference": "", + "description": "", + "reference": "https://vng.accept.commonground.nu/Endpoint-Views.json", "version": "0.0.1", "endpoint": "views", - "endpointArray": ["views"], + "endpointArray": [ + "views" + ], "endpointRegex": "#^views$#", "method": "GET", "targetType": "register/schema", @@ -97,8 +143,31 @@ "rules": [], "configurations": [], "slug": "views", - "created": "2025-01-17T12:00:00+00:00", - "updated": "2025-01-17T12:00:00+00:00" + "created": "2025-05-01T14:48:08+00:00", + "updated": "2025-05-02T10:39:14+00:00" + }, + "view": { + "name": "View", + "description": "", + "reference": "https://vng.accept.commonground.nu/Endpoint-View.json", + "version": "0.0.1", + "endpoint": "views/{{id}}", + "endpointArray": [ + "views", + "{{id}}" + ], + "endpointRegex": "#^views(/([^/]+))$#", + "method": "GET", + "targetType": "register/schema", + "targetId": "vng-gemma/view", + "conditions": [], + "inputMapping": null, + "outputMapping": null, + "rules": [], + "configurations": [], + "slug": "view", + "created": "2025-05-01T14:48:08+00:00", + "updated": "2025-05-02T10:39:24+00:00" } }, "schemas": { @@ -4294,288 +4363,7 @@ "deleted": null, "configuration": null }, - "extendview": { - "slug": "extendview", - "title": "Extended View", - "description": "AMEF Extended View - Vooraf uitgebreide view kopie voor prestatie optimalisatie", - "version": "0.0.5", - "summary": "", - "icon": "EyePlus", - "required": [ - "identifier", - "type", - "name", - "properties", - "nodes", - "connections" - ], - "properties": { - "identifier": { - "description": "De identifier van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "id-a6ee6077d3094afa91fc6ea92a9a2a40", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "type": { - "description": "De type van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Diagram", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "viewpoint": { - "description": "De viewpoint van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Application Structure", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name": { - "description": "De name van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "LV01 BGT basisregistratie en SVB view", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "name-lang": { - "description": "De name-language van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation": { - "description": "De documentation van deze View", - "type": "string", - "minLength": null, - "maxLength": null, - "example": "Toont de referentiecomponenten ter ondersteuning van applicatieservices voor publieksdiensten", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "documentation-lang": { - "description": "De documentation-language van deze View", - "type": "string", - "minLength": 2, - "maxLength": 2, - "example": "nl", - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "properties": { - "description": "De properties van deze View", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/Model_Property.json", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "nodes": { - "description": "De nodes van deze View", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View_Node.json", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - }, - "connections": { - "description": "De connections van deze View", - "type": "array", - "minLength": null, - "maxLength": null, - "minimum": null, - "maximum": null, - "multipleOf": null, - "minItems": null, - "maxItems": null, - "$ref": "", - "items": { - "cascadeDelete": true, - "$ref": "https://vng.accept.commonground.nu/openregister/schemas/View_Connection.json", - "type": "object" - }, - "objectConfiguration": { - "handling": "nested-object", - "schema": "" - }, - "fileConfiguration": { - "handling": "ignore", - "allowedMimeTypes": [], - "location": "", - "maxSize": 0 - }, - "oneOf": [] - } - }, - "archive": [], - "source": "", - "hardValidation": false, - "updated": "2025-05-15T10:54:50+00:00", - "created": "2025-05-13T19:49:15+00:00", - "maxDepth": 0, - "owner": null, - "application": null, - "organisation": null, - "authorization": null, - "deleted": null, - "configuration": null - }, + "module": { "uri": null, "slug": "module", From 2bb7cdfb9542c20d88cf66b2e45c68e0138b2f5b Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 29 Aug 2025 01:58:41 +0200 Subject: [PATCH 59/83] Update archimate xport for new saveObjects method --- lib/Service/ArchiMateImportService.php | 62 +++++++++++-------- lib/Service/ArchiMateService.php | 2 +- .../sections/ArchiMateImportExport.vue | 8 +-- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/lib/Service/ArchiMateImportService.php b/lib/Service/ArchiMateImportService.php index 3893d71d..620a94c8 100644 --- a/lib/Service/ArchiMateImportService.php +++ b/lib/Service/ArchiMateImportService.php @@ -1142,11 +1142,11 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj $allResults = []; $processedChunks = 0; - // Accumulate statistics from all chunks + // Accumulate statistics from all chunks (using new format) $aggregatedStats = [ 'saved' => [], 'updated' => [], - 'skipped' => [], + 'unchanged' => [], 'invalid' => [] ]; @@ -1170,16 +1170,18 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj events: !self::PERFORMANCE_OPTIMIZATIONS['disable_events'] ); + + // Calculate totals received back from this chunk $chunkTotalReceived = count($saveResult['saved'] ?? []) + count($saveResult['updated'] ?? []) + - count($saveResult['skipped'] ?? []) + + count($saveResult['unchanged'] ?? []) + count($saveResult['invalid'] ?? []); // Accumulate statistics from this chunk $aggregatedStats['saved'] = array_merge($aggregatedStats['saved'], $saveResult['saved'] ?? []); $aggregatedStats['updated'] = array_merge($aggregatedStats['updated'], $saveResult['updated'] ?? []); - $aggregatedStats['skipped'] = array_merge($aggregatedStats['skipped'], $saveResult['skipped'] ?? []); + $aggregatedStats['unchanged'] = array_merge($aggregatedStats['unchanged'], $saveResult['unchanged'] ?? []); $aggregatedStats['invalid'] = array_merge($aggregatedStats['invalid'], $saveResult['invalid'] ?? []); $savedObjects = array_merge( @@ -1200,7 +1202,7 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj 'objects_lost_in_chunk' => $chunkInputCount - $chunkTotalReceived, 'chunk_saved' => count($saveResult['saved'] ?? []), 'chunk_updated' => count($saveResult['updated'] ?? []), - 'chunk_skipped' => count($saveResult['skipped'] ?? []), + 'chunk_unchanged' => count($saveResult['unchanged'] ?? []), 'chunk_invalid' => count($saveResult['invalid'] ?? []) ]); @@ -1221,7 +1223,7 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj // Store the aggregated result for statistics calculation $this->lastSaveResult = $aggregatedStats; - $totalObjectsProcessed = count($aggregatedStats['saved']) + count($aggregatedStats['updated']) + count($aggregatedStats['skipped']) + count($aggregatedStats['invalid']); + $totalObjectsProcessed = count($aggregatedStats['saved']) + count($aggregatedStats['updated']) + count($aggregatedStats['unchanged']) + count($aggregatedStats['invalid']); $this->logger->info('Optimized batch processing completed', [ 'INPUT_SUMMARY' => [ @@ -1239,7 +1241,7 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj 'total_chunks_processed' => $totalChunks, 'aggregated_saved' => count($aggregatedStats['saved']), 'aggregated_updated' => count($aggregatedStats['updated']), - 'aggregated_skipped' => count($aggregatedStats['skipped']), + 'aggregated_unchanged' => count($aggregatedStats['unchanged']), 'aggregated_invalid' => count($aggregatedStats['invalid']) ] ]); @@ -1283,6 +1285,8 @@ private function saveObjectsInSingleBatch(array $objects, ObjectService $objectS events: !self::PERFORMANCE_OPTIMIZATIONS['disable_events'] ); + + // Store the save result for later access to statistics $this->lastSaveResult = $saveResult; @@ -1296,7 +1300,7 @@ private function saveObjectsInSingleBatch(array $objects, ObjectService $objectS $this->logger->info('Objects saved successfully', [ 'saved_count' => count($saveResult['saved'] ?? []), 'updated_count' => count($saveResult['updated'] ?? []), - 'skipped_count' => count($saveResult['skipped'] ?? []), + 'unchanged_count' => count($saveResult['unchanged'] ?? []), 'invalid_count' => count($saveResult['invalid'] ?? []), 'error_count' => count($saveResult['errors'] ?? []), 'total_processed' => $saveResult['statistics']['totalProcessed'] ?? 0 @@ -1313,12 +1317,12 @@ private function saveObjectsInSingleBatch(array $objects, ObjectService $objectS } } - // Log details about skipped objects if any - if (!empty($saveResult['skipped'])) { - $this->logger->info('Objects skipped during import (no changes detected)', [ - 'skipped_count' => count($saveResult['skipped']), - 'sample_skipped_ids' => array_slice( - array_map(fn($obj) => $obj->getUuid() ?? 'unknown', $saveResult['skipped']), + // Log details about unchanged objects if any + if (!empty($saveResult['unchanged'])) { + $this->logger->info('Objects unchanged during import (no changes detected)', [ + 'unchanged_count' => count($saveResult['unchanged']), + 'sample_unchanged_ids' => array_slice( + array_map(fn($obj) => $obj->getUuid() ?? 'unknown', $saveResult['unchanged']), 0, 5 ) @@ -1712,18 +1716,21 @@ private function calculateOptimizedStatistics(array $savedObjects): array 'total_objects_created' => 0, 'total_objects_updated' => 0, 'total_objects_deleted' => 0, - 'total_objects_skipped' => 0, + 'total_objects_unchanged' => 0, 'total_errors' => 0 ] ]; if ($this->lastSaveResult !== null) { $saveResult = $this->lastSaveResult; + + + $statistics['summary'] = [ 'total_objects_created' => count($saveResult['saved'] ?? []), 'total_objects_updated' => count($saveResult['updated'] ?? []), 'total_objects_deleted' => 0, - 'total_objects_skipped' => count($saveResult['skipped'] ?? []), + 'total_objects_unchanged' => count($saveResult['unchanged'] ?? $saveResult['skipped'] ?? []), 'total_errors' => count($saveResult['invalid'] ?? []) ]; @@ -1731,14 +1738,14 @@ private function calculateOptimizedStatistics(array $savedObjects): array $totalStatisticsCount = array_sum([ count($saveResult['saved'] ?? []), count($saveResult['updated'] ?? []), - count($saveResult['skipped'] ?? []), + count($saveResult['unchanged'] ?? $saveResult['skipped'] ?? []), count($saveResult['invalid'] ?? []) ]); $this->logger->info('Import statistics breakdown', [ 'created' => count($saveResult['saved'] ?? []), 'updated' => count($saveResult['updated'] ?? []), - 'skipped' => count($saveResult['skipped'] ?? []), + 'unchanged' => count($saveResult['unchanged'] ?? $saveResult['skipped'] ?? []), 'invalid' => count($saveResult['invalid'] ?? []), 'total_in_statistics' => $totalStatisticsCount ]); @@ -3821,11 +3828,11 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb { // Initialize statistics structure $statistics = [ - 'elements' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []], - 'organizations' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []], - 'relationships' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []], - 'views' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []], - 'property_definitions' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []] + 'elements' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []], + 'organizations' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []], + 'relationships' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []], + 'views' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []], + 'property_definitions' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []] ]; // If we have access to the actual save results from ObjectService, use those @@ -3836,7 +3843,7 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb $allProcessedObjects = array_merge( $saveResult['saved'] ?? [], $saveResult['updated'] ?? [], - $saveResult['skipped'] ?? [], + $saveResult['unchanged'] ?? $saveResult['skipped'] ?? [], // For invalid objects, extract the original object from the error structure array_map(fn($item) => $item['object'] ?? [], $saveResult['invalid'] ?? []) ); @@ -3874,9 +3881,10 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb $wasUpdated = !empty(array_filter($saveResult['updated'] ?? [], fn($updated) => ($updated->getUuid() === $objectId))); - // Check if this object was skipped (no changes) - $wasSkipped = !empty(array_filter($saveResult['skipped'] ?? [], - fn($skipped) => ($skipped->getUuid() === $objectId))); + // Check if this object was unchanged (no changes) + $unchangedObjects = $saveResult['unchanged'] ?? $saveResult['skipped'] ?? []; + $wasSkipped = !empty(array_filter($unchangedObjects, + fn($unchanged) => ($unchanged->getUuid() === $objectId))); // Check if this object had validation errors $hasErrors = !empty(array_filter($saveResult['invalid'] ?? [], diff --git a/lib/Service/ArchiMateService.php b/lib/Service/ArchiMateService.php index 49adf537..7b9cb689 100644 --- a/lib/Service/ArchiMateService.php +++ b/lib/Service/ArchiMateService.php @@ -909,7 +909,7 @@ private function saveObjectsInSingleBatch(array $objects, ObjectService $objectS $this->logger->info('Objects saved successfully', [ 'saved_count' => count($saveResult['saved'] ?? []), 'updated_count' => count($saveResult['updated'] ?? []), - 'skipped_count' => count($saveResult['skipped'] ?? []), + 'unchanged_count' => count($saveResult['skipped'] ?? []), 'invalid_count' => count($saveResult['invalid'] ?? []), 'error_count' => count($saveResult['errors'] ?? []), 'total_processed' => $saveResult['statistics']['totalProcessed'] ?? 0 diff --git a/src/views/settings/sections/ArchiMateImportExport.vue b/src/views/settings/sections/ArchiMateImportExport.vue index 168e550b..089f0429 100644 --- a/src/views/settings/sections/ArchiMateImportExport.vue +++ b/src/views/settings/sections/ArchiMateImportExport.vue @@ -144,12 +144,12 @@ Updated
-
+
- {{ importResult.statistics.summary.total_objects_skipped }} + {{ importResult.statistics.summary.total_objects_unchanged }}
- Skipped + Unchanged
@@ -872,7 +872,7 @@ export default { background: var(--color-warning-light); } -.summary-item.skipped { +.summary-item.unchanged { border-color: var(--color-text-lighter); background: var(--color-background-hover); } From 10dfb2ec341ef380e8eab68754d3f2ed0e52659a Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 29 Aug 2025 09:48:24 +0200 Subject: [PATCH 60/83] Lex fix the auto configure --- lib/Service/SettingsService.php | 253 +++++++------- lib/Settings/softwarecatalogus_register.json | 54 +-- src/store/modules/settings.js | 290 +++++++--------- .../sections/OpenRegisterIntegration.vue | 316 ++++++------------ 4 files changed, 357 insertions(+), 556 deletions(-) diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php index 5a22fb7f..c3915940 100644 --- a/lib/Service/SettingsService.php +++ b/lib/Service/SettingsService.php @@ -162,12 +162,12 @@ public function getSettings(): array 'amef' => [ 'name' => 'AMEF', 'description' => 'AMEF register for architectural elements', - 'objectTypes' => ['organization', 'element', 'relationship', 'view', 'model', 'property'] // Complete AMEF object types + 'objectTypes' => ['organization', 'element', 'relation', 'view', 'model', 'property', 'property-definition'] // Complete AMEF object types ], 'voorzieningen' => [ 'name' => 'Voorzieningen', 'description' => 'Voorzieningen register for software catalog services', - 'objectTypes' => ['organisatie', 'contactpersoon'] // Voorzieningen uses organisatie and contactpersoon schemas + 'objectTypes' => ['sector', 'product', 'dienst', 'kwetsbaarheid', 'contactpersoon', 'organisatie', 'gebruik', 'contract', 'koppeling', 'beoordeeling', 'module', 'compliancy', 'moduleVersie'] // All voorzieningen schemas ] ]; @@ -305,7 +305,7 @@ public function autoConfigure(bool $force = false): array * Auto-configures settings specifically after importing the softwarecatalogus_register.json * * This method looks for the voorzieningen register and automatically configures - * the organisatie and contactpersoon schemas, and creates required user groups. + * ALL schemas using the new consolidated configuration format, and creates required user groups. * * @return array The updated configuration * @@ -321,118 +321,52 @@ public function autoConfigureAfterImport(): array return []; } - $objectService = $this->getObjectService(); - $registers = $objectService->getRegisters(); - - if (empty($registers)) { - $this->logger->info('No registers available for auto-configuration after import'); - return []; - } - - $configuration = []; + $this->logger->info('Starting comprehensive auto-configuration after import'); // Step 1: Create required user groups $this->logger->info('Creating required user groups'); $this->createRequiredUserGroups(); $this->logger->info('User groups created successfully'); - // Look for the voorzieningen register - $voorzieningenRegister = null; - foreach ($registers as $register) { - $registerTitle = strtolower($register['title'] ?? ''); - $registerSlug = strtolower($register['slug'] ?? ''); - - if (stripos($registerTitle, 'voorzieningen') !== false || - stripos($registerSlug, 'voorzieningen') !== false || - $registerTitle === 'voorzieningen' || - $registerSlug === 'voorzieningen') { - $voorzieningenRegister = $register; - break; - } - } - - if ($voorzieningenRegister === null) { - $this->logger->info('No voorzieningen register found for auto-configuration after import'); + // Step 2: Configure Voorzieningen using the consolidated method + $this->logger->info('Running voorzieningen auto-configuration'); + $voorzieningenResult = $this->configureVoorzieningen(); + + if (!$voorzieningenResult['success']) { + $this->logger->warning('Voorzieningen auto-configuration failed', [ + 'message' => $voorzieningenResult['message'] ?? 'Unknown error' + ]); return []; } - $this->logger->info('Found voorzieningen register for auto-configuration', [ - 'register_id' => $voorzieningenRegister['id'], - 'register_title' => $voorzieningenRegister['title'], - 'schemas_count' => count($voorzieningenRegister['schemas'] ?? []) + $this->logger->info('Voorzieningen auto-configuration completed successfully', [ + 'configured' => $voorzieningenResult['configured'] ?? [] ]); - // Configure schemas within the voorzieningen register - if (!empty($voorzieningenRegister['schemas'])) { - foreach ($voorzieningenRegister['schemas'] as $schema) { - $schemaTitle = strtolower($schema['title'] ?? ''); - $schemaSlug = strtolower($schema['slug'] ?? ''); - - // Look for organisatie schema - if (stripos($schemaTitle, 'organisatie') !== false || - stripos($schemaSlug, 'organisatie') !== false || - $schemaTitle === 'organisatie' || - $schemaSlug === 'organisatie') { - - // Set voorzieningen_organisatie configuration - $configuration['voorzieningen_organisatie_source'] = 'openregister'; - $configuration['voorzieningen_organisatie_register'] = (string) $voorzieningenRegister['id']; - $configuration['voorzieningen_organisatie_schema'] = (string) $schema['id']; - - // Set sync-compatible configuration (OrganizationSyncService expects this key) - $configuration['voorzieningen_register'] = (string) $voorzieningenRegister['id']; - - // Also set backward compatibility organization configuration - $configuration['organization_source'] = 'openregister'; - $configuration['organization_register'] = (string) $voorzieningenRegister['id']; - $configuration['organization_schema'] = (string) $schema['id']; - - $this->logger->info('Configured organisatie schema', [ - 'schema_id' => $schema['id'], - 'schema_title' => $schema['title'] - ]); - } - // Look for contactpersoon schema - else if (stripos($schemaTitle, 'contactpersoon') !== false || - stripos($schemaSlug, 'contactpersoon') !== false || - $schemaTitle === 'contactpersoon' || - $schemaSlug === 'contactpersoon') { - - // Set voorzieningen_contactpersoon configuration - $configuration['voorzieningen_contactpersoon_source'] = 'openregister'; - $configuration['voorzieningen_contactpersoon_register'] = (string) $voorzieningenRegister['id']; - $configuration['voorzieningen_contactpersoon_schema'] = (string) $schema['id']; - - // Set sync-compatible configuration (OrganizationSyncService expects this key) - $configuration['voorzieningen_register'] = (string) $voorzieningenRegister['id']; - - // Also set backward compatibility contact configuration - $configuration['contact_source'] = 'openregister'; - $configuration['contact_register'] = (string) $voorzieningenRegister['id']; - $configuration['contact_schema'] = (string) $schema['id']; - - $this->logger->info('Configured contactpersoon schema', [ - 'schema_id' => $schema['id'], - 'schema_title' => $schema['title'] - ]); - } - } - } - - if (empty($configuration)) { - $this->logger->info('No matching schemas found in voorzieningen register for auto-configuration'); + // Step 3: Configure AMEF using the consolidated method + $this->logger->info('Running AMEF auto-configuration'); + $amefResult = $this->configureAmef(); + + if (!$amefResult['success']) { + $this->logger->info('AMEF auto-configuration not completed', [ + 'message' => $amefResult['message'] ?? 'No AMEF register found' + ]); } else { - $this->logger->info('Auto-configuration after import completed successfully', [ - 'configuration_keys' => array_keys($configuration), - 'register_used' => $voorzieningenRegister['title'] + $this->logger->info('AMEF auto-configuration completed successfully', [ + 'configured' => $amefResult['configured'] ?? [] ]); } // Mark auto-configuration as completed $this->config->setValueString($this->_appName, 'auto_config_completed', 'true'); - $this->logger->info('Auto-configuration marked as completed'); + $this->logger->info('Comprehensive auto-configuration marked as completed'); - return $configuration; + // Return the consolidated configuration result + return [ + 'voorzieningen' => $voorzieningenResult, + 'amef' => $amefResult, + 'user_groups_created' => true + ]; } catch (\Exception $e) { throw new \RuntimeException('Failed to auto-configure after import: ' . $e->getMessage()); @@ -2664,10 +2598,8 @@ private function configureVoorzieningen(): array // Find the voorzieningen register by slug OR by presence of expected schema slugs $targetRegister = null; $expectedSlugs = [ - 'organisatie', 'contactpersoon', 'voorziening', 'voorzieningaanbod', 'voorzieningversie', - 'kwetsbaarheid', 'contract', 'standaard', 'review', 'koppeling', 'beoordeeling', - 'voorzieningmodule', 'verklaring', 'koppelinggebruik', 'compliancy', 'modulegebruik', - 'moduleversie', 'sector' + 'sector', 'product', 'dienst', 'kwetsbaarheid', 'contactpersoon', 'organisatie', + 'gebruik', 'contract', 'koppeling', 'beoordeeling', 'module', 'compliancy', 'moduleversie', 'moduleVersie' ]; foreach ($registers as $register) { @@ -2691,35 +2623,72 @@ private function configureVoorzieningen(): array ]; } - // Map schema slugs to configuration keys (singular slug + _schema) + // Map schema slugs to configuration keys based on actual register schemas $slugToKey = [ 'organisatie' => 'organisatie_schema', 'contactpersoon' => 'contactpersoon_schema', - 'voorziening' => 'voorziening_schema', - 'voorzieningaanbod' => 'voorziening_aanbod_schema', - 'voorzieningversie' => 'voorziening_versie_schema', + 'product' => 'product_schema', + 'dienst' => 'dienst_schema', 'kwetsbaarheid' => 'kwetsbaarheid_schema', + 'gebruik' => 'gebruik_schema', 'contract' => 'contract_schema', - 'standaard' => 'standaard_schema', - 'review' => 'review_schema', 'koppeling' => 'koppeling_schema', 'beoordeeling' => 'beoordeeling_schema', - 'voorzieningmodule' => 'voorziening_module_schema', - 'verklaring' => 'verklaring_schema', - 'koppelinggebruik' => 'koppeling_gebruik_schema', + 'module' => 'module_schema', 'compliancy' => 'compliancy_schema', - 'modulegebruik' => 'module_gebruik_schema', - 'moduleversie' => 'module_versie_schema', + 'moduleversie' => 'moduleVersie_schema', // Handle both moduleversie and moduleVersie + 'moduleVersie' => 'moduleVersie_schema', 'sector' => 'sector_schema', ]; $config = [ 'register' => (string)($targetRegister['id'] ?? '') ]; + + $this->logger->info('DEBUG: About to process schemas', [ + 'register_id' => $targetRegister['id'], + 'schemas_count' => count($targetRegister['schemas'] ?? []), + 'slugToKey_map' => $slugToKey + ]); + foreach (($targetRegister['schemas'] ?? []) as $schema) { - $schemaSlug = strtolower($schema['slug'] ?? ''); - if (isset($slugToKey[$schemaSlug])) { - $config[$slugToKey[$schemaSlug]] = (string)$schema['id']; + $originalSlug = $schema['slug'] ?? ''; + $lowercaseSlug = strtolower($originalSlug); + + $this->logger->info('DEBUG: Processing schema', [ + 'original_slug' => $originalSlug, + 'lowercase_slug' => $lowercaseSlug, + 'schema_id' => $schema['id'] ?? 'NO_ID', + 'has_mapping_original' => isset($slugToKey[$originalSlug]) ? 'YES' : 'NO', + 'has_mapping_lowercase' => isset($slugToKey[$lowercaseSlug]) ? 'YES' : 'NO' + ]); + + $mappingKey = null; + $usedSlug = null; + + // Try original case first, then lowercase + if (isset($slugToKey[$originalSlug])) { + $mappingKey = $slugToKey[$originalSlug]; + $usedSlug = $originalSlug; + } elseif (isset($slugToKey[$lowercaseSlug])) { + $mappingKey = $slugToKey[$lowercaseSlug]; + $usedSlug = $lowercaseSlug; + } + + if ($mappingKey !== null) { + $config[$mappingKey] = (string)$schema['id']; + $this->logger->info('DEBUG: Mapped schema successfully', [ + 'used_slug' => $usedSlug, + 'config_key' => $mappingKey, + 'schema_id' => $schema['id'] + ]); + } else { + $this->logger->debug('DEBUG: No mapping found for schema slug', [ + 'original_slug' => $originalSlug, + 'lowercase_slug' => $lowercaseSlug + ]); } } + + $this->logger->info('DEBUG: Final config before persist', ['config' => $config]); // Persist normalized config $this->setVoorzieningenConfig($config); @@ -2795,16 +2764,17 @@ private function configureAmef(): array 'relation_schema' => '', 'view_schema' => '', 'model_schema' => '', - 'property-definition_schema' => '', + 'property_definition_schema' => '', 'property_schema' => '', - 'extendview_schema' => '', ]; foreach (($targetRegister['schemas'] ?? []) as $schema) { $slug = strtolower($schema['slug'] ?? ''); - $allowed = ['organization','element','relation','view','model','property','property-definition','extendview']; + $allowed = ['organization','element','relation','view','model','property','property-definition']; if (in_array($slug, $allowed, true)) { - $config[$slug . '_schema'] = (string)$schema['id']; + // Handle property-definition schema with underscore in config key + $configKey = $slug === 'property-definition' ? 'property_definition_schema' : $slug . '_schema'; + $config[$configKey] = (string)$schema['id']; } } @@ -2960,27 +2930,21 @@ private function normalizeVoorzieningenConfig(array $input): array // Register id $normalized['register'] = isset($input['register']) ? (string)$input['register'] : ''; - // Known schema keys to support (19 total) + // Known schema keys to support - updated to match actual schemas from register $schemaKeys = [ 'organisatie_schema', 'contactpersoon_schema', - 'voorziening_schema', - 'voorziening_aanbod_schema', - 'voorziening_versie_schema', + 'product_schema', + 'dienst_schema', 'kwetsbaarheid_schema', + 'gebruik_schema', 'contract_schema', - 'standaard_schema', - 'review_schema', 'koppeling_schema', 'beoordeeling_schema', - 'voorziening_module_schema', - 'verklaring_schema', - 'koppeling_gebruik_schema', + 'module_schema', 'compliancy_schema', - 'module_gebruik_schema', - 'module_versie_schema', + 'moduleVersie_schema', 'sector_schema', - 'gebruik_schema', ]; // Copy any present schema keys; ignore sources/registers @@ -3625,7 +3589,7 @@ public function cleanupOldConfiguration(): array try { // List of old configuration keys to remove $oldKeys = [ - // Voorzieningen keys + // Voorzieningen keys - old individual keys 'voorzieningen_register', 'voorzieningen_organisatie_schema', 'voorzieningen_contactpersoon_schema', @@ -3639,19 +3603,38 @@ public function cleanupOldConfiguration(): array 'voorzieningen_contactpersoon_register', 'voorzieningen_gebruiker_register', // Deprecated - no longer used 'voorzieningen_contactgegevens_register', // Deprecated - no longer used - - // AMEF keys + + // Old Voorzieningen schema keys that no longer exist in register + 'voorzieningen_voorziening_schema', + 'voorzieningen_voorziening_aanbod_schema', + 'voorzieningen_voorziening_versie_schema', + 'voorzieningen_standaard_schema', + 'voorzieningen_review_schema', + 'voorzieningen_voorziening_module_schema', + 'voorzieningen_verklaring_schema', + 'voorzieningen_koppeling_gebruik_schema', + 'voorzieningen_module_gebruik_schema', + 'voorzieningen_module_versie_schema', + + // AMEF keys - old individual keys 'amef_register_id', 'amef_organizations_schema', 'amef_elements_schema', 'amef_relationships_schema', 'amef_views_schema', + 'amef_models_schema', + 'amef_properties_schema', + 'amef_property_definitions_schema', 'amef_organization_source', 'amef_organization_register', 'amef_organization_schema', 'amef_elementss_schema', 'amef_organizationss_schema', 'amef_relationshipss_schema', + + // AMEF keys with hyphen format (old) + 'amef_property-definition_schema', + 'amef_extendview_schema', // No longer in register // Email keys 'email_enabled', @@ -4152,7 +4135,7 @@ public function updateAmefConfig(array $config): array 'relation_schema', 'view_schema', 'model_schema', - 'property-definition_schema', + 'property_definition_schema', 'property_schema', ]; diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index c618bc8d..73daef80 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -79,51 +79,6 @@ "created": "2025-05-16T13:11:49+00:00", "updated": "2025-05-16T13:38:20+00:00" }, - "elements": { - "name": "Elements", - "description": "", - "reference": "https://vng.accept.commonground.nu/Endpoint-Elements.json", - "version": "0.0.1", - "endpoint": "elements", - "endpointArray": [ - "elements" - ], - "endpointRegex": "#^elements$#", - "method": "GET", - "targetType": "register/schema", - "targetId": "vng-gemma/element", - "conditions": [], - "inputMapping": null, - "outputMapping": null, - "rules": [], - "configurations": [], - "slug": "elements", - "created": "2025-05-01T14:48:08+00:00", - "updated": "2025-05-02T10:37:48+00:00" - }, - "element": { - "name": "Element", - "description": "", - "reference": "https://vng.accept.commonground.nu/Endpoint-Element.json", - "version": "0.0.1", - "endpoint": "elements/{{id}}", - "endpointArray": [ - "elements", - "{{id}}" - ], - "endpointRegex": "#^elements(/([^/]+))$#", - "method": "GET", - "targetType": "register/schema", - "targetId": "vng-gemma/element", - "conditions": [], - "inputMapping": null, - "outputMapping": null, - "rules": [], - "configurations": [], - "slug": "element", - "created": "2025-05-01T14:48:08+00:00", - "updated": "2025-05-02T10:37:52+00:00" - }, "views": { "name": "Views", "description": "", @@ -496,15 +451,18 @@ }, "cloudDienstverleningsmodel": { "description": "Het cloud dienstverleningsmodel voor het product, suite of monobrand", - "type": "string", + "type": "array", + "items": { + "type": "string", + "enum": ["On-premises (self-managed)", "IaaS", "PaaS", "SaaS"] + }, "order": 0, "objectConfiguration": {}, "fileConfiguration": {}, "oneOf": [], - "enum": ["On-premises (self-managed)", "IaaS", "PaaS", "SaaS"], "facetable": true, "title": "Hosting vorm", - "example": "Bijvoorbeeld: SaaS" + "example": ["SaaS"] }, "hostingJurisdictie": { "description": "De jurisdictie waar de hosting plaatsvindt", diff --git a/src/store/modules/settings.js b/src/store/modules/settings.js index d572ba41..a4c50235 100644 --- a/src/store/modules/settings.js +++ b/src/store/modules/settings.js @@ -49,34 +49,28 @@ export const useSettingsStore = defineStore('settings', { // Configuration configuration: { - // AMEF register configuration - amef_elements: { schema: null }, - amef_organization: { schema: null }, - amef_relationships: { schema: null }, - amef_views: { schema: null }, - amef_models: { schema: null }, - amef_properties: { schema: null }, - amef_property_definitions: { schema: null }, - // Voorzieningen register configuration - voorzieningen_organisatie: { schema: null }, - voorzieningen_contactpersoon: { schema: null }, - // Extended schemas - voorzieningen_voorziening: { schema: null }, - voorzieningen_voorziening_aanbod: { schema: null }, - voorzieningen_voorziening_versie: { schema: null }, - voorzieningen_kwetsbaarheid: { schema: null }, - voorzieningen_contract: { schema: null }, - voorzieningen_standaard: { schema: null }, - voorzieningen_review: { schema: null }, - voorzieningen_koppeling: { schema: null }, - voorzieningen_beoordeeling: { schema: null }, - voorzieningen_voorziening_module: { schema: null }, - voorzieningen_verklaring: { schema: null }, - voorzieningen_koppeling_gebruik: { schema: null }, - voorzieningen_compliancy: { schema: null }, - voorzieningen_module_gebruik: { schema: null }, - voorzieningen_module_versie: { schema: null }, - voorzieningen_sector: { schema: null }, + // AMEF register configuration - updated to match current schemas + amef_element_schema: { schema: null }, + amef_organization_schema: { schema: null }, + amef_relation_schema: { schema: null }, + amef_view_schema: { schema: null }, + amef_model_schema: { schema: null }, + amef_property_schema: { schema: null }, + amef_property_definition_schema: { schema: null }, + // Voorzieningen register configuration - updated to match current schemas + voorzieningen_sector_schema: { schema: null }, + voorzieningen_product_schema: { schema: null }, + voorzieningen_dienst_schema: { schema: null }, + voorzieningen_kwetsbaarheid_schema: { schema: null }, + voorzieningen_contactpersoon_schema: { schema: null }, + voorzieningen_organisatie_schema: { schema: null }, + voorzieningen_gebruik_schema: { schema: null }, + voorzieningen_contract_schema: { schema: null }, + voorzieningen_koppeling_schema: { schema: null }, + voorzieningen_beoordeeling_schema: { schema: null }, + voorzieningen_module_schema: { schema: null }, + voorzieningen_compliancy_schema: { schema: null }, + voorzieningen_moduleVersie_schema: { schema: null }, }, // ArchiMate status and operations @@ -360,6 +354,7 @@ export const useSettingsStore = defineStore('settings', { } this.loading = true this.loadingMainSettings = true + this.loadingOpenRegisterConfig = true this.clearError() try { @@ -405,6 +400,7 @@ export const useSettingsStore = defineStore('settings', { } finally { this.loading = false this.loadingMainSettings = false + this.loadingOpenRegisterConfig = false } }, @@ -695,33 +691,28 @@ export const useSettingsStore = defineStore('settings', { initializeConfiguration() { // Initialize register-specific configuration this.configuration = { - // AMEF register configuration - amef_elements: { schema: null }, - amef_organization: { schema: null }, - amef_relationships: { schema: null }, - amef_views: { schema: null }, - amef_models: { schema: null }, - amef_properties: { schema: null }, - amef_property_definitions: { schema: null }, - // Voorzieningen register configuration - voorzieningen_organisatie: { schema: null }, - voorzieningen_contactpersoon: { schema: null }, - voorzieningen_voorziening: { schema: null }, - voorzieningen_voorziening_aanbod: { schema: null }, - voorzieningen_voorziening_versie: { schema: null }, - voorzieningen_kwetsbaarheid: { schema: null }, - voorzieningen_contract: { schema: null }, - voorzieningen_standaard: { schema: null }, - voorzieningen_review: { schema: null }, - voorzieningen_koppeling: { schema: null }, - voorzieningen_beoordeeling: { schema: null }, - voorzieningen_voorziening_module: { schema: null }, - voorzieningen_verklaring: { schema: null }, - voorzieningen_koppeling_gebruik: { schema: null }, - voorzieningen_compliancy: { schema: null }, - voorzieningen_module_gebruik: { schema: null }, - voorzieningen_module_versie: { schema: null }, - voorzieningen_sector: { schema: null }, + // AMEF register configuration - updated to match current schemas + amef_element_schema: { schema: null }, + amef_organization_schema: { schema: null }, + amef_relation_schema: { schema: null }, + amef_view_schema: { schema: null }, + amef_model_schema: { schema: null }, + amef_property_schema: { schema: null }, + amef_property_definition_schema: { schema: null }, + // Voorzieningen register configuration - updated to match current schemas + voorzieningen_sector_schema: { schema: null }, + voorzieningen_product_schema: { schema: null }, + voorzieningen_dienst_schema: { schema: null }, + voorzieningen_kwetsbaarheid_schema: { schema: null }, + voorzieningen_contactpersoon_schema: { schema: null }, + voorzieningen_organisatie_schema: { schema: null }, + voorzieningen_gebruik_schema: { schema: null }, + voorzieningen_contract_schema: { schema: null }, + voorzieningen_koppeling_schema: { schema: null }, + voorzieningen_beoordeeling_schema: { schema: null }, + voorzieningen_module_schema: { schema: null }, + voorzieningen_compliancy_schema: { schema: null }, + voorzieningen_moduleVersie_schema: { schema: null }, } }, @@ -765,27 +756,22 @@ export const useSettingsStore = defineStore('settings', { return options.find(o => o && o.value && o.value.toString() === id) || null } - // Voorzieningen schemas + // Voorzieningen schemas - updated mapping to match current schema structure const vc = this.voorzieningenRawConfig || {} const vMap = [ - ['organisatie_schema', 'voorzieningen_organisatie'], - ['contactpersoon_schema', 'voorzieningen_contactpersoon'], - ['voorziening_schema', 'voorzieningen_voorziening'], - ['voorziening_aanbod_schema', 'voorzieningen_voorziening_aanbod'], - ['voorziening_versie_schema', 'voorzieningen_voorziening_versie'], - ['kwetsbaarheid_schema', 'voorzieningen_kwetsbaarheid'], - ['contract_schema', 'voorzieningen_contract'], - ['standaard_schema', 'voorzieningen_standaard'], - ['review_schema', 'voorzieningen_review'], - ['koppeling_schema', 'voorzieningen_koppeling'], - ['beoordeeling_schema', 'voorzieningen_beoordeeling'], - ['voorziening_module_schema', 'voorzieningen_voorziening_module'], - ['verklaring_schema', 'voorzieningen_verklaring'], - ['koppeling_gebruik_schema', 'voorzieningen_koppeling_gebruik'], - ['compliancy_schema', 'voorzieningen_compliancy'], - ['module_gebruik_schema', 'voorzieningen_module_gebruik'], - ['module_versie_schema', 'voorzieningen_module_versie'], - ['sector_schema', 'voorzieningen_sector'], + ['sector_schema', 'voorzieningen_sector_schema'], + ['product_schema', 'voorzieningen_product_schema'], + ['dienst_schema', 'voorzieningen_dienst_schema'], + ['kwetsbaarheid_schema', 'voorzieningen_kwetsbaarheid_schema'], + ['contactpersoon_schema', 'voorzieningen_contactpersoon_schema'], + ['organisatie_schema', 'voorzieningen_organisatie_schema'], + ['gebruik_schema', 'voorzieningen_gebruik_schema'], + ['contract_schema', 'voorzieningen_contract_schema'], + ['koppeling_schema', 'voorzieningen_koppeling_schema'], + ['beoordeeling_schema', 'voorzieningen_beoordeeling_schema'], + ['module_schema', 'voorzieningen_module_schema'], + ['compliancy_schema', 'voorzieningen_compliancy_schema'], + ['moduleVersie_schema', 'voorzieningen_moduleVersie_schema'], ] vMap.forEach(([cfgKey, uiKey]) => { if (vc[cfgKey]) { @@ -796,35 +782,35 @@ export const useSettingsStore = defineStore('settings', { } }) - // AMEF schemas (singular keys) + // AMEF schemas - updated to match new key structure const ac = this.amefRawConfig || {} if (ac.organization_schema || ac.organizations_schema) { const opt = findOption((ac.organization_schema || ac.organizations_schema), this.amefSchemaOptions) - if (opt) this.configuration.amef_organization.schema = opt + if (opt) this.configuration.amef_organization_schema.schema = opt } if (ac.element_schema || ac.elements_schema) { const opt = findOption((ac.element_schema || ac.elements_schema), this.amefSchemaOptions) - if (opt) this.configuration.amef_elements.schema = opt + if (opt) this.configuration.amef_element_schema.schema = opt } if (ac.relation_schema || ac.relationships_schema) { const opt = findOption((ac.relation_schema || ac.relationships_schema), this.amefSchemaOptions) - if (opt) this.configuration.amef_relationships.schema = opt + if (opt) this.configuration.amef_relation_schema.schema = opt } if (ac.view_schema || ac.views_schema) { const opt = findOption((ac.view_schema || ac.views_schema), this.amefSchemaOptions) - if (opt) this.configuration.amef_views.schema = opt + if (opt) this.configuration.amef_view_schema.schema = opt } if (ac.model_schema || ac.models_schema) { const opt = findOption((ac.model_schema || ac.models_schema), this.amefSchemaOptions) - if (opt) this.configuration.amef_models.schema = opt + if (opt) this.configuration.amef_model_schema.schema = opt } if (ac.property_schema || ac.properties_schema) { const opt = findOption((ac.property_schema || ac.properties_schema), this.amefSchemaOptions) - if (opt) this.configuration.amef_properties.schema = opt + if (opt) this.configuration.amef_property_schema.schema = opt } if (ac['property-definition_schema'] || ac.property_definitions_schema) { const opt = findOption((ac['property-definition_schema'] || ac.property_definitions_schema), this.amefSchemaOptions) - if (opt) this.configuration.amef_property_definitions.schema = opt + if (opt) this.configuration.amef_property_definition_schema.schema = opt } }, @@ -1095,23 +1081,23 @@ export const useSettingsStore = defineStore('settings', { // Save AMEF configuration (clean payload) const amefConfig = {} const amefKeys = [ - 'amef_elements', - 'amef_organization', - 'amef_relationships', - 'amef_views', - 'amef_models', - 'amef_properties', - 'amef_property_definitions', + 'amef_element_schema', + 'amef_organization_schema', + 'amef_relation_schema', + 'amef_view_schema', + 'amef_model_schema', + 'amef_property_schema', + 'amef_property_definition_schema', ] // Map UI keys to API keys const amefMap = { - amef_organization: 'organization_schema', - amef_elements: 'element_schema', - amef_relationships: 'relation_schema', - amef_views: 'view_schema', - amef_models: 'model_schema', - amef_properties: 'property_schema', - amef_property_definitions: 'property-definition_schema', + amef_organization_schema: 'organization_schema', + amef_element_schema: 'element_schema', + amef_relation_schema: 'relation_schema', + amef_view_schema: 'view_schema', + amef_model_schema: 'model_schema', + amef_property_schema: 'property_schema', + amef_property_definition_schema: 'property-definition_schema', } if (this.amefRegister?.value) { amefConfig.register = this.amefRegister.value @@ -1139,45 +1125,35 @@ export const useSettingsStore = defineStore('settings', { // Save Voorzieningen configuration (clean payload) const voorzieningenConfig = {} const voorzieningenKeys = [ - 'voorzieningen_organisatie', - 'voorzieningen_contactpersoon', - 'voorzieningen_voorziening', - 'voorzieningen_voorziening_aanbod', - 'voorzieningen_voorziening_versie', - 'voorzieningen_kwetsbaarheid', - 'voorzieningen_contract', - 'voorzieningen_standaard', - 'voorzieningen_review', - 'voorzieningen_koppeling', - 'voorzieningen_beoordeeling', - 'voorzieningen_voorziening_module', - 'voorzieningen_verklaring', - 'voorzieningen_koppeling_gebruik', - 'voorzieningen_compliancy', - 'voorzieningen_module_gebruik', - 'voorzieningen_module_versie', - 'voorzieningen_sector', + 'voorzieningen_sector_schema', + 'voorzieningen_product_schema', + 'voorzieningen_dienst_schema', + 'voorzieningen_kwetsbaarheid_schema', + 'voorzieningen_contactpersoon_schema', + 'voorzieningen_organisatie_schema', + 'voorzieningen_gebruik_schema', + 'voorzieningen_contract_schema', + 'voorzieningen_koppeling_schema', + 'voorzieningen_beoordeeling_schema', + 'voorzieningen_module_schema', + 'voorzieningen_compliancy_schema', + 'voorzieningen_moduleVersie_schema', ] // Map UI keys to API keys const vzMap = { - voorzieningen_organisatie: 'organisatie_schema', - voorzieningen_contactpersoon: 'contactpersoon_schema', - voorzieningen_voorziening: 'voorziening_schema', - voorzieningen_voorziening_aanbod: 'voorziening_aanbod_schema', - voorzieningen_voorziening_versie: 'voorziening_versie_schema', - voorzieningen_kwetsbaarheid: 'kwetsbaarheid_schema', - voorzieningen_contract: 'contract_schema', - voorzieningen_standaard: 'standaard_schema', - voorzieningen_review: 'review_schema', - voorzieningen_koppeling: 'koppeling_schema', - voorzieningen_beoordeeling: 'beoordeeling_schema', - voorzieningen_voorziening_module: 'voorziening_module_schema', - voorzieningen_verklaring: 'verklaring_schema', - voorzieningen_koppeling_gebruik: 'koppeling_gebruik_schema', - voorzieningen_compliancy: 'compliancy_schema', - voorzieningen_module_gebruik: 'module_gebruik_schema', - voorzieningen_module_versie: 'module_versie_schema', - voorzieningen_sector: 'sector_schema', + voorzieningen_sector_schema: 'sector_schema', + voorzieningen_product_schema: 'product_schema', + voorzieningen_dienst_schema: 'dienst_schema', + voorzieningen_kwetsbaarheid_schema: 'kwetsbaarheid_schema', + voorzieningen_contactpersoon_schema: 'contactpersoon_schema', + voorzieningen_organisatie_schema: 'organisatie_schema', + voorzieningen_gebruik_schema: 'gebruik_schema', + voorzieningen_contract_schema: 'contract_schema', + voorzieningen_koppeling_schema: 'koppeling_schema', + voorzieningen_beoordeeling_schema: 'beoordeeling_schema', + voorzieningen_module_schema: 'module_schema', + voorzieningen_compliancy_schema: 'compliancy_schema', + voorzieningen_moduleVersie_schema: 'moduleVersie_schema', } if (this.voorzieningenRegister?.value) { voorzieningenConfig.register = this.voorzieningenRegister.value @@ -1623,34 +1599,28 @@ export const useSettingsStore = defineStore('settings', { this.voorzieningenSchemas = [] this.amefSchemas = [] this.configuration = { - // AMEF register configuration - amef_elements: { schema: null }, - amef_organization: { schema: null }, - amef_relationships: { schema: null }, - amef_views: { schema: null }, - amef_models: { schema: null }, - amef_properties: { schema: null }, - amef_property_definitions: { schema: null }, - // Voorzieningen register configuration - voorzieningen_organisatie: { schema: null }, - voorzieningen_contactpersoon: { schema: null }, - // Extended schemas - voorzieningen_voorziening: { schema: null }, - voorzieningen_voorziening_aanbod: { schema: null }, - voorzieningen_voorziening_versie: { schema: null }, - voorzieningen_kwetsbaarheid: { schema: null }, - voorzieningen_contract: { schema: null }, - voorzieningen_standaard: { schema: null }, - voorzieningen_review: { schema: null }, - voorzieningen_koppeling: { schema: null }, - voorzieningen_beoordeeling: { schema: null }, - voorzieningen_voorziening_module: { schema: null }, - voorzieningen_verklaring: { schema: null }, - voorzieningen_koppeling_gebruik: { schema: null }, - voorzieningen_compliancy: { schema: null }, - voorzieningen_module_gebruik: { schema: null }, - voorzieningen_module_versie: { schema: null }, - voorzieningen_sector: { schema: null }, + // AMEF register configuration - updated to match current schemas + amef_element_schema: { schema: null }, + amef_organization_schema: { schema: null }, + amef_relation_schema: { schema: null }, + amef_view_schema: { schema: null }, + amef_model_schema: { schema: null }, + amef_property_schema: { schema: null }, + amef_property_definition_schema: { schema: null }, + // Voorzieningen register configuration - updated to match current schemas + voorzieningen_sector_schema: { schema: null }, + voorzieningen_product_schema: { schema: null }, + voorzieningen_dienst_schema: { schema: null }, + voorzieningen_kwetsbaarheid_schema: { schema: null }, + voorzieningen_contactpersoon_schema: { schema: null }, + voorzieningen_organisatie_schema: { schema: null }, + voorzieningen_gebruik_schema: { schema: null }, + voorzieningen_contract_schema: { schema: null }, + voorzieningen_koppeling_schema: { schema: null }, + voorzieningen_beoordeeling_schema: { schema: null }, + voorzieningen_module_schema: { schema: null }, + voorzieningen_compliancy_schema: { schema: null }, + voorzieningen_moduleVersie_schema: { schema: null }, } this.archimateStatus = { import: {}, diff --git a/src/views/settings/sections/OpenRegisterIntegration.vue b/src/views/settings/sections/OpenRegisterIntegration.vue index e1eaf5ad..86bf450f 100644 --- a/src/views/settings/sections/OpenRegisterIntegration.vue +++ b/src/views/settings/sections/OpenRegisterIntegration.vue @@ -17,38 +17,18 @@ -->