Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: trigger notification from agent #1189

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion packages/_example/src/forest/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export default function makeAgent() {
typingsPath: 'src/forest/typings.ts',
};

return createAgent<Schema>(envOptions)
const agent = createAgent<Schema>(envOptions);

return agent
.addDataSource(createSqlDataSource({ dialect: 'sqlite', storage: './assets/db.sqlite' }))

.addDataSource(
Expand Down Expand Up @@ -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)
Expand Down
109 changes: 108 additions & 1 deletion packages/_example/src/forest/customizations/card.ts
Original file line number Diff line number Diff line change
@@ -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 [email protected]', {
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: ['[email protected]'] },
};

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');
Expand Down Expand Up @@ -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: ['[email protected]'] },
};
await send(notif, resultBuilder);

return resultBuilder.success('Card escalated to back office');
},
});
17 changes: 16 additions & 1 deletion packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -40,6 +40,10 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter
protected nocodeCustomizer: DataSourceCustomizer<S>;
protected customizationService: CustomizationService;

public publicServices?: {
sendNotifications: (payload: NotificationFromAgent) => Promise<void>;
};

/**
* Create a new Agent Builder.
* If any options are missing, the default will be applied:
Expand All @@ -64,6 +68,17 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter
this.customizer = new DataSourceCustomizer<S>({
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);
}

Expand Down
8 changes: 7 additions & 1 deletion packages/agent/src/types.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -58,3 +58,9 @@ export type SelectionIds = {
areExcluded: boolean;
ids: CompositeId[];
};

declare global {
interface PublicServices {
sendNotifications: (payload: NotificationFromAgent) => Promise<void>;
}
}
3 changes: 3 additions & 0 deletions packages/datasource-customizer/src/datasource-customizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export type Options = {
export default class DataSourceCustomizer<S extends TSchema = TSchema> {
private readonly compositeDataSource: CompositeDatasource<Collection>;
private readonly stack: DecoratorsStack;
public publicServices?: {
sendNotifications: (payload: object) => Promise<void>;
};

/**
* Retrieve schema of the agent
Expand Down
8 changes: 8 additions & 0 deletions packages/forestadmin-client/src/build-application-services.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -34,6 +35,7 @@ export default function buildApplicationServices(
modelCustomizationService: ModelCustomizationService;
eventsSubscription: EventsSubscriptionService;
eventsHandler: RefreshEventsHandlerService;
notifyFrontendService: NotifyFrontendService;
} {
const optionsWithDefaults = {
forestServerUrl: 'https://api.forestadmin.com',
Expand Down Expand Up @@ -72,13 +74,19 @@ export default function buildApplicationServices(

const eventsSubscription = new EventsSubscriptionService(optionsWithDefaults, eventsHandler);

const notifyFrontendService = new NotifyFrontendService(
optionsWithDefaults,
forestAdminServerInterface,
);

return {
renderingPermission,
optionsWithDefaults,
permission,
contextVariables,
eventsSubscription,
eventsHandler,
notifyFrontendService,
chartHandler: new ChartHandler(contextVariables),
ipWhitelist: new IpWhiteListService(optionsWithDefaults),
schema: new SchemaService(optionsWithDefaults),
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ChartHandler from './charts/chart-handler';
import NotifyFrontendService from './events-subscription/notify-frontend-service';
import {
BaseEventsSubscriptionService,
RefreshEventsHandlerService,
Expand Down Expand Up @@ -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<TSignedParameters>(signedParameters: string): TSignedParameters {
Expand Down
3 changes: 3 additions & 0 deletions packages/forestadmin-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -45,6 +46,7 @@ export default function createForestAdminClient(
modelCustomizationService,
eventsSubscription,
eventsHandler,
notifyFrontendService,
} = buildApplicationServices(new ForestHttpApi(), options);

return new ForestAdminClientWithCache(
Expand All @@ -59,6 +61,7 @@ export default function createForestAdminClient(
modelCustomizationService,
eventsSubscription,
eventsHandler,
notifyFrontendService,
);
}

Expand Down
11 changes: 11 additions & 0 deletions packages/forestadmin-client/src/permissions/forest-http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -37,4 +44,8 @@ export default class ForestHttpApi implements ForestAdminServerInterface {
makeAuthService(options: Required<ForestAdminClientOptions>): ForestAdminAuthServiceInterface {
return new AuthService(options);
}

async notifyFromAgent(options: HttpOptions, payload: NotificationFromAgent) {
await ServerUtils.query(options, 'post', `/liana/notifications-from-agent`, {}, payload);
}
}
4 changes: 4 additions & 0 deletions packages/forestadmin-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -50,6 +52,7 @@ export interface ForestAdminClient {
readonly chartHandler: ChartHandlerInterface;
readonly modelCustomizationService: ModelCustomizationService;
readonly authService: ForestAdminAuthServiceInterface;
readonly notifyFrontendService: NotifyFrontendService;

verifySignedActionParameters<TSignedParameters>(signedParameters: string): TSignedParameters;

Expand Down Expand Up @@ -161,4 +164,5 @@ export interface ForestAdminServerInterface {
getRenderingPermissions: (renderingId: number, ...args) => Promise<RenderingPermissionV4>;
getModelCustomizations: (options: HttpOptions) => Promise<ModelCustomization[]>;
makeAuthService(options: ForestAdminClientOptionsWithDefaults): ForestAdminAuthServiceInterface;
notifyFromAgent: (...args) => Promise<void>;
}
Loading