feat: group Divi-Bibliothek with lit/library-list

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 01:29:37 +02:00
co-authored by Claude Opus 5.5
parent 520a639411
commit b7c714939d
7 changed files with 528 additions and 4 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ Direkt sichtbar: `lit-guide`, `lit-site-status`, `lit-content-types`, `lit-conte
`lit-divi-outline`, `lit-divi-node-get`, `lit-divi-catalog`, `lit-divi-design-system`, `lit-divi-insert`, `lit-divi-outline`, `lit-divi-node-get`, `lit-divi-catalog`, `lit-divi-design-system`, `lit-divi-insert`,
`lit-divi-update`, `lit-divi-move`, `lit-divi-delete`. `lit-divi-update`, `lit-divi-move`, `lit-divi-delete`.
Die Tools sind nach Integration gruppiert (Grundlagen, WordPress, Medien, Divi). Jede Gruppe hat unter Die Tools sind nach Integration gruppiert (Grundlagen, WordPress, Medien, Divi, Divi-Bibliothek). Jede Gruppe hat unter
Einstellungen → LIT MCP → Tools eine eigene Einstellung (direkt/indirekt/deaktiviert), die ihre Tools übernehmen Einstellungen → LIT MCP → Tools eine eigene Einstellung (direkt/indirekt/deaktiviert), die ihre Tools übernehmen
(„wie Gruppe“). Eine Einstellung am einzelnen Tool geht vor; die Gruppe zeigt dann „gemischt“. „nur lesen“ („wie Gruppe“). Eine Einstellung am einzelnen Tool geht vor; die Gruppe zeigt dann „gemischt“. „nur lesen“
schaltet alle schreibenden Tools der Gruppe ab – auch solche mit eigener Einstellung. `lit-guide` bleibt immer schaltet alle schreibenden Tools der Gruppe ab – auch solche mit eigener Einstellung. `lit-guide` bleibt immer
@@ -36,7 +36,7 @@ Status (`lit/post-unpublish`, `lit/post-schedule`, `lit/post-trash`), Kopien (`l
`lit/post-replace-with-copy`), Revisionen (`lit/revisions-list`, `lit/revision-restore`), Menüs `lit/post-replace-with-copy`), Revisionen (`lit/revisions-list`, `lit/revision-restore`), Menüs
(`lit/menu-list`, `lit/menu-get`, `lit/menu-create`, `lit/menu-set-items`, `lit/menu-assign-location`), (`lit/menu-list`, `lit/menu-get`, `lit/menu-create`, `lit/menu-set-items`, `lit/menu-assign-location`),
Einstellungen (`lit/wp-settings-get`, `lit/wp-settings-update`), Medien (`lit/media-get`, `lit/media-upload`, Einstellungen (`lit/wp-settings-get`, `lit/wp-settings-update`), Medien (`lit/media-get`, `lit/media-upload`,
`lit/media-update`, `lit/media-delete`), Divi (`lit/divi-duplicate-node`, `lit/divi-replace-text`). `lit/media-update`, `lit/media-delete`), Divi (`lit/divi-duplicate-node`, `lit/divi-replace-text`), Divi-Bibliothek (`lit/library-list`).
Die `lit-divi-…`-Werkzeuge gibt es nur, wenn der Divi-5-Builder aktiv ist. Die `lit-divi-…`-Werkzeuge gibt es nur, wenn der Divi-5-Builder aktiv ist.
+13 -1
View File
@@ -14,6 +14,7 @@ use LIT\MCP\Integrations\Divi\Library\DiviRoles;
use LIT\MCP\Integrations\Divi\Library\GlobalEdits; use LIT\MCP\Integrations\Divi\Library\GlobalEdits;
use LIT\MCP\Integrations\Divi\Library\GlobalUsages; use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
use LIT\MCP\Integrations\Divi\Library\LibraryAccess; use LIT\MCP\Integrations\Divi\Library\LibraryAccess;
use LIT\MCP\Integrations\Divi\Library\LibraryIntegration;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition; use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer; use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
use LIT\MCP\Integrations\Divi\Pages\DiviDeleteAbility; use LIT\MCP\Integrations\Divi\Pages\DiviDeleteAbility;
@@ -98,7 +99,7 @@ final class DiviIntegration implements Integration
public function abilities(): array public function abilities(): array
{ {
$loader = new DiviPageLoader($this->schema, new Parser(), $this->access); $loader = $this->loader();
$composition = new DiviComposition($this->modules, $this->design); $composition = new DiviComposition($this->modules, $this->design);
$writer = new DiviWriter( $writer = new DiviWriter(
$loader, $loader,
@@ -122,4 +123,15 @@ final class DiviIntegration implements Integration
new DiviReplaceTextAbility($this->schema, $loader, $writer), new DiviReplaceTextAbility($this->schema, $loader, $writer),
]; ];
} }
/** The group "Divi-Bibliothek" (lit/library-*), sharing this integration's library access. */
public function library(): LibraryIntegration
{
return new LibraryIntegration($this->access, $this->loader(), $this->design);
}
private function loader(): DiviPageLoader
{
return new DiviPageLoader($this->schema, new Parser(), $this->access);
}
} }
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Library;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Integrations\Integration;
defined('ABSPATH') || exit;
/**
* The group "Divi-Bibliothek" (E9): lit/library-list, later lit/library-save and lit-library-insert.
* Built by DiviIntegration::library() so it shares the library access (rights, Tools-tab gates –
* its id LibraryAccess::GROUP is what the P9 hiding and the write gate ask). Available with the
* Divi 5 builder, like the Divi group.
*/
final class LibraryIntegration implements Integration
{
public function __construct(
private LibraryAccess $access,
private DiviPageLoader $loader,
private DesignSystemReader $design
) {
}
public function id(): string
{
return LibraryAccess::GROUP;
}
public function label(): string
{
return __('Divi-Bibliothek', 'wp-lit-mcp');
}
public function isAvailable(): bool
{
return DiviModules::isAvailable();
}
public function abilities(): array
{
return [
new LibraryListAbility($this->access, $this->loader, $this->design, new GlobalUsages()),
];
}
}
@@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Library;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Cursor;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Abilities\Rev;
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviPage;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
use LIT\MCP\Integrations\WordPress\PostGuard;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/library-list: the Divi Library (spec §5.7, E9 – replaces lit/library-get). Without id a page
* of published entries (filters, cursor); with id one entry with its node outline, tree_rev and,
* for a global entry, the pages that show it. Rights: LibraryAccess (edit_posts + divi_library;
* every listed entry also edit_post – others are left out, without title or count). With id,
* everything but a library entry the user may read answers like a missing ID (ruling P12).
*/
final class LibraryListAbility implements Ability
{
/** At most this many entries are looked at per call while skipping entries the user may not edit. */
public const MAX_SCAN = 500;
public function __construct(
private LibraryAccess $access,
private DiviPageLoader $loader,
private DesignSystemReader $design,
private GlobalUsages $usages
) {
}
public function name(): string
{
return 'lit/library-list';
}
public function label(): string
{
return 'Divi-Bibliothek auflisten';
}
public function description(): string
{
return 'Listet Einträge der Divi-Bibliothek (Layouts, Abschnitte, Zeilen, Module; normal oder global), '
. 'filterbar nach layout_type, scope, search und category, blätterbar mit cursor. Mit id: ein Eintrag '
. 'im Detail – Knoten-Übersicht, tree_rev und bei globalen Einträgen die Seiten, die ihn zeigen '
. '(affected). Einfügen mit lit-library-insert; bearbeiten mit den Divi-Werkzeugen und der ID als post_id.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => array_merge([
'id' => ['type' => 'integer', 'minimum' => 1, 'description' => 'Ein Eintrag im Detail.'],
'layout_type' => ['type' => 'string', 'enum' => DiviLibraryStore::LAYOUT_TYPES],
'scope' => ['type' => 'string', 'enum' => ['normal', 'global']],
'search' => ['type' => 'string', 'maxLength' => 100, 'description' => 'Suche im Titel.'],
'category' => ['type' => 'string', 'maxLength' => 100, 'description' => 'Name einer Kategorie.'],
], Cursor::inputProperties()),
'additionalProperties' => false,
];
}
public function outputSchema(): ?array
{
return [
'type' => 'object',
'properties' => [
'items' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'title' => ['type' => 'string'],
'layout_type' => ['type' => ['string', 'null']],
'scope' => ['type' => 'string'],
'module_type' => ['type' => ['string', 'null']],
'categories' => ['type' => 'array', 'items' => ['type' => 'string']],
'tags' => ['type' => 'array', 'items' => ['type' => 'string']],
'modified' => ['type' => 'string'],
],
'required' => [
'id', 'title', 'layout_type', 'scope', 'module_type', 'categories', 'tags', 'modified',
],
],
],
'next_cursor' => ['type' => ['string', 'null']],
'outline' => ['type' => 'string'],
'tree_rev' => ['type' => 'string'],
'rev' => ['type' => 'string'],
'edit_url' => ['type' => 'string'],
'affected' => ['type' => 'array', 'items' => ['type' => 'object']],
'affected_count' => ['type' => 'integer'],
],
'required' => ['items', 'next_cursor'],
];
}
public function annotations(): array
{
return Annotations::read();
}
public function isDirect(): bool
{
return false;
}
public function permission(array $input): bool|WP_Error
{
if (isset($input['id'])) {
$page = $this->entry($input['id']);
return $page instanceof WP_Error ? $page : true;
}
return $this->access->canList() ?? true;
}
public function execute(array $input): mixed
{
if (isset($input['id'])) {
$page = $this->entry($input['id']);
return $page instanceof WP_Error ? $page : $this->detail($page);
}
$refusal = $this->access->canList();
if ($refusal !== null) {
return $refusal;
}
$offset = Cursor::decode(is_string($input['cursor'] ?? null) ? $input['cursor'] : null);
if ($offset === null) {
return Cursor::invalid();
}
$filters = array_filter(
[
'layout_type' => $input['layout_type'] ?? null,
'scope' => $input['scope'] ?? null,
'search' => $input['search'] ?? null,
'category' => $input['category'] ?? null,
],
static fn (mixed $value): bool => is_string($value) && $value !== ''
);
return $this->page($filters, Cursor::limit($input['limit'] ?? null), $offset);
}
/**
* One page of the entries the user may edit. The cursor counts the store's rows, so entries
* left out never shift a page; next_cursor points at the next readable entry, null when none
* follows (or the scan cap is reached with nothing left).
*
* @param array<string, string> $filters
* @return array{items: list<array<string, mixed>>, next_cursor: ?string}
*/
private function page(array $filters, int $limit, int $offset): array
{
$store = $this->access->store();
$items = [];
$position = $offset;
while ($position - $offset < self::MAX_SCAN) {
$rows = $store->find($filters, $limit, $position);
foreach ($rows as $post) {
if ($this->access->canRead($post) !== null) {
++$position;
continue;
}
if (count($items) === $limit) {
return ['items' => $items, 'next_cursor' => Cursor::encode($position)];
}
$items[] = $store->describe($post);
++$position;
}
if (count($rows) <= $limit) {
return ['items' => $items, 'next_cursor' => null];
}
}
return ['items' => $items, 'next_cursor' => Cursor::encode($position)];
}
/**
* The library entry $id loaded for reading. Anything that is not a library entry reads like a
* missing ID, so the tool tells nothing about other posts (ruling P12).
*/
private function entry(mixed $id): DiviPage|WP_Error
{
$postId = PostGuard::id($id);
if ($postId === null) {
return Errors::invalidInput('id ist keine gültige Eintrags-ID.', ['id' => 'positive Ganzzahl']);
}
$item = $this->access->item($postId, false);
if (! $item instanceof WP_Post) {
return $item ?? PostGuard::notFound($postId);
}
$page = $this->loader->load($postId, false);
if ($page instanceof WP_Error) {
return $page;
}
return $page->isLibraryItem() ? $page : PostGuard::notFound($postId);
}
/** @return array<string, mixed> */
private function detail(DiviPage $page): array
{
$item = $this->access->store()->describe($page->post);
$outline = new Outline($this->design->read()->presetNames());
$result = [
'items' => [$item],
'next_cursor' => null,
'outline' => $outline->text($outline->rows($page->index)),
'tree_rev' => $page->treeRev(),
'rev' => Rev::ofPost($page->post),
'edit_url' => $page->editUrl(),
];
if ($item['scope'] === 'global') {
$usages = $this->usages->of((int) $page->post->ID);
$result['affected'] = $usages['items'];
$result['affected_count'] = $usages['count'];
}
return $result;
}
}
+2 -1
View File
@@ -94,7 +94,8 @@ final class Plugin
new CoreIntegration(self::pluginDir() . '/docs/guides'), new CoreIntegration(self::pluginDir() . '/docs/guides'),
new WordPressIntegration($schema, $changeLog, $divi->contentComposer($diviWritable)), new WordPressIntegration($schema, $changeLog, $divi->contentComposer($diviWritable)),
new MediaIntegration($schema, $changeLog, $limits), new MediaIntegration($schema, $changeLog, $limits),
$divi $divi,
$divi->library()
); );
$policy = ExposurePolicy::fromOption()->withGroups($registry->groupMap(...)); $policy = ExposurePolicy::fromOption()->withGroups($registry->groupMap(...));
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi; namespace LIT\MCP\Tests\Unit\Integrations\Divi;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Ability; use LIT\MCP\Abilities\Ability;
use LIT\MCP\Integrations\Divi\DiviIntegration; use LIT\MCP\Integrations\Divi\DiviIntegration;
use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer; use LIT\MCP\Integrations\Divi\Pages\DiviContentComposer;
@@ -48,6 +49,24 @@ final class DiviIntegrationTest extends TestCase
} }
} }
public function testProvidesTheLibraryGroup(): void
{
Functions\when('__')->returnArg();
$library = (new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()))->library();
self::assertSame('divi-library', $library->id());
self::assertSame('Divi-Bibliothek', $library->label());
self::assertFalse($library->isAvailable(), 'no Divi classes in unit tests');
self::assertSame(
['lit/library-list'],
array_map(static fn (Ability $ability): string => $ability->name(), $library->abilities())
);
foreach ($library->abilities() as $ability) {
self::assertFalse($ability->inputSchema()['additionalProperties'], $ability->name());
self::assertNotNull($ability->outputSchema(), $ability->name());
}
}
public function testGivesContentCreateTheDiviShortForm(): void public function testGivesContentCreateTheDiviShortForm(): void
{ {
$integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden()); $integration = new DiviIntegration(Mockery::mock(ContentSchema::class), FakeChangeLog::forbidden());
@@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library;
use Brain\Monkey\Functions;
use LIT\MCP\Abilities\Cursor;
use LIT\MCP\Integrations\Divi\Library\GlobalUsages;
use LIT\MCP\Integrations\Divi\Library\LibraryListAbility;
use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Tests\Support\DiviWriterFixture;
use LIT\MCP\Tests\Support\LibraryFixtures;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
use WP_Error;
final class LibraryListAbilityTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Functions\when('get_the_terms')->justReturn(false);
Functions\when('get_post_meta')->justReturn('');
}
/**
* @param list<string> $deniedRoles
* @param list<int> $ids Entries the store lists (in this order).
*/
private function ability(array $deniedRoles = [], array $ids = [1055, 1058, 1059]): LibraryListAbility
{
$store = LibraryFixtures::store();
$store->allows('find')->andReturnUsing(static fn (array $filters, int $limit, int $offset): array => array_slice(
array_map(static fn (int $id) => LibraryFixtures::item($id), $ids),
$offset,
$limit + 1
));
$access = LibraryFixtures::access($deniedRoles, true, $store);
$usages = Mockery::mock(GlobalUsages::class);
$usages->allows('of')->with(1058)->andReturn([
'items' => [['post_id' => 1053, 'title' => 'Globale Instanzen', 'status' => 'draft', 'type' => 'page']],
'count' => 1,
]);
return new LibraryListAbility($access, LibraryFixtures::loader($access), DiviWriterFixture::designReader(), $usages);
}
/** A user with every capability but edit_post on the entries $hidden. */
private static function withoutEditPostOn(array $hidden): void
{
Functions\when('current_user_can')->alias(
static fn (string $cap, mixed ...$args): bool => ! ($cap === 'edit_post' && in_array((int) ($args[0] ?? 0), $hidden, true))
);
}
public function testListsAPageWithCursor(): void
{
LibraryFixtures::given([]);
$first = $this->ability()->execute(['limit' => 2]);
$second = $this->ability()->execute(['limit' => 2, 'cursor' => $first['next_cursor']]);
self::assertSame([1055, 1058], array_column($first['items'], 'id'));
self::assertSame(['normal', 'global'], array_column($first['items'], 'scope'));
self::assertSame('section', $first['items'][0]['layout_type']);
self::assertSame(Cursor::encode(2), $first['next_cursor']);
self::assertSame([1059], array_column($second['items'], 'id'));
self::assertNull($second['next_cursor']);
}
public function testPassesTheFilters(): void
{
LibraryFixtures::given([]);
$store = LibraryFixtures::store();
$store->expects('find')->with(['layout_type' => 'module', 'scope' => 'global', 'search' => 'Zeiten'], 20, 0)
->andReturn([]);
$access = LibraryFixtures::access([], true, $store);
$ability = new LibraryListAbility(
$access,
LibraryFixtures::loader($access),
DiviWriterFixture::designReader(),
Mockery::mock(GlobalUsages::class)
);
$result = $ability->execute(['layout_type' => 'module', 'scope' => 'global', 'search' => 'Zeiten', 'category' => '']);
self::assertSame(['items' => [], 'next_cursor' => null], $result);
}
public function testDetailOfAGlobalEntryWithOutlineAndAffectedPages(): void
{
LibraryFixtures::given([LibraryFixtures::item(1058)]);
$result = $this->ability()->execute(['id' => 1058]);
self::assertSame(1058, $result['items'][0]['id']);
self::assertNull($result['next_cursor']);
self::assertStringStartsWith('s0 section "Global Abschnitt"', $result['outline']);
self::assertSame(12, strlen($result['tree_rev']));
self::assertSame(1, $result['affected_count']);
self::assertSame('https://kunde.test/wp-admin/post.php?post=1058&action=edit', $result['edit_url']);
}
public function testDetailOfSomethingElseIsNotFoundAndTheRoleIsNeeded(): void
{
LibraryFixtures::given([LibraryFixtures::page('')]);
self::assertToolError($this->ability()->execute(['id' => LibraryFixtures::PAGE]), 'not_found');
self::assertToolError($this->ability(['divi_library'])->permission([]), 'forbidden');
self::assertToolError($this->ability()->execute(['cursor' => '!!']), 'validation_failed');
self::assertTrue($this->ability()->annotations()['readonly']);
self::assertFalse($this->ability()->isDirect());
}
/** Output ruling: entries without edit_post are left out – no title, no count, the cursor still pages right. */
public function testLeavesOutEntriesTheUserMayNotEdit(): void
{
LibraryFixtures::given([]);
self::withoutEditPostOn([1055, 1057]);
$ids = [1054, 1055, 1056, 1057, 1058, 1059];
$first = $this->ability([], $ids)->execute(['limit' => 2]);
$second = $this->ability([], $ids)->execute(['limit' => 2, 'cursor' => $first['next_cursor']]);
self::assertSame([1054, 1056], array_column($first['items'], 'id'));
self::assertNotNull($first['next_cursor']);
self::assertSame([1058, 1059], array_column($second['items'], 'id'));
self::assertNull($second['next_cursor']);
$json = (string) json_encode([$first, $second]);
self::assertStringNotContainsString('[Fixture] 1055', $json);
self::assertStringNotContainsString('[Fixture] 1057', $json);
self::assertSame(['items', 'next_cursor'], array_keys($first));
}
public function testNoNextCursorWhenOnlyHiddenEntriesFollow(): void
{
LibraryFixtures::given([]);
self::withoutEditPostOn([1058, 1059]);
$result = $this->ability()->execute(['limit' => 1]);
self::assertSame([1055], array_column($result['items'], 'id'));
self::assertNull($result['next_cursor']);
}
/** Ruling P12: without the library right, an id reads exactly like a missing ID; the list says forbidden. */
public function testWithoutTheLibraryRightAnIdIsNotFoundLikeAMissingOne(): void
{
$missing = PostGuard::notFound(1058);
$cases = [
'no divi_library' => [[], ['divi_library']],
'no edit_posts' => [['edit_posts'], []],
];
foreach ($cases as $case => [$deniedCaps, $deniedRoles]) {
LibraryFixtures::given([LibraryFixtures::item(1058), LibraryFixtures::page('')], $deniedCaps);
$ability = $this->ability($deniedRoles);
foreach ([$ability->permission(['id' => 1058]), $ability->execute(['id' => 1058])] as $result) {
self::assertInstanceOf(WP_Error::class, $result, $case);
self::assertSame($missing->get_error_code(), $result->get_error_code(), $case);
self::assertSame($missing->get_error_message(), $result->get_error_message(), $case);
self::assertSame($missing->get_error_data(), $result->get_error_data(), $case);
}
self::assertToolError($ability->execute(['id' => 99999]), 'not_found');
self::assertToolError($ability->execute(['id' => LibraryFixtures::PAGE]), 'not_found');
self::assertToolError($ability->execute([]), 'forbidden');
self::assertToolError($ability->permission([]), 'forbidden');
}
}
public function testAnInvalidIdIsAValidationError(): void
{
LibraryFixtures::given([]);
self::assertToolError($this->ability()->execute(['id' => 'abc']), 'validation_failed');
}
public function testDetailWithoutEditPostIsForbiddenWithoutTheTitle(): void
{
LibraryFixtures::given([LibraryFixtures::item(1058, ['post_title' => 'Geheim'])]);
self::withoutEditPostOn([1058]);
$result = $this->ability()->execute(['id' => 1058]);
self::assertToolError($result, 'forbidden');
self::assertStringNotContainsString('Geheim', (string) json_encode([$result->get_error_message(), $result->get_error_data()]));
self::assertToolError($this->ability()->permission(['id' => 1058]), 'forbidden');
}
public function testDetailOfANormalEntryHasNoAffectedPages(): void
{
LibraryFixtures::given([LibraryFixtures::item(1055)]);
$result = $this->ability()->execute(['id' => 1055]);
self::assertSame('normal', $result['items'][0]['scope']);
self::assertArrayNotHasKey('affected', $result);
self::assertTrue($this->ability()->permission(['id' => 1055]));
}
}