@wordpress/abilities 0.2.1-next.8b30e05b0.0 → 0.3.0

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +32 -40
  3. package/build/api.js +19 -66
  4. package/build/api.js.map +3 -3
  5. package/build/index.js.map +2 -2
  6. package/build/store/actions.js +41 -31
  7. package/build/store/actions.js.map +2 -2
  8. package/build/store/constants.js +6 -15
  9. package/build/store/constants.js.map +2 -2
  10. package/build/store/index.js +1 -27
  11. package/build/store/index.js.map +3 -3
  12. package/build/store/reducer.js +2 -22
  13. package/build/store/reducer.js.map +2 -2
  14. package/build/store/selectors.js +1 -1
  15. package/build/store/selectors.js.map +2 -2
  16. package/build/store/types.js +19 -0
  17. package/build/store/types.js.map +7 -0
  18. package/build/types.js.map +1 -1
  19. package/build-module/api.js +20 -57
  20. package/build-module/api.js.map +2 -2
  21. package/build-module/index.js.map +2 -2
  22. package/build-module/store/actions.js +43 -32
  23. package/build-module/store/actions.js.map +2 -2
  24. package/build-module/store/constants.js +4 -10
  25. package/build-module/store/constants.js.map +2 -2
  26. package/build-module/store/index.js +3 -34
  27. package/build-module/store/index.js.map +2 -2
  28. package/build-module/store/reducer.js +2 -24
  29. package/build-module/store/reducer.js.map +2 -2
  30. package/build-module/store/selectors.js +1 -1
  31. package/build-module/store/selectors.js.map +2 -2
  32. package/build-module/store/types.js +1 -0
  33. package/build-module/store/types.js.map +7 -0
  34. package/build-types/api.d.ts +16 -23
  35. package/build-types/api.d.ts.map +1 -1
  36. package/build-types/index.d.ts +1 -1
  37. package/build-types/index.d.ts.map +1 -1
  38. package/build-types/store/actions.d.ts +5 -27
  39. package/build-types/store/actions.d.ts.map +1 -1
  40. package/build-types/store/constants.d.ts +2 -5
  41. package/build-types/store/constants.d.ts.map +1 -1
  42. package/build-types/store/index.d.ts.map +1 -1
  43. package/build-types/store/reducer.d.ts +21 -11
  44. package/build-types/store/reducer.d.ts.map +1 -1
  45. package/build-types/store/selectors.d.ts +2 -1
  46. package/build-types/store/selectors.d.ts.map +1 -1
  47. package/build-types/store/types.d.ts +18 -0
  48. package/build-types/store/types.d.ts.map +1 -0
  49. package/build-types/types.d.ts +14 -24
  50. package/build-types/types.d.ts.map +1 -1
  51. package/package.json +4 -7
  52. package/src/api.ts +43 -123
  53. package/src/index.ts +0 -1
  54. package/src/store/actions.ts +61 -46
  55. package/src/store/constants.ts +4 -5
  56. package/src/store/index.ts +2 -34
  57. package/src/store/reducer.ts +27 -33
  58. package/src/store/selectors.ts +3 -7
  59. package/src/store/tests/actions.test.ts +100 -244
  60. package/src/store/tests/reducer.test.ts +0 -451
  61. package/src/store/tests/selectors.test.ts +1 -1
  62. package/src/store/types.ts +19 -0
  63. package/src/tests/api.test.ts +112 -158
  64. package/src/types.ts +14 -26
  65. package/build/store/resolvers.js +0 -107
  66. package/build/store/resolvers.js.map +0 -7
  67. package/build-module/store/resolvers.js +0 -84
  68. package/build-module/store/resolvers.js.map +0 -7
  69. package/build-types/store/resolvers.d.ts +0 -47
  70. package/build-types/store/resolvers.d.ts.map +0 -1
  71. package/src/store/resolvers.ts +0 -151
  72. package/src/store/tests/resolvers.test.ts +0 -642
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import type { Ability, AbilityCategory } from '../types';
5
+ /**
6
+ * The state shape for the abilities store.
7
+ */
8
+ export interface AbilitiesState {
9
+ /**
10
+ * Map of ability names to ability objects.
11
+ */
12
+ abilitiesByName: Record<string, Ability>;
13
+ /**
14
+ * Map of category slugs to category objects.
15
+ */
16
+ categoriesBySlug: Record<string, AbilityCategory>;
17
+ }
18
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEzD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B;;OAEG;IACH,eAAe,EAAE,MAAM,CAAE,MAAM,EAAE,OAAO,CAAE,CAAC;IAE3C;;OAEG;IACH,gBAAgB,EAAE,MAAM,CAAE,MAAM,EAAE,eAAe,CAAE,CAAC;CACpD"}
@@ -50,28 +50,27 @@ export interface Ability {
50
50
  */
51
51
  output_schema?: Record<string, any>;
52
52
  /**
53
- * Callback function for client-side abilities.
54
- * If present, the ability will be executed locally in the browser.
55
- * If not present, the ability will be executed via REST API on the server.
53
+ * Callback function for ability execution.
54
+ * This property is required for all abilities.
56
55
  */
57
56
  callback?: AbilityCallback;
58
57
  /**
59
- * Client Permission callback for abilities.
58
+ * Permission callback for abilities.
60
59
  * Called before executing the ability to check if it's allowed.
61
60
  * If it returns false, the ability execution will be denied.
62
61
  */
63
62
  permissionCallback?: PermissionCallback;
64
63
  /**
65
64
  * Metadata about the ability.
66
- * @see WP_Ability::get_meta()
67
65
  */
68
66
  meta?: {
69
67
  annotations?: {
70
- readonly?: boolean | null;
71
- destructive?: boolean | null;
72
- idempotent?: boolean | null;
68
+ clientRegistered?: boolean;
69
+ serverRegistered?: boolean;
70
+ readonly?: boolean;
71
+ destructive?: boolean;
72
+ idempotent?: boolean;
73
73
  };
74
- [key: string]: any;
75
74
  };
76
75
  }
77
76
  /**
@@ -108,9 +107,13 @@ export interface AbilityCategory {
108
107
  description: string;
109
108
  /**
110
109
  * Metadata about the category.
111
- * @see WP_Ability_Category::get_meta()
112
110
  */
113
- meta?: Record<string, any>;
111
+ meta?: {
112
+ annotations?: {
113
+ clientRegistered?: boolean;
114
+ serverRegistered?: boolean;
115
+ };
116
+ };
114
117
  }
115
118
  /**
116
119
  * Arguments for registering an ability category.
@@ -132,19 +135,6 @@ export interface AbilityCategoryArgs {
132
135
  */
133
136
  meta?: Record<string, any>;
134
137
  }
135
- /**
136
- * The state shape for the abilities store.
137
- */
138
- export interface AbilitiesState {
139
- /**
140
- * Map of ability names to ability objects.
141
- */
142
- abilitiesByName: Record<string, Ability>;
143
- /**
144
- * Map of category slugs to category objects.
145
- */
146
- categoriesBySlug: Record<string, AbilityCategory>;
147
- }
148
138
  /**
149
139
  * Input parameters for ability execution.
150
140
  * Can be any JSON-serializable value: primitive, array, object, or null.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,CAC7B,KAAK,EAAE,YAAY,KACf,aAAa,GAAG,OAAO,CAAE,aAAa,CAAE,CAAC;AAE9C;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAChC,KAAK,CAAC,EAAE,YAAY,KAChB,OAAO,GAAG,OAAO,CAAE,OAAO,CAAE,CAAC;AAElC;;;;GAIG;AACH,MAAM,WAAW,OAAO;IACvB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAE,MAAM,EAAE,GAAG,CAAE,CAAC;IAErC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAE,MAAM,EAAE,GAAG,CAAE,CAAC;IAEtC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAC;IAE3B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IAExC;;;OAGG;IACH,IAAI,CAAC,EAAE;QACN,WAAW,CAAC,EAAE;YACb,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;YAC1B,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;YAC7B,UAAU,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;SAC5B,CAAC;QACF,CAAE,GAAG,EAAE,MAAM,GAAI,GAAG,CAAC;KACrB,CAAC;CACF;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC/B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAE,MAAM,EAAE,GAAG,CAAE,CAAC;CAC7B;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAE,MAAM,EAAE,GAAG,CAAE,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B;;OAEG;IACH,eAAe,EAAE,MAAM,CAAE,MAAM,EAAE,OAAO,CAAE,CAAC;IAE3C;;OAEG;IACH,gBAAgB,EAAE,MAAM,CAAE,MAAM,EAAE,eAAe,CAAE,CAAC;CACpD;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC;AAE/B;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC;AAEhC;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,CAC7B,KAAK,EAAE,YAAY,KACf,aAAa,GAAG,OAAO,CAAE,aAAa,CAAE,CAAC;AAE9C;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAChC,KAAK,CAAC,EAAE,YAAY,KAChB,OAAO,GAAG,OAAO,CAAE,OAAO,CAAE,CAAC;AAElC;;;;GAIG;AACH,MAAM,WAAW,OAAO;IACvB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAE,MAAM,EAAE,GAAG,CAAE,CAAC;IAErC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAE,MAAM,EAAE,GAAG,CAAE,CAAC;IAEtC;;;OAGG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAC;IAE3B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IAExC;;OAEG;IACH,IAAI,CAAC,EAAE;QACN,WAAW,CAAC,EAAE;YACb,gBAAgB,CAAC,EAAE,OAAO,CAAC;YAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;YAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;YACnB,WAAW,CAAC,EAAE,OAAO,CAAC;YACtB,UAAU,CAAC,EAAE,OAAO,CAAC;SACrB,CAAC;KACF,CAAC;CACF;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC/B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,IAAI,CAAC,EAAE;QACN,WAAW,CAAC,EAAE;YACb,gBAAgB,CAAC,EAAE,OAAO,CAAC;YAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;SAC3B,CAAC;KACF,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAE,MAAM,EAAE,GAAG,CAAE,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC;AAE/B;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC;AAEhC;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/abilities",
3
- "version": "0.2.1-next.8b30e05b0.0",
3
+ "version": "0.3.0",
4
4
  "description": "JavaScript client for WordPress Abilities API.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -45,11 +45,8 @@
45
45
  },
46
46
  "types": "build-types",
47
47
  "dependencies": {
48
- "@wordpress/api-fetch": "^7.36.1-next.8b30e05b0.0",
49
- "@wordpress/core-data": "^7.36.1-next.8b30e05b0.0",
50
- "@wordpress/data": "^10.36.1-next.8b30e05b0.0",
51
- "@wordpress/i18n": "^6.9.1-next.8b30e05b0.0",
52
- "@wordpress/url": "^4.36.1-next.8b30e05b0.0",
48
+ "@wordpress/data": "^10.37.0",
49
+ "@wordpress/i18n": "^6.10.0",
53
50
  "ajv": "^8.17.1",
54
51
  "ajv-draft-04": "^1.0.0",
55
52
  "ajv-formats": "^3.0.1"
@@ -57,5 +54,5 @@
57
54
  "publishConfig": {
58
55
  "access": "public"
59
56
  },
60
- "gitHead": "2466f6bc223f8be98c55e1ac7270e8c3e413eaaf"
57
+ "gitHead": "2cf13ec6cf86153c9b3cf369bf5c59046f5cd950"
61
58
  }
package/src/api.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { dispatch, resolveSelect } from '@wordpress/data';
5
- import apiFetch from '@wordpress/api-fetch';
6
- import { addQueryArgs } from '@wordpress/url';
4
+ import { dispatch, select } from '@wordpress/data';
7
5
  import { sprintf } from '@wordpress/i18n';
8
6
 
9
7
  /**
@@ -24,43 +22,41 @@ import { validateValueFromSchema } from './validation';
24
22
  * Get all available abilities with optional filtering.
25
23
  *
26
24
  * @param args Optional query arguments to filter. Defaults to empty object.
27
- * @return Promise resolving to array of abilities.
25
+ * @return Array of abilities.
28
26
  */
29
- export async function getAbilities(
30
- args: AbilitiesQueryArgs = {}
31
- ): Promise< Ability[] > {
32
- return await resolveSelect( store ).getAbilities( args );
27
+ export function getAbilities( args: AbilitiesQueryArgs = {} ): Ability[] {
28
+ return select( store ).getAbilities( args );
33
29
  }
34
30
 
35
31
  /**
36
32
  * Get a specific ability by name.
37
33
  *
38
34
  * @param name The ability name.
39
- * @return Promise resolving to the ability or null if not found.
35
+ * @return The ability or undefined if not found.
40
36
  */
41
- export async function getAbility( name: string ): Promise< Ability | null > {
42
- return await resolveSelect( store ).getAbility( name );
37
+ export function getAbility( name: string ): Ability | undefined {
38
+ return select( store ).getAbility( name );
43
39
  }
44
40
 
45
41
  /**
46
42
  * Get all available ability categories.
47
43
  *
48
- * @return Promise resolving to array of categories.
44
+ * @return Array of categories.
49
45
  */
50
- export async function getAbilityCategories(): Promise< AbilityCategory[] > {
51
- return await resolveSelect( store ).getAbilityCategories();
46
+ export function getAbilityCategories(): AbilityCategory[] {
47
+ return select( store ).getAbilityCategories();
52
48
  }
53
49
 
54
50
  /**
55
51
  * Get a specific ability category by slug.
56
52
  *
57
53
  * @param slug The category slug.
58
- * @return Promise resolving to the category or null if not found.
54
+ * @return The category or undefined if not found.
59
55
  */
60
- export async function getAbilityCategory(
56
+ export function getAbilityCategory(
61
57
  slug: string
62
- ): Promise< AbilityCategory | null > {
63
- return await resolveSelect( store ).getAbilityCategory( slug );
58
+ ): AbilityCategory | undefined {
59
+ return select( store ).getAbilityCategory( slug );
64
60
  }
65
61
 
66
62
  /**
@@ -70,17 +66,14 @@ export async function getAbilityCategory(
70
66
  * a callback function. The ability will be validated by the store action,
71
67
  * and an error will be thrown if validation fails.
72
68
  *
73
- * Categories will be automatically fetched from the REST API if they
74
- * haven't been loaded yet, so you don't need to call getAbilityCategories()
75
- * before registering abilities.
69
+ * The category must already be registered before registering abilities.
76
70
  *
77
71
  * @param ability The ability definition including callback.
78
- * @return Promise that resolves when registration is complete.
79
72
  * @throws {Error} If the ability fails validation.
80
73
  *
81
74
  * @example
82
75
  * ```js
83
- * await registerAbility({
76
+ * registerAbility({
84
77
  * name: 'my-plugin/navigate',
85
78
  * label: 'Navigate to URL',
86
79
  * description: 'Navigates to a URL within WordPress admin',
@@ -99,8 +92,8 @@ export async function getAbilityCategory(
99
92
  * });
100
93
  * ```
101
94
  */
102
- export async function registerAbility( ability: Ability ): Promise< void > {
103
- await dispatch( store ).registerAbility( ability );
95
+ export function registerAbility( ability: Ability ): void {
96
+ dispatch( store ).registerAbility( ability );
104
97
  }
105
98
 
106
99
  /**
@@ -123,24 +116,20 @@ export function unregisterAbility( name: string ): void {
123
116
  * This is useful when registering client-side abilities that introduce new
124
117
  * categories not defined by the server.
125
118
  *
126
- * Categories will be automatically fetched from the REST API if they haven't been
127
- * loaded yet to check for duplicates against server-side categories.
128
- *
129
119
  * @param slug Category slug (lowercase alphanumeric with dashes only).
130
120
  * @param args Category arguments (label, description, optional meta).
131
- * @return Promise that resolves when registration is complete.
132
121
  * @throws {Error} If the category fails validation.
133
122
  *
134
123
  * @example
135
124
  * ```js
136
125
  * // Register a new category for block editor abilities
137
- * await registerAbilityCategory('block-editor', {
126
+ * registerAbilityCategory('block-editor', {
138
127
  * label: 'Block Editor',
139
128
  * description: 'Abilities for interacting with the WordPress block editor'
140
129
  * });
141
130
  *
142
131
  * // Then register abilities using this category
143
- * await registerAbility({
132
+ * registerAbility({
144
133
  * name: 'my-plugin/insert-block',
145
134
  * label: 'Insert Block',
146
135
  * description: 'Inserts a block into the editor',
@@ -152,11 +141,11 @@ export function unregisterAbility( name: string ): void {
152
141
  * });
153
142
  * ```
154
143
  */
155
- export async function registerAbilityCategory(
144
+ export function registerAbilityCategory(
156
145
  slug: string,
157
146
  args: AbilityCategoryArgs
158
- ): Promise< void > {
159
- await dispatch( store ).registerAbilityCategory( slug, args );
147
+ ): void {
148
+ dispatch( store ).registerAbilityCategory( slug, args );
160
149
  }
161
150
 
162
151
  /**
@@ -176,21 +165,29 @@ export function unregisterAbilityCategory( slug: string ): void {
176
165
  }
177
166
 
178
167
  /**
179
- * Execute a client-side ability.
168
+ * Execute an ability.
180
169
  *
181
- * @param ability The ability to execute.
182
- * @param input Input parameters for the ability.
170
+ * Executes abilities with validation for client-side abilities only.
171
+ * Server abilities bypass validation as it's handled on the server.
172
+ *
173
+ * @param name The ability name.
174
+ * @param input Optional input parameters for the ability.
183
175
  * @return Promise resolving to the ability execution result.
184
- * @throws Error if validation fails or execution errors.
176
+ * @throws Error if the ability is not found or execution fails.
185
177
  */
186
- async function executeClientAbility(
187
- ability: Ability,
188
- input: AbilityInput
178
+ export async function executeAbility(
179
+ name: string,
180
+ input?: AbilityInput
189
181
  ): Promise< AbilityOutput > {
182
+ const ability = getAbility( name );
183
+ if ( ! ability ) {
184
+ throw new Error( sprintf( 'Ability not found: %s', name ) );
185
+ }
186
+
190
187
  if ( ! ability.callback ) {
191
188
  throw new Error(
192
189
  sprintf(
193
- 'Client ability %s is missing callback function',
190
+ 'Ability "%s" is missing callback. Please ensure the ability is properly registered.',
194
191
  ability.name
195
192
  )
196
193
  );
@@ -208,6 +205,7 @@ async function executeClientAbility(
208
205
  }
209
206
  }
210
207
 
208
+ // Validate input
211
209
  if ( ability.input_schema ) {
212
210
  const inputValidation = validateValueFromSchema(
213
211
  input,
@@ -227,18 +225,17 @@ async function executeClientAbility(
227
225
  }
228
226
  }
229
227
 
228
+ // Execute the ability
230
229
  let result: AbilityOutput;
231
230
  try {
232
231
  result = await ability.callback( input );
233
232
  } catch ( error ) {
234
233
  // eslint-disable-next-line no-console
235
- console.error(
236
- `Error executing client ability ${ ability.name }:`,
237
- error
238
- );
234
+ console.error( `Error executing ability ${ ability.name }:`, error );
239
235
  throw error;
240
236
  }
241
237
 
238
+ // Validate output
242
239
  if ( ability.output_schema ) {
243
240
  const outputValidation = validateValueFromSchema(
244
241
  result,
@@ -260,80 +257,3 @@ async function executeClientAbility(
260
257
 
261
258
  return result;
262
259
  }
263
-
264
- /**
265
- * Execute a server-side ability.
266
- *
267
- * @param ability The ability to execute.
268
- * @param input Input parameters for the ability.
269
- * @return Promise resolving to the ability execution result.
270
- * @throws Error if the API call fails.
271
- */
272
- async function executeServerAbility(
273
- ability: Ability,
274
- input: AbilityInput
275
- ): Promise< AbilityOutput > {
276
- let method = 'POST';
277
- if ( !! ability.meta?.annotations?.readonly ) {
278
- method = 'GET';
279
- } else if (
280
- !! ability.meta?.annotations?.destructive &&
281
- !! ability.meta?.annotations?.idempotent
282
- ) {
283
- method = 'DELETE';
284
- }
285
-
286
- let path = `/wp-abilities/v1/abilities/${ ability.name }/run`;
287
- const options: {
288
- method: string;
289
- data?: { input: AbilityInput };
290
- } = {
291
- method,
292
- };
293
-
294
- if ( [ 'GET', 'DELETE' ].includes( method ) && input !== null ) {
295
- // For GET and DELETE requests, pass the input directly.
296
- path = addQueryArgs( path, { input } );
297
- } else if ( method === 'POST' && input !== null ) {
298
- options.data = { input };
299
- }
300
-
301
- // Note: Input and output validation happens on the server side for these abilities.
302
- try {
303
- return await apiFetch< AbilityOutput >( {
304
- path,
305
- ...options,
306
- } );
307
- } catch ( error ) {
308
- // eslint-disable-next-line no-console
309
- console.error( `Error executing ability ${ ability.name }:`, error );
310
- throw error;
311
- }
312
- }
313
-
314
- /**
315
- * Execute an ability.
316
- *
317
- * Determines whether to execute locally (client abilities) or remotely (server abilities)
318
- * based on whether the ability has a callback function.
319
- *
320
- * @param name The ability name.
321
- * @param input Optional input parameters for the ability.
322
- * @return Promise resolving to the ability execution result.
323
- * @throws Error if the ability is not found or execution fails.
324
- */
325
- export async function executeAbility(
326
- name: string,
327
- input?: AbilityInput
328
- ): Promise< AbilityOutput > {
329
- const ability = await getAbility( name );
330
- if ( ! ability ) {
331
- throw new Error( sprintf( 'Ability not found: %s', name ) );
332
- }
333
-
334
- if ( ability.callback ) {
335
- return executeClientAbility( ability, input );
336
- }
337
-
338
- return executeServerAbility( ability, input );
339
- }
package/src/index.ts CHANGED
@@ -50,7 +50,6 @@ export type {
50
50
  Ability,
51
51
  AbilityCategory,
52
52
  AbilityCategoryArgs,
53
- AbilitiesState,
54
53
  AbilitiesQueryArgs,
55
54
  AbilityCallback,
56
55
  PermissionCallback,
@@ -2,54 +2,50 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { sprintf } from '@wordpress/i18n';
5
- import { resolveSelect } from '@wordpress/data';
6
5
 
7
6
  /**
8
7
  * Internal dependencies
9
8
  */
10
9
  import type { Ability, AbilityCategory, AbilityCategoryArgs } from '../types';
11
10
  import {
12
- RECEIVE_ABILITIES,
13
11
  REGISTER_ABILITY,
14
12
  UNREGISTER_ABILITY,
15
- RECEIVE_CATEGORIES,
16
13
  REGISTER_ABILITY_CATEGORY,
17
14
  UNREGISTER_ABILITY_CATEGORY,
18
- STORE_NAME,
15
+ ABILITY_NAME_PATTERN,
16
+ CATEGORY_SLUG_PATTERN,
19
17
  } from './constants';
20
18
 
21
- /**
22
- * Returns an action object used to receive abilities into the store.
23
- *
24
- * @param abilities Array of abilities to store.
25
- * @return Action object.
26
- */
27
- export function receiveAbilities( abilities: Ability[] ) {
28
- return {
29
- type: RECEIVE_ABILITIES,
30
- abilities,
31
- };
32
- }
19
+ type AbilityAnnotations = NonNullable< Ability[ 'meta' ] >[ 'annotations' ];
33
20
 
34
21
  /**
35
- * Returns an action object used to receive categories into the store.
22
+ * Filters annotations to only include allowed keys with non-null values.
36
23
  *
37
- * @param categories Array of categories to store.
38
- * @return Action object.
24
+ * @param sourceAnnotations The source annotations object to filter.
25
+ * @param allowedKeys Array of annotation keys to include.
26
+ * @return Filtered annotations object.
39
27
  */
40
- export function receiveCategories( categories: AbilityCategory[] ) {
41
- return {
42
- type: RECEIVE_CATEGORIES,
43
- categories,
44
- };
28
+ function filterAnnotations< K extends keyof NonNullable< AbilityAnnotations > >(
29
+ sourceAnnotations: Record< string, boolean > | undefined,
30
+ allowedKeys: readonly K[]
31
+ ): NonNullable< AbilityAnnotations > {
32
+ const annotations: NonNullable< AbilityAnnotations > = {};
33
+
34
+ if ( sourceAnnotations ) {
35
+ for ( const key of allowedKeys ) {
36
+ if ( sourceAnnotations[ key ] !== undefined ) {
37
+ annotations[ key ] = sourceAnnotations[ key ];
38
+ }
39
+ }
40
+ }
41
+ return annotations;
45
42
  }
46
43
 
47
44
  /**
48
45
  * Registers an ability in the store.
49
46
  *
50
47
  * This action validates the ability before registration. If validation fails,
51
- * an error will be thrown. Categories will be automatically fetched from the
52
- * REST API if they haven't been loaded yet.
48
+ * an error will be thrown.
53
49
  *
54
50
  * @param ability The ability to register.
55
51
  * @return Action object or function.
@@ -57,13 +53,13 @@ export function receiveCategories( categories: AbilityCategory[] ) {
57
53
  */
58
54
  export function registerAbility( ability: Ability ) {
59
55
  // @ts-expect-error - registry types are not yet available
60
- return async ( { select, dispatch } ) => {
56
+ return ( { select, dispatch } ) => {
61
57
  if ( ! ability.name ) {
62
58
  throw new Error( 'Ability name is required' );
63
59
  }
64
60
 
65
61
  // Validate name format matches server implementation
66
- if ( ! /^[a-z0-9-]+\/[a-z0-9-]+$/.test( ability.name ) ) {
62
+ if ( ! ABILITY_NAME_PATTERN.test( ability.name ) ) {
67
63
  throw new Error(
68
64
  'Ability name must be a string containing a namespace prefix, i.e. "my-plugin/my-ability". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'
69
65
  );
@@ -88,19 +84,18 @@ export function registerAbility( ability: Ability ) {
88
84
  }
89
85
 
90
86
  // Validate category format
91
- if ( ! /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test( ability.category ) ) {
87
+ if ( ! CATEGORY_SLUG_PATTERN.test( ability.category ) ) {
92
88
  throw new Error(
93
89
  sprintf(
94
- 'Ability "%1$s" has an invalid category. Category must be lowercase alphanumeric with dashes only Got: "%2$s"',
90
+ 'Ability "%1$s" has an invalid category. Category must be lowercase alphanumeric with dashes only. Got: "%2$s"',
95
91
  ability.name,
96
92
  ability.category
97
93
  )
98
94
  );
99
95
  }
100
96
 
101
- // Ensure categories are loaded before validating
102
- const categories =
103
- await resolveSelect( STORE_NAME ).getAbilityCategories();
97
+ // Check that the category exists
98
+ const categories = select.getAbilityCategories();
104
99
  const existingCategory = categories.find(
105
100
  ( cat: AbilityCategory ) => cat.slug === ability.category
106
101
  );
@@ -132,10 +127,27 @@ export function registerAbility( ability: Ability ) {
132
127
  );
133
128
  }
134
129
 
130
+ const annotations = filterAnnotations( ability.meta?.annotations, [
131
+ 'readonly',
132
+ 'destructive',
133
+ 'idempotent',
134
+ 'serverRegistered',
135
+ 'clientRegistered',
136
+ ] );
137
+
138
+ if ( ! annotations.serverRegistered ) {
139
+ annotations.clientRegistered = true;
140
+ }
141
+
142
+ const meta = { annotations };
143
+
135
144
  // All validation passed, dispatch the registration action
136
145
  dispatch( {
137
146
  type: REGISTER_ABILITY,
138
- ability,
147
+ ability: {
148
+ ...ability,
149
+ meta,
150
+ },
139
151
  } );
140
152
  };
141
153
  }
@@ -157,8 +169,7 @@ export function unregisterAbility( name: string ) {
157
169
  * Registers a client-side ability category in the store.
158
170
  *
159
171
  * This action validates the category before registration. If validation fails,
160
- * an error will be thrown. Categories will be automatically fetched from the
161
- * REST API if they haven't been loaded yet to check for duplicates.
172
+ * an error will be thrown.
162
173
  *
163
174
  * @param slug The unique category slug identifier.
164
175
  * @param args Category arguments (label, description, optional meta).
@@ -170,20 +181,19 @@ export function registerAbilityCategory(
170
181
  args: AbilityCategoryArgs
171
182
  ) {
172
183
  // @ts-expect-error - registry types are not yet available
173
- return async ( { select, dispatch } ) => {
184
+ return ( { select, dispatch } ) => {
174
185
  if ( ! slug ) {
175
186
  throw new Error( 'Category slug is required' );
176
187
  }
177
188
 
178
189
  // Validate slug format matches server implementation
179
- if ( ! /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test( slug ) ) {
190
+ if ( ! CATEGORY_SLUG_PATTERN.test( slug ) ) {
180
191
  throw new Error(
181
192
  'Category slug must contain only lowercase alphanumeric characters and dashes.'
182
193
  );
183
194
  }
184
195
 
185
- // Ensure categories are loaded before checking for duplicates
186
- await resolveSelect( STORE_NAME ).getAbilityCategories();
196
+ // Check for duplicates
187
197
  const existingCategory = select.getAbilityCategory( slug );
188
198
  if ( existingCategory ) {
189
199
  throw new Error(
@@ -214,16 +224,21 @@ export function registerAbilityCategory(
214
224
  );
215
225
  }
216
226
 
227
+ const annotations = filterAnnotations( args.meta?.annotations, [
228
+ 'serverRegistered',
229
+ 'clientRegistered',
230
+ ] );
231
+
232
+ if ( ! annotations.serverRegistered ) {
233
+ annotations.clientRegistered = true;
234
+ }
235
+
236
+ const meta = { annotations };
217
237
  const category: AbilityCategory = {
218
238
  slug,
219
239
  label: args.label,
220
240
  description: args.description,
221
- meta: {
222
- ...( args.meta || {} ),
223
- // Internal implementation note: Client-registered categories will have `meta._clientRegistered` set to `true` to differentiate them from server-fetched categories.
224
- // This is used internally by the resolver to determine whether to fetch categories from the server.
225
- _clientRegistered: true,
226
- },
241
+ meta,
227
242
  };
228
243
 
229
244
  dispatch( {
@@ -2,14 +2,13 @@
2
2
  * Store constants
3
3
  */
4
4
  export const STORE_NAME = 'core/abilities';
5
- export const ENTITY_KIND = 'root';
6
- export const ENTITY_NAME = 'abilities';
7
- export const ENTITY_NAME_CATEGORIES = 'ability-categories';
5
+
6
+ // Validation patterns
7
+ export const ABILITY_NAME_PATTERN = /^[a-z0-9-]+\/[a-z0-9-]+$/;
8
+ export const CATEGORY_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
9
 
9
10
  // Action types
10
- export const RECEIVE_ABILITIES = 'RECEIVE_ABILITIES';
11
11
  export const REGISTER_ABILITY = 'REGISTER_ABILITY';
12
12
  export const UNREGISTER_ABILITY = 'UNREGISTER_ABILITY';
13
- export const RECEIVE_CATEGORIES = 'RECEIVE_CATEGORIES';
14
13
  export const REGISTER_ABILITY_CATEGORY = 'REGISTER_ABILITY_CATEGORY';
15
14
  export const UNREGISTER_ABILITY_CATEGORY = 'UNREGISTER_ABILITY_CATEGORY';