Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

!!! FEATURE: Extract workspace metadata and user-assignment to Neos #5146

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
5f1e296
FEATURE: Extract workspace metadata and user-assignment to Neos
bwaidelich Jun 17, 2024
113e0b6
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Jun 28, 2024
773de1a
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Jul 4, 2024
8975a5f
wip
bwaidelich Jul 8, 2024
9c74b65
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Aug 2, 2024
4326424
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Aug 7, 2024
54df32a
Mini tweaks
bwaidelich Aug 9, 2024
dccfb50
wip
bwaidelich Aug 16, 2024
723f26c
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Sep 6, 2024
0af1fa2
WIP: Fix WorkspaceService as discussed with basti & christian
mhsdesign Sep 9, 2024
80e3aef
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Sep 10, 2024
e705924
Fix WorkspaceService::createRootWorkspace()
bwaidelich Sep 10, 2024
11aa9b3
Use `WorkspaceService` in workspace::createroot CLI command
bwaidelich Sep 10, 2024
bdfb954
Stabilize unique workspace name creation
bwaidelich Sep 10, 2024
cf95ee2
Merge remote-tracking branch 'origin/9.0' into feature/4726-extract-w…
mhsdesign Sep 17, 2024
0f20b89
Provide CLI to synchronize workspace metadata and roles
bwaidelich Sep 19, 2024
a467296
Stabilize and document WorkspaceService
bwaidelich Sep 23, 2024
616852e
Fix workspace role handling
bwaidelich Sep 25, 2024
5d448c8
Improve "pending changes" projection and read model
bwaidelich Sep 25, 2024
fd1a616
Cosmetic tweaks to satisfy linter
bwaidelich Sep 25, 2024
b70fb60
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Sep 25, 2024
fe34b60
Move owner assignment from role to metadata, fix migration
bwaidelich Sep 27, 2024
96c427b
Remove UI specific publishable node info array from `Changes` VO
bwaidelich Sep 27, 2024
c3f51e2
Make `WorkspaceNameBuilder` obsolete and remove it
bwaidelich Sep 27, 2024
732b81a
Remove workspace related left-overs from `UserService`
bwaidelich Sep 27, 2024
8f87ddf
Make `Utility\User` class obsolete and remove it
bwaidelich Sep 30, 2024
cbe3ba5
Rework migration to work on the database table directly
bwaidelich Sep 30, 2024
22d8d3a
TASK: Make migration more stable by not relying on the projection but…
mhsdesign Sep 30, 2024
1e1849d
Optimize `migrateWorkspaceMetadataToWorkspaceService` migration
bwaidelich Oct 1, 2024
a27fb02
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Oct 1, 2024
b84ae90
TASK: Adjust deprecations
mhsdesign Oct 1, 2024
702d749
TASK: Deprecate `UserInterface`
mhsdesign Oct 1, 2024
3410ca7
TASK: Add comment for `$Persistence_Object_Identifier` "hack"
mhsdesign Oct 1, 2024
686ff3d
TASK: Improve exceptions
mhsdesign Oct 1, 2024
5c3e817
TASK: Make migration independent of workspaceService
mhsdesign Oct 1, 2024
721b413
TASK: Add missing `@internal` annotations
mhsdesign Oct 1, 2024
e64ff93
TASK: Remove obsolete todos
mhsdesign Oct 1, 2024
88e344d
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Oct 7, 2024
25e9957
Rename `WorkspaceSubjectType` to `WorkspaceRoleSubjectType`
bwaidelich Oct 8, 2024
2dde2cf
Make `WorkspaceService` more explicit
bwaidelich Oct 8, 2024
1e3e2f7
Workspace UI: Workspace role preset
bwaidelich Oct 8, 2024
904c716
`WorkspacePermissions` doc comments
bwaidelich Oct 8, 2024
8bafb5a
Add more doc comments
bwaidelich Oct 8, 2024
f68300a
wip
bwaidelich Oct 8, 2024
7d6a819
Fix and improve Behat tests for WorkspaceService
bwaidelich Oct 9, 2024
3b3d718
Remove `workspaceName` from `WorkspaceMetadata`
bwaidelich Oct 9, 2024
e284cf0
Fix typo in private method
bwaidelich Oct 9, 2024
cc85cbf
TASK: Improve documentation
mhsdesign Oct 9, 2024
02d9c97
Merge branch 'feature/4726-extract-workspace-metadata-and-user-assign…
bwaidelich Oct 9, 2024
5fc33d5
Allow to determine Workspace role assignments
bwaidelich Oct 9, 2024
31119ae
Allow to determine Workspace role assignments
bwaidelich Oct 9, 2024
51c7201
stabilize WorkspaceMetadata constructor
bwaidelich Oct 9, 2024
aa1655d
Fix `WorkspaceService::getWorkspacePermissionsForUser()`
bwaidelich Oct 9, 2024
732acc8
Merge branch 'feature/4726-extract-workspace-metadata-and-user-assign…
bwaidelich Oct 9, 2024
5540c83
TASK: Use `WorkspaceRoleAssignment` in `assignWorkspaceRole` signatur
mhsdesign Oct 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
*/
final class WorkspaceName implements \JsonSerializable
{
private const PATTERN = '/^[a-z0-9\-]{1,30}$/';

public const WORKSPACE_NAME_LIVE = 'live';

/**
Expand All @@ -33,7 +35,7 @@ final class WorkspaceName implements \JsonSerializable
private function __construct(
public readonly string $value
) {
if (preg_match('/^[\p{L}\p{P}\d \.]{1,200}$/u', $value) !== 1) {
if (!self::hasValidFormat($value)) {
throw new \InvalidArgumentException('Invalid workspace name given.', 1505826610318);
}
}
Expand All @@ -48,6 +50,11 @@ public static function fromString(string $value): self
return self::instance($value);
}

public static function tryFromString(string $value): ?self
{
return self::hasValidFormat($value) ? self::instance($value) : null;
}

public static function forLive(): self
{
return self::instance(self::WORKSPACE_NAME_LIVE);
Expand All @@ -61,19 +68,16 @@ public static function forLive(): self
*/
public static function transliterateFromString(string $name): self
{
try {
// Check if name already match name pattern to prevent unnecessary transliteration
if (self::hasValidFormat($name)) {
return self::fromString($name);
} catch (\InvalidArgumentException $e) {
// Okay, let's transliterate
}
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved

$originalName = $name;
$originalName = strtolower($name);

// Transliterate (transform 北京 to 'Bei Jing')
$name = Transliterator::transliterate($name);

// Urlization (replace spaces with dash, special special characters)
// Urlization (replace spaces with dash, special characters)
$name = Transliterator::urlize($name);

// Ensure only allowed characters are left
Expand All @@ -84,7 +88,7 @@ public static function transliterateFromString(string $name): self
$name = 'workspace-' . strtolower(md5($originalName));
}

return new self($name);
return self::fromString($name);
bwaidelich marked this conversation as resolved.
Show resolved Hide resolved
}

public function isLive(): bool
Expand All @@ -101,4 +105,9 @@ public function equals(self $other): bool
{
return $this === $other;
}

private static function hasValidFormat(string $value): bool
{
return preg_match(self::PATTERN, $value) === 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ public function get(ContentRepositoryId $contentRepositoryId): ContentRepository
return $this->getFactory($contentRepositoryId)->getOrBuild();
}

/**
* @return array<ContentRepositoryId>
*/
public function getContentRepositoryIds(): array
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
{
return array_map(ContentRepositoryId::fromString(...), array_keys($this->settings['contentRepositories']));
}
bwaidelich marked this conversation as resolved.
Show resolved Hide resolved

/**
* @internal for test cases only
*/
Expand Down
88 changes: 49 additions & 39 deletions Neos.Neos/Classes/Command/WorkspaceCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
use Neos\Flow\Cli\CommandController;
use Neos\Flow\Cli\Exception\StopCommandException;
use Neos\Flow\Persistence\PersistenceManagerInterface;
use Neos\Neos\Domain\Model\WorkspaceClassification;
use Neos\Neos\Domain\Service\UserService;
use Neos\Neos\Domain\Workspace\WorkspaceProvider;
use Neos\Neos\Domain\Service\WorkspaceService;
use Neos\Neos\Domain\Service\WorkspacePublishingService;
use Neos\Neos\PendingChangesProjection\ChangeFinder;

/**
Expand All @@ -56,7 +58,10 @@ class WorkspaceCommandController extends CommandController
protected ContentRepositoryRegistry $contentRepositoryRegistry;

#[Flow\Inject]
protected WorkspaceProvider $workspaceProvider;
protected WorkspacePublishingService $workspacePublishingService;

#[Flow\Inject]
protected WorkspaceService $workspaceService;

/**
* Publish changes of a workspace
Expand All @@ -68,16 +73,14 @@ class WorkspaceCommandController extends CommandController
*/
public function publishCommand(string $workspace, string $contentRepository = 'default'): void
{
// @todo: bypass access control
$workspace = $this->workspaceProvider->provideForWorkspaceName(
$this->workspacePublishingService->publishWorkspace(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
);
$workspace->publishAllChanges();

$this->outputLine(
'Published all nodes in workspace %s to its base workspace',
[$workspace->name->value]
[$workspace]
);
}

Expand All @@ -92,19 +95,17 @@ public function publishCommand(string $workspace, string $contentRepository = 'd
*/
public function discardCommand(string $workspace, string $contentRepository = 'default'): void
{
// @todo: bypass access control
$workspace = $this->workspaceProvider->provideForWorkspaceName(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
);

try {
$workspace->discardAllChanges();
// @todo: bypass access control
$this->workspacePublishingService->discardAllWorkspaceChanges(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
);
} catch (WorkspaceDoesNotExist $exception) {
$this->outputLine('Workspace "%s" does not exist', [$workspace->name->value]);
$this->outputLine('Workspace "%s" does not exist', [$workspace]);
$this->quit(1);
}
$this->outputLine('Discarded all nodes in workspace %s', [$workspace->name->value]);
$this->outputLine('Discarded all nodes in workspace %s', [$workspace]);
}

/**
Expand All @@ -121,11 +122,11 @@ public function rebaseCommand(string $workspace, string $contentRepository = 'de
{
try {
// @todo: bypass access control
$workspace = $this->workspaceProvider->provideForWorkspaceName(
$this->workspacePublishingService->rebaseWorkspace(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
WorkspaceName::fromString($workspace),
$force ? RebaseErrorHandlingStrategy::STRATEGY_FORCE : RebaseErrorHandlingStrategy::STRATEGY_FAIL,
);
$workspace->rebase($force ? RebaseErrorHandlingStrategy::STRATEGY_FORCE : RebaseErrorHandlingStrategy::STRATEGY_FAIL);
} catch (WorkspaceDoesNotExist $exception) {
$this->outputLine('Workspace "%s" does not exist', [$workspace]);
$this->quit(1);
Expand All @@ -134,7 +135,7 @@ public function rebaseCommand(string $workspace, string $contentRepository = 'de
$this->quit(1);
}

$this->outputLine('Rebased workspace %s', [$workspace->name->value]);
$this->outputLine('Rebased workspace %s', [$workspace]);
}

/**
Expand Down Expand Up @@ -178,13 +179,23 @@ public function createCommand(
string $contentRepository = 'default'
): void {
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);

$this->workspaceService->createWorkspace(
$contentRepositoryId,
\Neos\Neos\Domain\Model\WorkspaceTitle::fromString($title),
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
\Neos\Neos\Domain\Model\WorkspaceDescription::fromString($description ?? ''),
WorkspaceName::fromString($baseWorkspace),
\Neos\Neos\Domain\Model\UserId::fromString($owner),
WorkspaceClassification::PERSONAL,
);

$contentRepositoryInstance = $this->contentRepositoryRegistry->get($contentRepositoryId);

if ($owner === '') {
$workspaceOwnerUserId = null;
} else {
$workspaceOwnerUserId = UserId::fromString($owner);
$workspaceOwner = $this->userService->findByUserIdentifier($workspaceOwnerUserId);
$workspaceOwner = $this->userService->findUserById($workspaceOwnerUserId);
bwaidelich marked this conversation as resolved.
Show resolved Hide resolved
if ($workspaceOwner === null) {
$this->outputLine('The user "%s" specified as owner does not exist', [$owner]);
$this->quit(3);
Expand Down Expand Up @@ -243,13 +254,14 @@ public function deleteCommand(string $workspace, bool $force = false, string $co
$this->quit(2);
}

$workspace = $contentRepositoryInstance->getWorkspaceFinder()->findOneByName($workspaceName);
if (!$workspace instanceof Workspace) {
$crWorkspace = $contentRepositoryInstance->getWorkspaceFinder()->findOneByName($workspaceName);
if ($crWorkspace === null) {
$this->outputLine('Workspace "%s" does not exist', [$workspaceName->value]);
$this->quit(1);
}
$workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspaceName);

if ($workspace->isPersonalWorkspace()) {
if ($workspaceMetadata->classification === WorkspaceClassification::PERSONAL) {
$this->outputLine(
'Did not delete workspace "%s" because it is a personal workspace.'
. ' Personal workspaces cannot be deleted manually.',
Expand All @@ -264,28 +276,28 @@ public function deleteCommand(string $workspace, bool $force = false, string $co
'Workspace "%s" cannot be deleted because the following workspaces are based on it:',
[$workspaceName->value]
);

$this->outputLine();
$tableRows = [];
$headerRow = ['Name', 'Title', 'Description'];

foreach ($dependentWorkspaces as $dependentWorkspace) {
$dependentWorkspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $dependentWorkspace->workspaceName);
$tableRows[] = [
$dependentWorkspace->workspaceName->value,
$dependentWorkspace->workspaceTitle->value,
$dependentWorkspace->workspaceDescription->value
$dependentWorkspaceMetadata->title->value,
$dependentWorkspaceMetadata->description->value
];
}
$this->output->outputTable($tableRows, $headerRow);
$this->quit(3);
}


try {
$nodesCount = $contentRepositoryInstance->projectionState(ChangeFinder::class)
->countByContentStreamId(
$workspace->currentContentStreamId
);
$nodesCount = $this->workspacePublishingService->countPendingWorkspaceChanges($contentRepositoryId, $workspaceName);
} catch (\Exception $exception) {
$this->outputLine('Could not fetch unpublished nodes for workspace %s, nothing was deleted. %s', [$workspace->workspaceName->value, $exception->getMessage()]);
$this->outputLine('Could not fetch unpublished nodes for workspace %s, nothing was deleted. %s', [$workspaceName->value, $exception->getMessage()]);
$this->quit(4);
}

Expand All @@ -299,11 +311,7 @@ public function deleteCommand(string $workspace, bool $force = false, string $co
$this->quit(5);
}
// @todo bypass access control?
$workspace = $this->workspaceProvider->provideForWorkspaceName(
$contentRepositoryId,
$workspaceName
);
$workspace->discardAllChanges();
$this->workspacePublishingService->discardAllWorkspaceChanges($contentRepositoryId, $workspaceName);
}

$contentRepositoryInstance->handle(
Expand Down Expand Up @@ -359,16 +367,18 @@ public function listCommand(string $contentRepository = 'default'): void
}

$tableRows = [];
$headerRow = ['Name', 'Base Workspace', 'Title', 'Owner', 'Description', 'Status', 'Content Stream'];
$headerRow = ['Name', 'Classification', 'Base Workspace', 'Title', 'Description', 'Status', 'Content Stream'];
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved

foreach ($workspaces as $workspace) {
$workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspace->workspaceName);

/* @var Workspace $workspace */
$tableRows[] = [
$workspace->workspaceName->value,
$workspaceMetadata->classification->name,
$workspace->baseWorkspaceName?->value ?: '',
$workspace->workspaceTitle->value,
$workspace->workspaceOwner ?: '',
$workspace->workspaceDescription->value,
$workspaceMetadata->title->value,
$workspaceMetadata->description->value,
$workspace->status->value,
$workspace->currentContentStreamId->value,
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

declare(strict_types=1);

namespace Neos\Neos\Domain\Workspace;
namespace Neos\Neos\Domain\Model;

use Neos\Flow\Annotations as Flow;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@

declare(strict_types=1);

namespace Neos\Neos\Domain\Workspace;
namespace Neos\Neos\Domain\Model;

use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use Neos\Flow\Annotations as Flow;

/**
Expand All @@ -26,6 +27,7 @@
{
public function __construct(
public int $numberOfPublishedChanges,
public WorkspaceName $targetWorkspaceName,
) {
}
}
10 changes: 10 additions & 0 deletions Neos.Neos/Classes/Domain/Model/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ class User extends Person implements UserInterface
*/
protected $preferences;

/**
* @var string
*/
protected $Persistence_Object_Identifier;
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved

/**
* Constructs this User object
*/
Expand All @@ -45,6 +50,11 @@ public function __construct()
$this->preferences = new UserPreferences();
}

public function getId(): UserId
{
return UserId::fromString($this->Persistence_Object_Identifier);
}

/**
* Returns a label which can be used as a human-friendly identifier for this user.
*
Expand Down
36 changes: 36 additions & 0 deletions Neos.Neos/Classes/Domain/Model/UserId.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Neos\Neos\Domain\Model;

/**
* Globally unique identifier of a Neos user
*
* @api
*/
final readonly class UserId implements \JsonSerializable
bwaidelich marked this conversation as resolved.
Show resolved Hide resolved
{
public function __construct(
public string $value
) {
if (!preg_match('/^([a-z0-9\-]{1,40})$/', $value)) {
throw new \InvalidArgumentException(sprintf('Invalid user id "%s" (a user id must only contain lowercase characters, numbers and the "-" sign).', 1718293224));
}
}

public static function fromString(string $value): self
{
return new self($value);
}

public function jsonSerialize(): string
{
return $this->value;
}

public function equals(self $other): bool
{
return $this->value === $other->value;
}
}
6 changes: 6 additions & 0 deletions Neos.Neos/Classes/Domain/Model/UserInterface.php
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
*/
interface UserInterface
{

/**
* Returns the globally unique identifier for this user
*/
public function getId(): UserId;

/**
* Returns a label which can be used as a human-friendly identifier for this user, for example his or her first
* and last name.
Expand Down
Loading
Loading