Skip to content

Commit

Permalink
feat(agent): allow to create update record custom actions from the fr…
Browse files Browse the repository at this point in the history
…ontend (#729)
  • Loading branch information
Thenkei authored Jun 15, 2023
1 parent 7f48cd1 commit e06ac79
Show file tree
Hide file tree
Showing 24 changed files with 706 additions and 533 deletions.
28 changes: 9 additions & 19 deletions packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import bodyParser from 'koa-bodyparser';
import FrameworkMounter from './framework-mounter';
import makeRoutes from './routes';
import makeServices from './services';
import ActionCustomizationService from './services/model-customizations/action-customization';
import CustomizationService from './services/model-customizations/customization';
import { AgentOptions, AgentOptionsWithDefaults } from './types';
import SchemaGenerator from './utils/forest-schema/generator';
import OptionsValidator from './utils/options-validator';
Expand All @@ -38,7 +38,7 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter
private options: AgentOptionsWithDefaults;
private customizer: DataSourceCustomizer<S>;
private nocodeCustomizer: DataSourceCustomizer<S>;
private actionCustomizationService: ActionCustomizationService;
private customizationService: CustomizationService;

/**
* Create a new Agent Builder.
Expand All @@ -62,7 +62,7 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter

this.options = allOptions;
this.customizer = new DataSourceCustomizer<S>();
this.actionCustomizationService = new ActionCustomizationService(allOptions);
this.customizationService = new CustomizationService(allOptions);
}

/**
Expand Down Expand Up @@ -170,15 +170,12 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter
}

private async buildRouterAndSendSchema(): Promise<Router> {
const { isProduction, logger, typingsPath, typingsMaxDepth, experimental } = this.options;
const { isProduction, logger, typingsPath, typingsMaxDepth } = this.options;

// It allows to rebuild the full customization stack with no code customizations
this.nocodeCustomizer = new DataSourceCustomizer<S>();
this.nocodeCustomizer.addDataSource(this.customizer.getFactory());
this.nocodeCustomizer.use(
this.actionCustomizationService.addWebhookActions,
experimental?.webhookCustomActions,
);
this.nocodeCustomizer.use(this.customizationService.addCustomizations);

const dataSource = await this.nocodeCustomizer.getDataSource(logger);
const [router] = await Promise.all([
Expand Down Expand Up @@ -219,7 +216,10 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter
throw new Error(`Can't load ${schemaPath}. Providing a schema is mandatory in production.`);
}
} else {
schema = await SchemaGenerator.buildSchema(dataSource, this.buildSchemaFeatures());
schema = await SchemaGenerator.buildSchema(
dataSource,
this.customizationService.buildFeatures(),
);

const pretty = stringify(schema, { maxLength: 100 });
await writeFile(schemaPath, pretty, { encoding: 'utf-8' });
Expand All @@ -233,14 +233,4 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter

this.options.logger('Info', message);
}

private buildSchemaFeatures(): string[] | null {
const mapping: Record<keyof AgentOptions['experimental'], string> = {
webhookCustomActions: ActionCustomizationService.FEATURE,
};

return Object.entries(mapping)
.filter(([experimentalFeature]) => this.options.experimental?.[experimentalFeature])
.map(([, feature]) => feature);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
ActionConfiguration,
ActionType,
ModelCustomization,
ModelCustomizationType,
} from '@forestadmin/forestadmin-client';

export default function getActions<TConfiguration extends ActionConfiguration>(
type: ActionType,
configuration: ModelCustomization[],
): ModelCustomization<TConfiguration>[] {
return configuration.filter(
customization =>
customization.type === ModelCustomizationType.action &&
(customization as ModelCustomization<TConfiguration>).configuration.type === type,
) as ModelCustomization<TConfiguration>[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Plugin } from '@forestadmin/datasource-customizer';
import {
ModelCustomization,
UpdateRecordActionConfiguration,
} from '@forestadmin/forestadmin-client';

import getActions from '../get-actions';

export default class UpdateRecordActionsPlugin {
public static VERSION = '1.0.0';
public static FEATURE = 'update-record-actions';

public static addUpdateRecordActions: Plugin<ModelCustomization[]> = (
datasourceCustomizer,
_,
modelCustomizations,
) => {
const actions = getActions<UpdateRecordActionConfiguration>(
'update-record',
modelCustomizations,
);

actions.forEach(action => {
const collection = datasourceCustomizer.getCollection(action.modelName);

collection.addAction(action.name, {
scope: action.configuration.scope,
execute: async context => {
const {
configuration: {
configuration: { fields },
},
} = action;

await context.collection.update(context.filter, fields);
},
});
});
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Plugin } from '@forestadmin/datasource-customizer';
import { ModelCustomization, WebhookActionConfiguration } from '@forestadmin/forestadmin-client';

import executeWebhook from './execute-webhook';
import getActions from '../get-actions';

export default class WebhookActionsPlugin {
public static VERSION = '1.0.0';
public static FEATURE = 'webhook-custom-actions';

public static addWebhookActions: Plugin<ModelCustomization[]> = (
datasourceCustomizer,
_,
modelCustomizations,
) => {
const actions = getActions<WebhookActionConfiguration>('webhook', modelCustomizations);
actions.forEach(action => {
const collection = datasourceCustomizer.getCollection(action.modelName);

collection.addAction(action.name, {
scope: action.configuration.scope,
execute: context => executeWebhook(action, context),
});
});
};
}
89 changes: 89 additions & 0 deletions packages/agent/src/services/model-customizations/customization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Plugin } from '@forestadmin/datasource-customizer';
import { ForestAdminClient, ModelCustomization } from '@forestadmin/forestadmin-client';

import UpdateRecordActionsPlugin from './actions/update-record/update-record-plugin';
import WebhookActionsPlugin from './actions/webhook/webhook-plugin';
import { AgentOptionsWithDefaults } from '../../types';

type ExperimentalOptions = AgentOptionsWithDefaults['experimental'];

const optionsToFeatureMapping: Record<keyof ExperimentalOptions, string> = {
webhookCustomActions: WebhookActionsPlugin.FEATURE,
updateRecordCustomActions: UpdateRecordActionsPlugin.FEATURE,
};

const featuresFormattedWithVersion = [
{ feature: WebhookActionsPlugin.FEATURE, version: WebhookActionsPlugin.VERSION },
{ feature: UpdateRecordActionsPlugin.FEATURE, version: UpdateRecordActionsPlugin.VERSION },
];

export default class CustomizationPluginService {
private readonly client: ForestAdminClient;

private readonly options: AgentOptionsWithDefaults;

public constructor(agentOptions: AgentOptionsWithDefaults) {
this.client = agentOptions.forestAdminClient;

this.options = agentOptions;
}

public addCustomizations: Plugin<void> = async (datasourceCustomizer, _) => {
const { experimental } = this.options;
if (!experimental) return;

const modelCustomizations = await this.client.modelCustomizationService.getConfiguration();

CustomizationPluginService.makeAddCustomizations(experimental)(
datasourceCustomizer,
_,
modelCustomizations,
);
};

public static makeAddCustomizations: (
experimental: ExperimentalOptions,
) => Plugin<ModelCustomization[]> = (experimental: ExperimentalOptions) => {
return (datasourceCustomizer, _, modelCustomizations) => {
if (experimental.webhookCustomActions) {
WebhookActionsPlugin.addWebhookActions(datasourceCustomizer, _, modelCustomizations);
}

if (experimental.updateRecordCustomActions) {
UpdateRecordActionsPlugin.addUpdateRecordActions(
datasourceCustomizer,
_,
modelCustomizations,
);
}
};
};

public buildFeatures() {
return CustomizationPluginService.buildFeatures(this.options?.experimental);
}

public static buildFeatures(experimental: ExperimentalOptions): Record<string, string> | null {
const features = CustomizationPluginService.getFeatures(experimental);

const enabledFeaturesFormattedWithVersion = featuresFormattedWithVersion
.filter(({ feature }) => features?.includes(feature))
.reduce(
(acc, { feature, version }) => ({
...acc,
[feature]: version,
}),
{},
);

return Object.keys(enabledFeaturesFormattedWithVersion).length
? enabledFeaturesFormattedWithVersion
: null;
}

private static getFeatures(experimental: ExperimentalOptions): string[] {
return Object.entries(optionsToFeatureMapping)
.filter(([experimentalFeature]) => experimental?.[experimentalFeature])
.map(([, feature]) => feature);
}
}
1 change: 1 addition & 0 deletions packages/agent/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type AgentOptions = {
forestAdminClient?: ForestAdminClient;
experimental?: {
webhookCustomActions?: boolean;
updateRecordCustomActions?: boolean;
};
};
export type AgentOptionsWithDefaults = Readonly<Required<AgentOptions>>;
Expand Down
24 changes: 5 additions & 19 deletions packages/agent/src/utils/forest-schema/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { DataSource } from '@forestadmin/datasource-toolkit';
import { ForestSchema } from '@forestadmin/forestadmin-client';

import SchemaGeneratorCollection from './generator-collection';
import ActionCustomizationService from '../../services/model-customizations/action-customization';

export default class SchemaGenerator {
static async buildSchema(dataSource: DataSource, features: string[]): Promise<ForestSchema> {
static async buildSchema(
dataSource: DataSource,
features: Record<string, string> | null,
): Promise<ForestSchema> {
const { version } = require('../../../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires,global-require,max-len

return {
Expand All @@ -17,28 +19,12 @@ export default class SchemaGenerator {
metadata: {
liana: 'forest-nodejs-agent',
liana_version: version,
liana_features: SchemaGenerator.buildFeatures(features),
liana_features: features,
stack: {
engine: 'nodejs',
engine_version: process.versions && process.versions.node,
},
},
};
}

private static buildFeatures(features: string[]): Record<string, string> {
const result = Object.entries({
[ActionCustomizationService.FEATURE]: ActionCustomizationService.VERSION,
})
.filter(([feature]) => features.includes(feature))
.reduce(
(acc, [feature, version]) => ({
...acc,
[feature]: version,
}),
{},
);

return Object.keys(result).length ? result : null;
}
}
1 change: 1 addition & 0 deletions packages/agent/test/__factories__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { default as serializer } from './serializer';
export { default as superagent } from './superagent';
export { default as authorization } from './authorization/authorization';
export { default as forestAdminClient } from './forest-admin-client';
export { default as customization } from './model-customizations/customization';

This file was deleted.

Loading

0 comments on commit e06ac79

Please sign in to comment.