Skip to content

Commit d8450e9

Browse files
Merge pull request #33 from developmentseed/feature/plugin-hooks
Implement hook system
2 parents 131a0d8 + 6e8a34b commit d8450e9

File tree

9 files changed

+307
-20
lines changed

9 files changed

+307
-20
lines changed

docs/PLUGINS.md

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
- [Object field](#object-field)
1313
- [Restrictions](#restrictions)
1414
- [Json field](#json-field)
15-
15+
- [Hooks](#hooks)
1616

1717
Each plugin should be created handle a specific part of the data.
1818
Avoid creating a single plugin to handle all the data, as this will make the code harder to maintain and understand.
@@ -389,3 +389,91 @@ The `json` field is an escape-hatch that allows the user to input any JSON data.
389389
```
390390

391391
![JSON](images/field-json.png)
392+
393+
## Hooks
394+
395+
> [!WARNING]
396+
> The hooks are an advanced feature and should be used with caution.
397+
398+
Hooks are functions that can be added to the plugins to perform actions at specific points in their lifecycle.
399+
400+
Available hooks:
401+
- `onAfterInit(targetInstance: Plugin, data: any) => void` - Executed after the target plugin's `init` function. Can be a promise.
402+
- `onAfterEditSchema(targetInstance: Plugin, formData: any, schema: PluginEditSchema) => PluginEditSchema` - Allows changing a plugin's schema. Receives the schema returned by the target plugin's `editSchema` function and the data entered in the form. Should return the modified schema.
403+
404+
Hooks should be registered in the plugin's constructor:
405+
406+
```ts
407+
import { Plugin } from '@stac-manager/data-core';
408+
409+
export class PluginName extends Plugin {
410+
name = 'Plugin Name';
411+
412+
constructor() {
413+
super();
414+
this.registerHook(
415+
'Target plugin name',
416+
'onAfterInit',
417+
(targetInstance: Plugin, data: any) => {
418+
// Do something.
419+
}
420+
);
421+
422+
this.registerHook(
423+
'Target plugin name',
424+
'onAfterEditSchema',
425+
(targetInstance: Plugin, formData: any, schema: PluginEditSchema) => {
426+
// Do something.
427+
return schema;
428+
}
429+
);
430+
}
431+
}
432+
```
433+
434+
**Real world example**
435+
Whenever an STAC extension plugin is added, it should add the extension to the `stac_extensions` field of the CollectionsCore plugin. This will allow the interface to show the extension as an option in the dropdown.
436+
437+
This can be achieved with th `onAfterEditSchema` hook:
438+
```ts
439+
import { Plugin } from '@stac-manager/data-core';
440+
441+
export class PluginName extends Plugin {
442+
name = 'Plugin Name';
443+
444+
constructor() {
445+
super();
446+
this.registerHook(
447+
'CollectionsCore',
448+
'onAfterEditSchema',
449+
(targetInstance: Plugin, formData: any, schema: PluginEditSchema) => {
450+
const stac_extensions = schema.properties
451+
.stac_extensions as SchemaFieldArray<SchemaFieldString>;
452+
453+
// Set the new extension value in the schema.
454+
stac_extensions.items.enum!.push([value, label]);
455+
456+
return schema;
457+
}
458+
);
459+
}
460+
}
461+
```
462+
463+
> [!TIP]
464+
> Given that this is a common enough use-case there is a helper function (`addStacExtensionOption`) that can be used.
465+
> ```ts
466+
>import { Plugin } from '@stac-manager/data-core';
467+
>import { addStacExtensionOption } from '@stac-manager/data-plugins';
468+
>
469+
>class PluginName extends Plugin {
470+
> constructor() {
471+
> super();
472+
>
473+
> addStacExtensionOption(
474+
> this,
475+
> 'Item Assets Definition',
476+
> 'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json'
477+
> );
478+
> }
479+
>}

packages/client/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,20 @@ See root README.md for instructions on how to install and run the project.
1010

1111
Some client options are controlled by environment variables. These are:
1212
```
13+
# App config
14+
## Title and description of the app for metadata
1315
APP_TITLE
1416
APP_DESCRIPTION
17+
18+
# API
19+
## If the app is being served in from a subfolder, the domain url must be set.
20+
PUBLIC_URL
1521
REACT_APP_STAC_BROWSER
1622
REACT_APP_STAC_API
23+
24+
# Theming
25+
REACT_APP_THEME_PRIMARY_COLOR
26+
REACT_APP_THEME_SECONDARY_COLOR
1727
```
1828

1929
You must provide a value for the `REACT_APP_STAC_API` environment variable. This should be the URL of the STAC API you wish to interact with.

packages/data-core/lib/context/plugin-config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const PluginConfigContext = createContext<PluginConfigContextProps | null>(
1313

1414
/**
1515
* Global context provider for the configuration
16-
*
16+
*
1717
* @param props.config Configuration object
1818
* @param props.children Child components
1919
*/

packages/data-core/lib/plugin-utils/plugin.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,31 @@ export type PluginEditSchema =
77
| undefined
88
| symbol;
99

10+
export interface PluginHook {
11+
// Name of the target plugin.
12+
name: string;
13+
14+
// The `onAfterInit` hook is executed after the target plugin's `init`
15+
// function.
16+
onAfterInit?: (pluginInstance: Plugin, data: any) => void;
17+
18+
// The `onAfterEditSchema` hook is composed with the target plugin's
19+
// `editSchema` function.
20+
onAfterEditSchema?: (
21+
pluginInstance: Plugin,
22+
formData: any,
23+
schema: PluginEditSchema
24+
) => PluginEditSchema;
25+
}
26+
27+
const HIDDEN: unique symbol = Symbol('hidden');
28+
const HOOKS: unique symbol = Symbol('hooks');
29+
1030
export abstract class Plugin {
11-
static HIDDEN = Symbol('hidden');
31+
static readonly HIDDEN: typeof HIDDEN = HIDDEN;
32+
static readonly HOOKS: typeof HOOKS = HOOKS;
33+
34+
[HOOKS]: PluginHook[] = [];
1235

1336
name: string = 'Plugin';
1437

@@ -25,6 +48,28 @@ export abstract class Plugin {
2548
exitData(data: any): Record<string, any> {
2649
throw new Error(`Plugin [${this.name}] must implement exitData`);
2750
}
51+
52+
/**
53+
* Registers a hook to be applied on a given plugin.
54+
*
55+
* @param targetName - The name of the target plugin to which the hook will be
56+
* applied.
57+
* @param hookName - The name of the hook.
58+
* @param hook - The hook function.
59+
*/
60+
registerHook<K extends keyof Omit<PluginHook, 'name'>>(
61+
targetName: string,
62+
hookName: K,
63+
hook: PluginHook[K]
64+
) {
65+
const hookEntry = this[Plugin.HOOKS].find((h) => h.name === targetName);
66+
67+
if (hookEntry) {
68+
hookEntry[hookName] = hook;
69+
} else {
70+
this[Plugin.HOOKS].push({ name: targetName, [hookName]: hook });
71+
}
72+
}
2873
}
2974

3075
export type PluginConfigResolved = Plugin | Plugin[] | undefined | null;

packages/data-core/lib/plugin-utils/resolve.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1+
import { cloneDeep } from 'lodash-es';
12
import { Plugin, PluginConfigItem } from './plugin';
23

4+
/**
5+
* Resolves the plugin config.
6+
*
7+
* @param plugins - An array of plugin configuration items which can be
8+
* instances of `Plugin` or functions that return a `Plugin`.
9+
* @param data - The data to be passed to plugin functions if they are not
10+
* instances of `Plugin`.
11+
* @returns An array of processed `Plugin` instances after applying hooks.
12+
*/
313
export const resolvePlugins = (plugins: PluginConfigItem[], data: any) => {
4-
return plugins
14+
const p = plugins
515
.flatMap((pl) => {
616
if (pl instanceof Plugin) {
717
return pl;
@@ -11,4 +21,84 @@ export const resolvePlugins = (plugins: PluginConfigItem[], data: any) => {
1121
return;
1222
})
1323
.filter((p) => p instanceof Plugin);
24+
25+
return applyHooks(p);
26+
};
27+
28+
/**
29+
* Applies hooks from the provided plugins to their respective targets.
30+
*
31+
* This function iterates over each plugin and applies hooks such as
32+
* `onAfterInit` and `onAfterEditSchema` to the corresponding target plugins.
33+
* The hooks are executed in the context of the source plugin.
34+
*
35+
* @param plugins - List of plugins. All the source and target plugins must be
36+
* on the list.
37+
*
38+
*
39+
* @remarks
40+
* - The `onAfterInit` hook is executed after the target plugin's `init`
41+
* function.
42+
* - The `onAfterEditSchema` hook is composed with the target plugin's
43+
* `editSchema` function.
44+
*
45+
* @example
46+
* ```typescript
47+
* class MyPlugin extends Plugin {
48+
* name = 'MyPlugin';
49+
*
50+
* [Plugin.HOOKS]: [
51+
* {
52+
* name: 'pluginA', // Target plugin
53+
* onAfterInit: async (targetInstance, data) => { }, // Executes after pluginA's init function.
54+
* onAfterEditSchema: (targetInstance, formData, origEditSchema) => { } // Composes with pluginA's editSchema function and returns a new one.
55+
* },
56+
* {
57+
* name: 'pluginB', // Target plugin
58+
* onAfterInit: async (targetInstance, data) => { }, // Executes after pluginB's init function.
59+
* }
60+
* ];
61+
* }
62+
*
63+
* applyHooks(plugins);
64+
* ```
65+
*/
66+
export const applyHooks = (plugins: Plugin[]) => {
67+
const pluginsCopy = cloneDeep(plugins);
68+
69+
for (const plSource of pluginsCopy) {
70+
for (const hook of plSource[Plugin.HOOKS]) {
71+
// Target where to apply the hook
72+
const plTarget = plugins.find((p) => p.name === hook.name);
73+
if (!plTarget) {
74+
continue;
75+
}
76+
77+
// The onAfterInit hook is made by executing one function after another.
78+
if (hook.onAfterInit) {
79+
const fn = hook.onAfterInit;
80+
const origInit = plTarget.init;
81+
plTarget.init = async (data: any) => {
82+
await origInit.call(plTarget, data);
83+
await fn.call(plSource, plTarget, data);
84+
};
85+
}
86+
87+
// The onAfterEditSchema hook is made by composing functions.
88+
if (hook.onAfterEditSchema) {
89+
const fn = hook.onAfterEditSchema;
90+
const origEditSchema = plTarget.editSchema;
91+
plTarget.editSchema = (formData?: any) => {
92+
return fn.call(
93+
plSource,
94+
plTarget,
95+
formData,
96+
origEditSchema.call(plTarget, formData)
97+
);
98+
};
99+
}
100+
}
101+
}
102+
103+
return pluginsCopy;
14104
};

packages/data-plugins/lib/collections/core.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,7 @@ export class PluginCore extends Plugin {
9999
allowOther: {
100100
type: 'string'
101101
},
102-
enum: [
103-
[
104-
'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json',
105-
'Item Assets Defenition'
106-
],
107-
[
108-
'https://stac-extensions.github.io/render/v2.0.0/schema.json',
109-
'Render'
110-
]
111-
]
102+
enum: []
112103
}
113104
},
114105
spatial: {

packages/data-plugins/lib/collections/ext-item-assets.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
import { Plugin, PluginEditSchema } from '@stac-manager/data-core';
2-
import { array2Object, hasExtension, object2Array } from '../utils';
2+
import {
3+
addStacExtensionOption,
4+
array2Object,
5+
hasStacExtension,
6+
object2Array
7+
} from '../utils';
38

49
export class PluginItemAssets extends Plugin {
510
name = 'Item Assets Extension';
611

12+
constructor() {
13+
super();
14+
15+
addStacExtensionOption(
16+
this,
17+
'Item Assets Definition',
18+
'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json'
19+
);
20+
}
21+
722
editSchema(data: any): PluginEditSchema {
8-
if (!hasExtension(data, 'item-assets')) {
23+
if (!hasStacExtension(data, 'item-assets')) {
924
return Plugin.HIDDEN;
1025
}
1126

packages/data-plugins/lib/collections/ext-render.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Plugin, PluginEditSchema } from '@stac-manager/data-core';
22
import {
3+
addStacExtensionOption,
34
array2Object,
4-
hasExtension,
5+
hasStacExtension,
56
object2Array,
67
object2Tuple,
78
tuple2Object
@@ -10,8 +11,18 @@ import {
1011
export class PluginRender extends Plugin {
1112
name = 'Render Extension';
1213

14+
constructor() {
15+
super();
16+
17+
addStacExtensionOption(
18+
this,
19+
'Render',
20+
'https://stac-extensions.github.io/render/v2.0.0/schema.json'
21+
);
22+
}
23+
1324
editSchema(data: any): PluginEditSchema {
14-
if (!hasExtension(data, 'render')) {
25+
if (!hasStacExtension(data, 'render')) {
1526
return Plugin.HIDDEN;
1627
}
1728

0 commit comments

Comments
 (0)