@webiny/mcp 6.3.0-beta.2 → 6.3.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -4
- package/skills/admin/form-model/SKILL.md +527 -0
- package/skills/admin/website-builder/page-settings/SKILL.md +216 -0
- package/skills/api/v5-to-v6-migration/SKILL.md +367 -18
- package/skills/content-models/SKILL.md +261 -23
- package/skills/generated/admin/SKILL.md +16 -1
- package/skills/generated/admin/cms/SKILL.md +26 -1
- package/skills/generated/admin/form/SKILL.md +58 -1
- package/skills/generated/admin/ui/SKILL.md +26 -1
- package/skills/generated/admin/website-builder/SKILL.md +11 -1
- package/skills/generated/api/mailer/SKILL.md +74 -0
- package/skills/generated/api/tenant-manager/SKILL.md +36 -1
- package/skills/generated/infra/SKILL.md +3 -6
- package/skills/mailer-smtp/SKILL.md +98 -0
- package/skills/project-structure/SKILL.md +0 -1
- package/skills/webiny-sdk/SKILL.md +111 -3
- package/skills/website-builder/SKILL.md +91 -4
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webiny-page-settings-extensions
|
|
3
|
+
context: webiny-extensions
|
|
4
|
+
description: >
|
|
5
|
+
Extending the Website Builder page settings with custom settings groups and modifiers.
|
|
6
|
+
Use this skill when the developer wants to add a new tab/group to the page settings
|
|
7
|
+
drawer (e.g., Publishing, Analytics, Access Control), or modify an existing settings
|
|
8
|
+
group (e.g., add fields to General or SEO). Covers PageSettingsGroup, PageSettingsGroupModifier,
|
|
9
|
+
and the doc.extensions data model. For field types, renderers, and layout details,
|
|
10
|
+
see the webiny-form-model skill.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Page Settings Extensions
|
|
14
|
+
|
|
15
|
+
## TL;DR
|
|
16
|
+
|
|
17
|
+
Page settings extensions let you add new tabs to the page settings drawer or inject fields into existing tabs. Create a class implementing `PageSettingsGroup.Interface` for a new tab, or `PageSettingsGroupModifier.Interface` to extend an existing one. Register both via `createFeature()` and `<RegisterFeature>`. **Always store custom data in `doc.extensions` — never write to `doc.properties`, which is reserved for built-in system properties.**
|
|
18
|
+
|
|
19
|
+
**YOU MUST include the full file path with the `.tsx` extension in every `src` prop.** For example, use `src={"/extensions/myPageSettings/index.tsx"}`, NOT `src={"/extensions/myPageSettings/index"}`. Omitting the file extension will cause a build failure.
|
|
20
|
+
|
|
21
|
+
For field types, renderers, layout, validation, and all other form builder APIs, refer to the **webiny-form-model** skill.
|
|
22
|
+
|
|
23
|
+
## Important: Where to Store Data
|
|
24
|
+
|
|
25
|
+
> **Use `doc.extensions` for all custom data.** The `doc.properties` object holds built-in system properties (title, path, snippet, image, tags, seo, social). Writing custom fields into `doc.properties` risks naming collisions with future Webiny updates and can corrupt system behavior. Always namespace your data under `doc.extensions.<yourGroupName>`.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// CORRECT — custom data in doc.extensions
|
|
29
|
+
mapFromForm(formData, doc) {
|
|
30
|
+
doc.extensions.mySettings = doc.extensions.mySettings ?? {};
|
|
31
|
+
doc.extensions.mySettings.myField = formData.myField;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// WRONG — never write custom data into doc.properties
|
|
35
|
+
mapFromForm(formData, doc) {
|
|
36
|
+
doc.properties.myField = formData.myField; // DON'T DO THIS
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Adding a New Settings Group
|
|
41
|
+
|
|
42
|
+
A new settings group appears as its own tab in the page settings drawer. Implement `PageSettingsGroup.Interface` with these members:
|
|
43
|
+
|
|
44
|
+
| Member | Type | Description |
|
|
45
|
+
| ---------------------------- | ------------------------------------------- | --------------------------------------------------------- |
|
|
46
|
+
| `name` | `string` | Unique group identifier (used as form field namespace) |
|
|
47
|
+
| `label` | `string` | Tab label shown in the UI |
|
|
48
|
+
| `description` | `string` (optional) | Description shown below the tab label |
|
|
49
|
+
| `icon` | `{ type: "icon", name: string }` (optional) | FontAwesome icon for the tab (e.g., `"fas/calendar-alt"`) |
|
|
50
|
+
| `buildForm(form)` | method | Define fields and layout |
|
|
51
|
+
| `mapToForm(doc)` | method | Read from document to populate the form |
|
|
52
|
+
| `mapFromForm(formData, doc)` | method | Write form values back to the document |
|
|
53
|
+
|
|
54
|
+
### Complete Example: Publishing Settings Group
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// extensions/myPageSettings/PublishingSettingsGroup.ts
|
|
58
|
+
import { PageSettingsGroup } from "webiny/admin/website-builder/page/editor";
|
|
59
|
+
|
|
60
|
+
class PublishingSettingsGroupImpl implements PageSettingsGroup.Interface {
|
|
61
|
+
name = "publishing";
|
|
62
|
+
label = "Publishing";
|
|
63
|
+
description = "Configure publishing schedule and visibility.";
|
|
64
|
+
icon = { type: "icon", name: "fas/calendar-alt" };
|
|
65
|
+
|
|
66
|
+
buildForm(form: PageSettingsGroup.FormBuilder): void {
|
|
67
|
+
form.fields(fields => ({
|
|
68
|
+
publishDate: fields.datetime().withTimezone().label("Publish date"),
|
|
69
|
+
unpublishDate: fields.datetime().dateOnly().label("Unpublish date"),
|
|
70
|
+
visibility: fields
|
|
71
|
+
.text()
|
|
72
|
+
.label("Visibility")
|
|
73
|
+
.options([
|
|
74
|
+
{ label: "Public", value: "public" },
|
|
75
|
+
{ label: "Private", value: "private" },
|
|
76
|
+
{ label: "Password Protected", value: "password" }
|
|
77
|
+
])
|
|
78
|
+
.defaultValue("public"),
|
|
79
|
+
featured: fields.boolean().label("Featured page")
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
form.layout(layout => [
|
|
83
|
+
layout.row("publishDate"),
|
|
84
|
+
layout.row("unpublishDate"),
|
|
85
|
+
layout.row("visibility"),
|
|
86
|
+
layout.row("featured")
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
mapToForm(doc: PageSettingsGroup.PageDocument): Record<string, any> {
|
|
91
|
+
const publishing = doc.extensions?.publishing;
|
|
92
|
+
return {
|
|
93
|
+
publishDate: publishing?.publishDate ?? null,
|
|
94
|
+
unpublishDate: publishing?.unpublishDate ?? null,
|
|
95
|
+
visibility: publishing?.visibility ?? "public",
|
|
96
|
+
featured: publishing?.featured ?? false
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
mapFromForm(formData: Record<string, any>, doc: PageSettingsGroup.PageDocument): void {
|
|
101
|
+
doc.extensions.publishing = doc.extensions.publishing ?? {};
|
|
102
|
+
doc.extensions.publishing.publishDate = formData.publishDate;
|
|
103
|
+
doc.extensions.publishing.unpublishDate = formData.unpublishDate;
|
|
104
|
+
doc.extensions.publishing.visibility = formData.visibility;
|
|
105
|
+
doc.extensions.publishing.featured = formData.featured;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const PublishingSettingsGroup = PageSettingsGroup.createImplementation({
|
|
110
|
+
implementation: PublishingSettingsGroupImpl,
|
|
111
|
+
dependencies: []
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Modifying an Existing Settings Group
|
|
116
|
+
|
|
117
|
+
A modifier injects fields into an existing tab without subclassing it. Implement `PageSettingsGroupModifier.Interface`:
|
|
118
|
+
|
|
119
|
+
| Member | Type | Description |
|
|
120
|
+
| ---------------------------- | ----------------- | ----------------------------------------------------------------------- |
|
|
121
|
+
| `group` | `string` | Name of the target group (`"general"`, `"seo"`, `"social"`, `"schema"`) |
|
|
122
|
+
| `modifyForm(form)` | method | Add fields and layout entries to the existing group |
|
|
123
|
+
| `mapToForm(doc)` | method (optional) | Supply values for the new fields |
|
|
124
|
+
| `mapFromForm(formData, doc)` | method (optional) | Persist the new field values |
|
|
125
|
+
|
|
126
|
+
### Complete Example: Add Expiration Date to the General Tab
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// extensions/myPageSettings/GeneralSettingsModifier.ts
|
|
130
|
+
import { PageSettingsGroupModifier } from "webiny/admin/website-builder/page/editor";
|
|
131
|
+
|
|
132
|
+
class GeneralSettingsModifierImpl implements PageSettingsGroupModifier.Interface {
|
|
133
|
+
group = "general";
|
|
134
|
+
|
|
135
|
+
modifyForm(form: PageSettingsGroupModifier.FormBuilder): void {
|
|
136
|
+
form.fields(fields => ({
|
|
137
|
+
expirationDate: fields.datetime().monthOnly().label("Expiration month")
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
// .after("snippet") places the field after the "snippet" field in the General tab
|
|
141
|
+
form.layout(layout => [layout.row("expirationDate").after("snippet")]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
mapToForm(doc: PageSettingsGroupModifier.PageDocument): Record<string, any> {
|
|
145
|
+
return {
|
|
146
|
+
expirationDate: doc.extensions?.expirationDate ?? null
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
mapFromForm(formData: Record<string, any>, doc: PageSettingsGroupModifier.PageDocument): void {
|
|
151
|
+
doc.extensions.expirationDate = formData.expirationDate;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const GeneralSettingsModifier = PageSettingsGroupModifier.createImplementation({
|
|
156
|
+
implementation: GeneralSettingsModifierImpl,
|
|
157
|
+
dependencies: []
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Feature Registration
|
|
162
|
+
|
|
163
|
+
Wrap your group and/or modifier in a feature and export a React component:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
// extensions/myPageSettings/index.tsx
|
|
167
|
+
import React from "react";
|
|
168
|
+
import { createFeature, RegisterFeature } from "webiny/admin";
|
|
169
|
+
import { PublishingSettingsGroup } from "./PublishingSettingsGroup.js";
|
|
170
|
+
import { GeneralSettingsModifier } from "./GeneralSettingsModifier.js";
|
|
171
|
+
|
|
172
|
+
const MyPageSettingsFeature = createFeature({
|
|
173
|
+
name: "MyPageSettings",
|
|
174
|
+
register(container) {
|
|
175
|
+
container.register(PublishingSettingsGroup);
|
|
176
|
+
container.register(GeneralSettingsModifier);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export default () => {
|
|
181
|
+
return <RegisterFeature feature={MyPageSettingsFeature} />;
|
|
182
|
+
};
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Register in `webiny.config.tsx`:
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
<Admin.Extension src={"/extensions/myPageSettings/index.tsx"} />
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Layout Positioning in Modifiers
|
|
192
|
+
|
|
193
|
+
When modifying an existing group, use `.after("existingFieldName")` to position your new fields relative to built-in fields. The built-in field names for each group:
|
|
194
|
+
|
|
195
|
+
- **general**: `title`, `path`, `snippet`, `image`, `tags`
|
|
196
|
+
- **seo**: `title`, `description`, `metaTags`, `canonicalUrl`, `noIndex`, `noFollow`
|
|
197
|
+
- **social**: `title`, `description`, `image`, `metaTags`
|
|
198
|
+
- **schema**: `structuredSchema`
|
|
199
|
+
|
|
200
|
+
## The Page Document Model
|
|
201
|
+
|
|
202
|
+
The `doc` parameter in `mapToForm` / `mapFromForm` has three top-level namespaces:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
interface IPageDocument {
|
|
206
|
+
properties: { ... }; // SYSTEM — title, path, snippet, seo, social, etc.
|
|
207
|
+
metadata: { ... }; // SYSTEM — document metadata
|
|
208
|
+
extensions: { ... }; // YOUR DATA — use this for all custom fields
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Reminder**: `doc.properties` and `doc.metadata` are managed by the system. Always read/write your custom data via `doc.extensions`. Namespace it under your group name to avoid collisions with other extensions (e.g., `doc.extensions.publishing`, `doc.extensions.analytics`).
|
|
213
|
+
|
|
214
|
+
## Related Skills
|
|
215
|
+
|
|
216
|
+
- **webiny-form-model** — Field types, renderers, layout, validation, conditional rules, computed fields, and dynamic zones
|
|
@@ -48,7 +48,7 @@ new ContextPlugin(async context => {
|
|
|
48
48
|
|
|
49
49
|
```typescript
|
|
50
50
|
// features/lingotekService/abstractions.ts
|
|
51
|
-
import { createAbstraction } from "
|
|
51
|
+
import { createAbstraction } from "webiny/api";
|
|
52
52
|
|
|
53
53
|
export interface ILingotekService {
|
|
54
54
|
translate(docId: string, locale: string): Promise<Result<void, Error>>;
|
|
@@ -349,22 +349,22 @@ cat node_modules/@webiny/api-core/features/security/roles/shared/abstractions.d.
|
|
|
349
349
|
|
|
350
350
|
### Backend: Plugin Classes → v6 Equivalents
|
|
351
351
|
|
|
352
|
-
| v5 Plugin | v6 Equivalent
|
|
353
|
-
| ----------------------------------------------- |
|
|
354
|
-
| `ContextPlugin` | DI-registered implementations
|
|
355
|
-
| `createContextPlugin` | DI-registered implementations
|
|
356
|
-
| `CmsModelPlugin` | `ModelFactory`
|
|
357
|
-
| `GraphQLSchemaPlugin` | `GraphQLSchemaFactory`
|
|
358
|
-
| `createGraphQLSchemaPlugin` | `GraphQLSchemaFactory`
|
|
359
|
-
| `createTaskDefinition` | `TaskDefinition`
|
|
360
|
-
| `CmsModelFieldToGraphQLPlugin` |
|
|
361
|
-
| `createSecurityRolePlugin` | `RoleFactory`
|
|
362
|
-
| `createSecurityTeamPlugin` | `TeamFactory`
|
|
363
|
-
| `StorageTransformPlugin` |
|
|
364
|
-
| `createApiGatewayRoute` |
|
|
365
|
-
| `CmsModelFieldValidatorPlugin` |
|
|
366
|
-
| `createCmsGraphQLSchemaSorterPlugin` |
|
|
367
|
-
| `createCmsEntryElasticsearchBodyModifierPlugin` |
|
|
352
|
+
| v5 Plugin | v6 Equivalent |
|
|
353
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
354
|
+
| `ContextPlugin` | DI-registered implementations |
|
|
355
|
+
| `createContextPlugin` | DI-registered implementations |
|
|
356
|
+
| `CmsModelPlugin` | `ModelFactory` |
|
|
357
|
+
| `GraphQLSchemaPlugin` | `GraphQLSchemaFactory` |
|
|
358
|
+
| `createGraphQLSchemaPlugin` | `GraphQLSchemaFactory` |
|
|
359
|
+
| `createTaskDefinition` | `TaskDefinition` |
|
|
360
|
+
| `CmsModelFieldToGraphQLPlugin` | `CmsModelFieldToGraphQL` |
|
|
361
|
+
| `createSecurityRolePlugin` | `RoleFactory` |
|
|
362
|
+
| `createSecurityTeamPlugin` | `TeamFactory` |
|
|
363
|
+
| `StorageTransformPlugin` | `StorageTransform` |
|
|
364
|
+
| `createApiGatewayRoute` | `Api.Route` (`webiny.config.tsx`) and `Route.Interface` (imported from `webiny/api`) |
|
|
365
|
+
| `CmsModelFieldValidatorPlugin` | `CmsModelFieldValidator` |
|
|
366
|
+
| `createCmsGraphQLSchemaSorterPlugin` | `CmsGraphQLSchemaSorter` |
|
|
367
|
+
| `createCmsEntryElasticsearchBodyModifierPlugin` | `CmsEntryOpenSearchBodyModifier` |
|
|
368
368
|
|
|
369
369
|
### Admin: React Plugins → AdminConfig API
|
|
370
370
|
|
|
@@ -374,7 +374,7 @@ cat node_modules/@webiny/api-core/features/security/roles/shared/abstractions.d.
|
|
|
374
374
|
| `RoutePlugin` | `<AdminConfig.Route/>` |
|
|
375
375
|
| `AddMenu` / menu components | `<AdminConfig.Menu/>` |
|
|
376
376
|
| `HasPermission` | `HasPermission` or `createHasPermission` with new schema |
|
|
377
|
-
| `GraphQLPlaygroundTabPlugin` |
|
|
377
|
+
| `GraphQLPlaygroundTabPlugin` | Not migratable. |
|
|
378
378
|
| `CmsModelFieldTypePlugin` | `<CmsModelFieldType/>` |
|
|
379
379
|
| `CmsModelFieldRendererPlugin` | `<CmsModelFieldRenderer/>` |
|
|
380
380
|
| `AdminAppPermissionRendererPlugin` | `createPermissionSchema` / `<Security.Permissions/>` |
|
|
@@ -408,6 +408,355 @@ v5: `context.myService = { ... }`. v6: create an abstraction and register it in
|
|
|
408
408
|
|
|
409
409
|
v5 habit: putting logic directly in the subscription callback. v6: handlers are thin orchestrators — extract logic into a Service or UseCase.
|
|
410
410
|
|
|
411
|
+
## Pattern 7: CmsModelFieldToGraphQLPlugin → CmsModelFieldToGraphQL
|
|
412
|
+
|
|
413
|
+
### v5
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
import { CmsModelFieldToGraphQLPlugin } from "@webiny/api-headless-cms";
|
|
417
|
+
|
|
418
|
+
new CmsModelFieldToGraphQLPlugin({
|
|
419
|
+
fieldType: "myField",
|
|
420
|
+
isSearchable: true,
|
|
421
|
+
isSortable: false,
|
|
422
|
+
read: {
|
|
423
|
+
createTypeField({ field }) {
|
|
424
|
+
return `${field.fieldId}: String`;
|
|
425
|
+
},
|
|
426
|
+
createListFilters({ field }) {
|
|
427
|
+
return `${field.fieldId}: String`;
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
manage: {
|
|
431
|
+
createTypeField({ field }) {
|
|
432
|
+
return `${field.fieldId}: String`;
|
|
433
|
+
},
|
|
434
|
+
createInputField({ field }) {
|
|
435
|
+
return `${field.fieldId}: String`;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### v6
|
|
442
|
+
|
|
443
|
+
Import from `webiny/api/cms/graphql.js`. Implement `CmsModelFieldToGraphQL.Interface` — split read/manage into separate classes, export via `CmsModelFieldToGraphQL.createImplementation`, and register in a `createFeature` container.
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
import { createFeature } from "webiny/api";
|
|
447
|
+
import { CmsModelFieldToGraphQL } from "webiny/api/cms/graphql";
|
|
448
|
+
|
|
449
|
+
class ReadApi implements CmsModelFieldToGraphQL.ReadApi {
|
|
450
|
+
createTypeField({ field }: CmsModelFieldToGraphQL.TypeFieldParams): string {
|
|
451
|
+
return `${field.fieldId}: String`;
|
|
452
|
+
}
|
|
453
|
+
createListFilters({ field }: CmsModelFieldToGraphQL.ListFiltersParams): string {
|
|
454
|
+
return `${field.fieldId}: String`;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
class ManageApi implements CmsModelFieldToGraphQL.ManageApi {
|
|
459
|
+
createTypeField({ field }: CmsModelFieldToGraphQL.TypeFieldParams): string {
|
|
460
|
+
return `${field.fieldId}: String`;
|
|
461
|
+
}
|
|
462
|
+
createInputField({ field }: CmsModelFieldToGraphQL.TypeFieldParams): string {
|
|
463
|
+
return `${field.fieldId}: String`;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
class MyFieldToGraphQL implements CmsModelFieldToGraphQL.Interface {
|
|
468
|
+
public readonly fieldType = "myField";
|
|
469
|
+
public readonly isSearchable = true;
|
|
470
|
+
public readonly isSortable = false;
|
|
471
|
+
public readonly isFullTextSearchable = false;
|
|
472
|
+
public readonly read = new ReadApi();
|
|
473
|
+
public readonly manage = new ManageApi();
|
|
474
|
+
getReadApi() {
|
|
475
|
+
return this.read;
|
|
476
|
+
}
|
|
477
|
+
getManageApi() {
|
|
478
|
+
return this.manage;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export const MyFieldToGraphQLImplementation = CmsModelFieldToGraphQL.createImplementation({
|
|
483
|
+
implementation: MyFieldToGraphQL,
|
|
484
|
+
dependencies: []
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
export const MyGraphQLFeature = createFeature({
|
|
488
|
+
name: "MyApp/MyGraphQLFeature",
|
|
489
|
+
register: container => {
|
|
490
|
+
container.register(MyFieldToGraphQLImplementation);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Looking up a field handler by type
|
|
496
|
+
|
|
497
|
+
Use `CmsModelFieldToGraphQLRegistry` (also from `webiny/api/cms/graphql.js`) to retrieve any registered field handler by its `fieldType` string. Inject it as a dependency:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
import { CmsModelFieldToGraphQL } from "webiny/api/cms/graphql";
|
|
501
|
+
import { CmsModelFieldToGraphQLRegistry } from "webiny/api/cms/graphql";
|
|
502
|
+
|
|
503
|
+
class MyFieldToGraphQL implements CmsModelFieldToGraphQL.Interface {
|
|
504
|
+
constructor(private readonly registry: CmsModelFieldToGraphQLRegistry.Interface) {}
|
|
505
|
+
|
|
506
|
+
// Example: delegate to another field's read API
|
|
507
|
+
someMethod(fieldType: string) {
|
|
508
|
+
const handler = this.registry.get(fieldType);
|
|
509
|
+
// handler is CmsModelFieldToGraphQL.Interface | undefined
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ... rest of implementation
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export const MyFieldToGraphQLImplementation = CmsModelFieldToGraphQL.createImplementation({
|
|
516
|
+
implementation: MyFieldToGraphQL,
|
|
517
|
+
dependencies: [CmsModelFieldToGraphQLRegistry]
|
|
518
|
+
});
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
`registry.getAll()` returns every registered handler when you need to iterate.
|
|
522
|
+
|
|
523
|
+
## Pattern 8: createCmsGraphQLSchemaSorterPlugin → CmsGraphQLSchemaSorter
|
|
524
|
+
|
|
525
|
+
### v5
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
import { createCmsGraphQLSchemaSorterPlugin } from "@webiny/api-headless-cms";
|
|
529
|
+
|
|
530
|
+
createCmsGraphQLSchemaSorterPlugin({
|
|
531
|
+
sorter({ model, sorters }) {
|
|
532
|
+
return [...sorters, `${model.singularApiName}CustomSort_ASC`];
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### v6
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
import { createFeature } from "webiny/api";
|
|
541
|
+
import { CmsGraphQLSchemaSorter } from "webiny/api/cms/graphql";
|
|
542
|
+
|
|
543
|
+
class MyCustomSorter implements CmsGraphQLSchemaSorter.Interface {
|
|
544
|
+
execute({ model, sorters }: CmsGraphQLSchemaSorter.Params): string[] {
|
|
545
|
+
return [...sorters, `${model.singularApiName}CustomSort_ASC`];
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export const MyCustomSorterImplementation = CmsGraphQLSchemaSorter.createImplementation({
|
|
550
|
+
implementation: MyCustomSorter,
|
|
551
|
+
dependencies: []
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
export const MySorterFeature = createFeature({
|
|
555
|
+
name: "MyApp/MySorterFeature",
|
|
556
|
+
register: container => {
|
|
557
|
+
container.register(MyCustomSorterImplementation);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
## Pattern 9: StorageTransformPlugin → StorageTransform
|
|
563
|
+
|
|
564
|
+
### v5
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
import { StorageTransformPlugin } from "@webiny/api-headless-cms";
|
|
568
|
+
|
|
569
|
+
new StorageTransformPlugin({
|
|
570
|
+
fieldType: "myField",
|
|
571
|
+
async toStorage({ value }) {
|
|
572
|
+
return serialize(value);
|
|
573
|
+
},
|
|
574
|
+
async fromStorage({ value }) {
|
|
575
|
+
return deserialize(value);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### v6
|
|
581
|
+
|
|
582
|
+
Import from `webiny/api/cms/storage.js`. Implement `StorageTransform.Interface` with `toStorage` and `fromStorage` async methods. Register in a `createFeature` container.
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
import { createFeature } from "webiny/api";
|
|
586
|
+
import { StorageTransform } from "webiny/api/cms/storage";
|
|
587
|
+
|
|
588
|
+
class MyStorageTransform implements StorageTransform.Interface {
|
|
589
|
+
public readonly fieldType = "myField";
|
|
590
|
+
|
|
591
|
+
async toStorage({ value }: StorageTransform.ToStorageParams): Promise<unknown> {
|
|
592
|
+
return serialize(value);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async fromStorage({ value }: StorageTransform.FromStorageParams): Promise<unknown> {
|
|
596
|
+
return deserialize(value);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export const MyStorageTransformImpl = StorageTransform.createImplementation({
|
|
601
|
+
implementation: MyStorageTransform,
|
|
602
|
+
dependencies: []
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
export const MyStorageFeature = createFeature({
|
|
606
|
+
name: "MyApp/MyStorageFeature",
|
|
607
|
+
register: container => {
|
|
608
|
+
container.register(MyStorageTransformImpl);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Use `fieldType: "*"` for a catch-all transform that applies to all field types without a specific handler.
|
|
614
|
+
|
|
615
|
+
### Looking up a transform by type
|
|
616
|
+
|
|
617
|
+
Inject `StorageTransformRegistry` (also from `webiny/api/cms/storage.js`) to retrieve any registered transform:
|
|
618
|
+
|
|
619
|
+
```ts
|
|
620
|
+
import { StorageTransform } from "webiny/api/cms/storage";
|
|
621
|
+
import { StorageTransformRegistry } from "webiny/api/cms/storage";
|
|
622
|
+
|
|
623
|
+
class MyStorageTransform implements StorageTransform.Interface {
|
|
624
|
+
constructor(private readonly registry: StorageTransformRegistry.Interface) {}
|
|
625
|
+
|
|
626
|
+
async toStorage({ value, field }: StorageTransform.ToStorageParams): Promise<unknown> {
|
|
627
|
+
const delegate = this.registry.get(field.type);
|
|
628
|
+
// delegate is StorageTransform.Interface | undefined
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export const MyStorageTransformImpl = StorageTransform.createImplementation({
|
|
633
|
+
implementation: MyStorageTransform,
|
|
634
|
+
dependencies: [StorageTransformRegistry]
|
|
635
|
+
});
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
`registry.getAll()` returns every registered transform when you need to iterate.
|
|
639
|
+
|
|
640
|
+
## Pattern 10: CmsModelFieldValidatorPlugin → CmsModelFieldValidator
|
|
641
|
+
|
|
642
|
+
### v5
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
import { CmsModelFieldValidatorPlugin } from "@webiny/api-headless-cms";
|
|
646
|
+
|
|
647
|
+
new CmsModelFieldValidatorPlugin({
|
|
648
|
+
validator: {
|
|
649
|
+
name: "myValidator",
|
|
650
|
+
async validate({ value, validator }) {
|
|
651
|
+
if (!meetsCondition(value, validator.settings)) {
|
|
652
|
+
throw new Error("Validation failed.");
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### v6
|
|
660
|
+
|
|
661
|
+
Import from `webiny/api/cms/validation.js`. Implement `CmsModelFieldValidator.Interface` with a `name` string and an async `validate` method that returns `boolean`. Register in a `createFeature` container.
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
import { createFeature } from "webiny/api";
|
|
665
|
+
import { CmsModelFieldValidator } from "webiny/api/cms/validation";
|
|
666
|
+
|
|
667
|
+
class MyValidatorImpl implements CmsModelFieldValidator.Interface {
|
|
668
|
+
public readonly name = "myValidator";
|
|
669
|
+
|
|
670
|
+
async validate({ value, validator }: CmsModelFieldValidator.Params): Promise<boolean> {
|
|
671
|
+
return meetsCondition(value, validator.settings);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export const MyValidator = CmsModelFieldValidator.createImplementation({
|
|
676
|
+
implementation: MyValidatorImpl,
|
|
677
|
+
dependencies: []
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
export const MyValidationFeature = createFeature({
|
|
681
|
+
name: "MyApp/MyValidationFeature",
|
|
682
|
+
register: container => {
|
|
683
|
+
container.register(MyValidator);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### Looking up a validator by name
|
|
689
|
+
|
|
690
|
+
Inject `CmsModelFieldValidatorRegistry` (also from `webiny/api/cms/validation.js`) to retrieve any registered validator:
|
|
691
|
+
|
|
692
|
+
```ts
|
|
693
|
+
import { CmsModelFieldValidator } from "webiny/api/cms/validation";
|
|
694
|
+
import { CmsModelFieldValidatorRegistry } from "webiny/api/cms/validation";
|
|
695
|
+
|
|
696
|
+
class MyValidatorImpl implements CmsModelFieldValidator.Interface {
|
|
697
|
+
constructor(private readonly registry: CmsModelFieldValidatorRegistry.Interface) {}
|
|
698
|
+
|
|
699
|
+
async validate({ value, validator }: CmsModelFieldValidator.Params): Promise<boolean> {
|
|
700
|
+
const delegate = this.registry.get(validator.name);
|
|
701
|
+
// delegate is CmsModelFieldValidator.Interface | undefined
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export const MyValidator = CmsModelFieldValidator.createImplementation({
|
|
707
|
+
implementation: MyValidatorImpl,
|
|
708
|
+
dependencies: [CmsModelFieldValidatorRegistry]
|
|
709
|
+
});
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
`registry.getAll()` returns every registered validator when you need to iterate.
|
|
713
|
+
|
|
714
|
+
## Pattern 11: createCmsEntryElasticsearchBodyModifierPlugin → CmsEntryOpenSearchBodyModifier
|
|
715
|
+
|
|
716
|
+
Note: this abstraction lives in the `api-headless-cms-ddb-es` package (the DynamoDB + OpenSearch storage driver), not `api-headless-cms`. Only register it when that storage driver is in use.
|
|
717
|
+
|
|
718
|
+
### v5
|
|
719
|
+
|
|
720
|
+
```ts
|
|
721
|
+
import { createCmsEntryElasticsearchBodyModifierPlugin } from "@webiny/api-headless-cms-ddb-es";
|
|
722
|
+
|
|
723
|
+
createCmsEntryElasticsearchBodyModifierPlugin({
|
|
724
|
+
modelId: "myModel", // optional — omit to apply to all models
|
|
725
|
+
modifyBody({ body, model, where }) {
|
|
726
|
+
body.query.bool.filter.push({ term: { tenant: where.tenant } });
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### v6
|
|
732
|
+
|
|
733
|
+
Import from `webiny/api/cms/opensearch.js`. Implement `CmsEntryOpenSearchBodyModifier.Interface` with a synchronous `modifyBody` method. The optional `modelId` property scopes the modifier to a single model; omit it to apply to all models.
|
|
734
|
+
|
|
735
|
+
```ts
|
|
736
|
+
import { createFeature } from "webiny/api";
|
|
737
|
+
import { CmsEntryOpenSearchBodyModifier } from "webiny/api/cms/opensearch";
|
|
738
|
+
|
|
739
|
+
class MyBodyModifier implements CmsEntryOpenSearchBodyModifier.Interface {
|
|
740
|
+
public readonly modelId = "myModel"; // omit to apply to all models
|
|
741
|
+
|
|
742
|
+
modifyBody({ body, model, where }: CmsEntryOpenSearchBodyModifier.Params): void {
|
|
743
|
+
body.query.bool.filter.push({ term: { tenant: where.tenant } });
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export const MyBodyModifierImpl = CmsEntryOpenSearchBodyModifier.createImplementation({
|
|
748
|
+
implementation: MyBodyModifier,
|
|
749
|
+
dependencies: []
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
export const MyOpenSearchFeature = createFeature({
|
|
753
|
+
name: "MyApp/MyOpenSearchFeature",
|
|
754
|
+
register: container => {
|
|
755
|
+
container.register(MyBodyModifierImpl);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
```
|
|
759
|
+
|
|
411
760
|
## Related Skills
|
|
412
761
|
|
|
413
762
|
- **webiny-api-architect** — Full v6 architecture, Services vs UseCases, anti-patterns
|