diff --git a/packages/_example/src/forest/agent.ts b/packages/_example/src/forest/agent.ts index 3bfa639f28..f31f9d9ca2 100644 --- a/packages/_example/src/forest/agent.ts +++ b/packages/_example/src/forest/agent.ts @@ -32,7 +32,9 @@ export default function makeAgent() { typingsPath: 'src/forest/typings.ts', }; - return createAgent(envOptions) + const agent = createAgent(envOptions); + + return agent .addDataSource(createSqlDataSource({ dialect: 'sqlite', storage: './assets/db.sqlite' })) .addDataSource( @@ -75,6 +77,27 @@ export default function makeAgent() { }) .customizeCollection('card', customizeCard) + .customizeCollection('card', cards => { + cards.addAction('Hello', { + scope: 'Bulk', + async execute(context, resultBuilder) { + const notif = { + notification: { + message: { + type: 'warning', + text: 'Data refreshed', + }, + refresh: { + collectionName: 'card', + }, + }, + }; + globalThis.publicServices.sendNotifications(notif); + + return resultBuilder.success('hello'); + }, + }); + }) .customizeCollection('account', customizeAccount) .customizeCollection('owner', customizeOwner) .customizeCollection('store', customizeStore) diff --git a/packages/_example/src/forest/customizations/card.ts b/packages/_example/src/forest/customizations/card.ts index 90ea6cc0fc..8675875003 100644 --- a/packages/_example/src/forest/customizations/card.ts +++ b/packages/_example/src/forest/customizations/card.ts @@ -1,9 +1,94 @@ import { CardCustomizer } from '../typings'; +export type NotificationFromAgent = { + notification: + | { refresh: { collectionName: string; recordIds?: string[] } } + | { message: { type: 'success' | 'info' | 'warning' | 'error'; text: string } }; + target?: { users?: string[]; team?: string; roles?: string[] }; +}; + +const send = async (notif: NotificationFromAgent, resultBuilder?) => { + const resp = await fetch( + `https://api.development.forestadmin.com/liana/notifications-from-agent`, + { + method: 'POST', + body: JSON.stringify(notif), + headers: { + 'forest-secret-key': '5eb3ab09768a960a059bfaac57db9e1d2b33633a6d37cd4c13e19100c553bf14', + 'Content-Type': 'application/json', + }, + }, + ); + + return resultBuilder?.success(`Notif sent !!!`); +}; + export default (collection: CardCustomizer) => collection .addManyToOneRelation('customer', 'customer', { foreignKey: 'customer_id' }) - .addAction('Create new card', { + .addAction('trigger notif to everyone', { + scope: 'Global', + async execute(context, resultBuilder) { + const notif: NotificationFromAgent = { + notification: { + message: { + type: 'warning', + text: 'Data refreshed', + }, + refresh: { + collectionName: 'card', + }, + }, + }; + + return send(notif, resultBuilder); + }, + }) + .addAction('trigger notif to nicolas@email.com', { + scope: 'Global', + async execute(context, resultBuilder) { + const notif: NotificationFromAgent = { + notification: { + message: { + type: 'warning', + text: 'Your data has been refreshed Nicolas', + }, + refresh: { + collectionName: 'card', + }, + }, + target: { users: ['nicolas@email.com'] }, + }; + + return send(notif, resultBuilder); + }, + }) + .addAction('send love to…', { + scope: 'Global', + submitButtonLabel: '😘', + form: [ + { + label: 'Who do you love?', + id: 'loved', + type: 'StringList', + widget: 'UserDropdown', + }, + ], + async execute(context, resultBuilder) { + const notif: NotificationFromAgent = { + notification: { + message: { + type: 'info', + text: `❤️ ${context.caller.firstName} ${context.caller.lastName} loves you ❤️`, + }, + }, + target: { users: context.formValues.loved }, + }; + + return send(notif, resultBuilder); + }, + }) + .addAction('create new card', { scope: 'Global', execute: (context, resultBuilder) => { return resultBuilder.success('ok'); @@ -136,4 +221,26 @@ export default (collection: CardCustomizer) => ], }, ], + }) + .addAction('Escalate', { + scope: 'Single', + execute: async (context, resultBuilder) => { + await context.collection.update(context.filter, { is_active: false }); + + const notif: NotificationFromAgent = { + notification: { + message: { + type: 'warning', + text: 'A new card has been escalated', + }, + refresh: { + collectionName: 'card', + }, + }, + target: { users: ['nicolas@email.com'] }, + }; + await send(notif, resultBuilder); + + return resultBuilder.success('Card escalated to back office'); + }, }); diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 75a87619f2..eec1821f6c 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -9,7 +9,7 @@ import { TSchema, } from '@forestadmin/datasource-customizer'; import { DataSource, DataSourceFactory } from '@forestadmin/datasource-toolkit'; -import { ForestSchema } from '@forestadmin/forestadmin-client'; +import { ForestSchema, NotificationFromAgent } from '@forestadmin/forestadmin-client'; import cors from '@koa/cors'; import Router from '@koa/router'; import { readFile, writeFile } from 'fs/promises'; @@ -40,6 +40,10 @@ export default class Agent extends FrameworkMounter protected nocodeCustomizer: DataSourceCustomizer; protected customizationService: CustomizationService; + public publicServices?: { + sendNotifications: (payload: NotificationFromAgent) => Promise; + }; + /** * Create a new Agent Builder. * If any options are missing, the default will be applied: @@ -64,6 +68,17 @@ export default class Agent extends FrameworkMounter this.customizer = new DataSourceCustomizer({ ignoreMissingSchemaElementErrors: options.ignoreMissingSchemaElementErrors || false, }); + this.publicServices = { + sendNotifications: this.options.forestAdminClient.notifyFrontendService.notify.bind( + this.options.forestAdminClient.notifyFrontendService, + ), + }; + globalThis.publicServices = this.publicServices; + this.customizer.publicServices = { + sendNotifications: this.options.forestAdminClient.notifyFrontendService.notify.bind( + this.options.forestAdminClient.notifyFrontendService, + ), + }; this.customizationService = new CustomizationService(allOptions); } diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 1298d1b5dd..c5b28fa9bc 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -1,5 +1,5 @@ import { CompositeId, Logger, LoggerLevel } from '@forestadmin/datasource-toolkit'; -import { ForestAdminClient } from '@forestadmin/forestadmin-client'; +import { ForestAdminClient, NotificationFromAgent } from '@forestadmin/forestadmin-client'; import { IncomingMessage, ServerResponse } from 'http'; /** Options to configure behavior of an agent's forestadmin driver */ @@ -58,3 +58,9 @@ export type SelectionIds = { areExcluded: boolean; ids: CompositeId[]; }; + +declare global { + interface PublicServices { + sendNotifications: (payload: NotificationFromAgent) => Promise; + } +} diff --git a/packages/datasource-customizer/src/datasource-customizer.ts b/packages/datasource-customizer/src/datasource-customizer.ts index dd34a6a96f..059aaa0b6a 100644 --- a/packages/datasource-customizer/src/datasource-customizer.ts +++ b/packages/datasource-customizer/src/datasource-customizer.ts @@ -35,6 +35,9 @@ export type Options = { export default class DataSourceCustomizer { private readonly compositeDataSource: CompositeDatasource; private readonly stack: DecoratorsStack; + public publicServices?: { + sendNotifications: (payload: object) => Promise; + }; /** * Retrieve schema of the agent diff --git a/packages/forestadmin-client/src/build-application-services.ts b/packages/forestadmin-client/src/build-application-services.ts index 10847fa8e0..e7fafa2c87 100644 --- a/packages/forestadmin-client/src/build-application-services.ts +++ b/packages/forestadmin-client/src/build-application-services.ts @@ -1,6 +1,7 @@ import ChartHandler from './charts/chart-handler'; import EventsSubscriptionService from './events-subscription'; import NativeRefreshEventsHandlerService from './events-subscription/native-refresh-events-handler-service'; +import NotifyFrontendService from './events-subscription/notify-frontend-service'; import { RefreshEventsHandlerService } from './events-subscription/types'; import IpWhiteListService from './ip-whitelist'; import ModelCustomizationFromApiService from './model-customizations/model-customization-from-api'; @@ -34,6 +35,7 @@ export default function buildApplicationServices( modelCustomizationService: ModelCustomizationService; eventsSubscription: EventsSubscriptionService; eventsHandler: RefreshEventsHandlerService; + notifyFrontendService: NotifyFrontendService; } { const optionsWithDefaults = { forestServerUrl: 'https://api.forestadmin.com', @@ -72,6 +74,11 @@ export default function buildApplicationServices( const eventsSubscription = new EventsSubscriptionService(optionsWithDefaults, eventsHandler); + const notifyFrontendService = new NotifyFrontendService( + optionsWithDefaults, + forestAdminServerInterface, + ); + return { renderingPermission, optionsWithDefaults, @@ -79,6 +86,7 @@ export default function buildApplicationServices( contextVariables, eventsSubscription, eventsHandler, + notifyFrontendService, chartHandler: new ChartHandler(contextVariables), ipWhitelist: new IpWhiteListService(optionsWithDefaults), schema: new SchemaService(optionsWithDefaults), diff --git a/packages/forestadmin-client/src/events-subscription/notify-frontend-service.ts b/packages/forestadmin-client/src/events-subscription/notify-frontend-service.ts new file mode 100644 index 0000000000..86d0a1e3b0 --- /dev/null +++ b/packages/forestadmin-client/src/events-subscription/notify-frontend-service.ts @@ -0,0 +1,17 @@ +import { ForestHttpApi } from '..'; +import { NotificationFromAgent } from '../permissions/forest-http-api'; +import { ForestAdminClientOptionsWithDefaults } from '../types'; + +export default class NotifyFrontendService { + private readonly options: ForestAdminClientOptionsWithDefaults; + private readonly forestAdminServerInterface: ForestHttpApi; + + constructor(options, forestAdminServerInterface) { + this.options = options; + this.forestAdminServerInterface = forestAdminServerInterface; + } + + notify(payload: NotificationFromAgent) { + return this.forestAdminServerInterface.notifyFromAgent(this.options, payload); + } +} diff --git a/packages/forestadmin-client/src/forest-admin-client-with-cache.ts b/packages/forestadmin-client/src/forest-admin-client-with-cache.ts index ab4edb71a3..723367c671 100644 --- a/packages/forestadmin-client/src/forest-admin-client-with-cache.ts +++ b/packages/forestadmin-client/src/forest-admin-client-with-cache.ts @@ -1,4 +1,5 @@ import ChartHandler from './charts/chart-handler'; +import NotifyFrontendService from './events-subscription/notify-frontend-service'; import { BaseEventsSubscriptionService, RefreshEventsHandlerService, @@ -32,6 +33,7 @@ export default class ForestAdminClientWithCache implements ForestAdminClient { public readonly modelCustomizationService: ModelCustomizationService, protected readonly eventsSubscription: BaseEventsSubscriptionService, protected readonly eventsHandler: RefreshEventsHandlerService, + public readonly notifyFrontendService: NotifyFrontendService, ) {} verifySignedActionParameters(signedParameters: string): TSignedParameters { diff --git a/packages/forestadmin-client/src/index.ts b/packages/forestadmin-client/src/index.ts index 2aa634758e..d0edba056e 100644 --- a/packages/forestadmin-client/src/index.ts +++ b/packages/forestadmin-client/src/index.ts @@ -20,6 +20,7 @@ export { ForestAdminServerInterface, } from './types'; export { IpWhitelistConfiguration } from './ip-whitelist/types'; +export { NotificationFromAgent } from './permissions/forest-http-api'; // These types are used for the agent-generator package export { @@ -45,6 +46,7 @@ export default function createForestAdminClient( modelCustomizationService, eventsSubscription, eventsHandler, + notifyFrontendService, } = buildApplicationServices(new ForestHttpApi(), options); return new ForestAdminClientWithCache( @@ -59,6 +61,7 @@ export default function createForestAdminClient( modelCustomizationService, eventsSubscription, eventsHandler, + notifyFrontendService, ); } diff --git a/packages/forestadmin-client/src/permissions/forest-http-api.ts b/packages/forestadmin-client/src/permissions/forest-http-api.ts index 79f1b7cc00..8dd011b43f 100644 --- a/packages/forestadmin-client/src/permissions/forest-http-api.ts +++ b/packages/forestadmin-client/src/permissions/forest-http-api.ts @@ -9,6 +9,13 @@ import { } from '../types'; import ServerUtils from '../utils/server'; +export type NotificationFromAgent = { + notification: + | { refresh: { collectionName: string; recordIds?: string[] } } + | { message: { type: 'success' | 'info' | 'warning' | 'error'; text: string } }; + target?: { users?: string[]; team?: string; roles?: string[] }; +}; + export type HttpOptions = Pick< ForestAdminClientOptionsWithDefaults, 'envSecret' | 'forestServerUrl' @@ -37,4 +44,8 @@ export default class ForestHttpApi implements ForestAdminServerInterface { makeAuthService(options: Required): ForestAdminAuthServiceInterface { return new AuthService(options); } + + async notifyFromAgent(options: HttpOptions, payload: NotificationFromAgent) { + await ServerUtils.query(options, 'post', `/liana/notifications-from-agent`, {}, payload); + } } diff --git a/packages/forestadmin-client/src/types.ts b/packages/forestadmin-client/src/types.ts index bc30063b4b..d18226a4ea 100644 --- a/packages/forestadmin-client/src/types.ts +++ b/packages/forestadmin-client/src/types.ts @@ -4,6 +4,7 @@ import type { Chart, QueryChart } from './charts/types'; import { ParsedUrlQuery } from 'querystring'; import { Tokens, UserInfo } from './auth/types'; +import NotifyFrontendService from './events-subscription/notify-frontend-service'; import { IpWhitelistConfiguration } from './ip-whitelist/types'; import { ModelCustomization, ModelCustomizationService } from './model-customizations/types'; import { HttpOptions } from './permissions/forest-http-api'; @@ -22,6 +23,7 @@ export type { CollectionActionEvent, RawTree, RawTreeWithSources } from './permi export type LoggerLevel = 'Debug' | 'Info' | 'Warn' | 'Error'; export type Logger = (level: LoggerLevel, message: unknown) => void; +export { NotificationFromAgent } from './permissions/forest-http-api'; export type ForestAdminClientOptions = { envSecret: string; @@ -50,6 +52,7 @@ export interface ForestAdminClient { readonly chartHandler: ChartHandlerInterface; readonly modelCustomizationService: ModelCustomizationService; readonly authService: ForestAdminAuthServiceInterface; + readonly notifyFrontendService: NotifyFrontendService; verifySignedActionParameters(signedParameters: string): TSignedParameters; @@ -161,4 +164,5 @@ export interface ForestAdminServerInterface { getRenderingPermissions: (renderingId: number, ...args) => Promise; getModelCustomizations: (options: HttpOptions) => Promise; makeAuthService(options: ForestAdminClientOptionsWithDefaults): ForestAdminAuthServiceInterface; + notifyFromAgent: (...args) => Promise; }