diff --git a/docs/guides/server-integration.md b/docs/guides/server-integration.md index 7041a90b..706020c1 100644 --- a/docs/guides/server-integration.md +++ b/docs/guides/server-integration.md @@ -5,22 +5,9 @@ sidebar_label: "Server-Side Integration" # Server-Side Integration -The recommended approach to connect dhtmlxScheduler to a backend is to implement RESTful API on the server and use dhtmlxDataProcessor on the client. +The recommended approach of connecting dhtmlxScheduler to a backend is to implement a RESTful API on the server and use the module on the client. -[DataProcessor](https://docs.dhtmlx.com/dataprocessor__index.html) is a client-side library included into dhtmlxScheduler.js. -It monitors data changes and gets the server requests on the client side. - -It's possible to bind dhtmlxScheduler to the server side using REST API together with different frameworks and programming languages. -Here's a list of available server side implementations that you can use for Scheduler backend integration: - -- [dhtmlxScheduler with PHP:Slim](integrations/php/howtostart-php-slim4.md) -- [dhtmlxScheduler with ASP.NET Core](integrations/dotnet/howtostart-dotnet-core.md) -- [dhtmlxScheduler with ASP.NET MVC](integrations/dotnet/howtostart-dotnet.md) -- [dhtmlxScheduler with Node.js](integrations/node/howtostart-nodejs.md) -- [dhtmlxScheduler with PHP](integrations/php/howtostart-plain-php.md) -- [dhtmlxScheduler with PHP:Laravel](integrations/php/howtostart-php-laravel.md) -- [dhtmlxScheduler with Ruby on Rails](integrations/other/howtostart-ruby.md) -- [dhtmlxScheduler with PHP:Slim 3](integrations/other/howtostart-php.md) +DataProcessor is a built-in module that monitors data changes in Scheduler and sends updates to the REST API in the specified format, allowing easy [integration with server-side platforms](integrations/howtostart-guides.md). When using an object data source, DataProcessor can be configured to provide callbacks for data changes, which you can utilize for data binding. ## Technique @@ -28,33 +15,40 @@ Generally, to load data from the server side using REST API, you need to: ### Client side -1) Call the [load](api/method/load.md) method, where as a parameter specify the URL that returns Scheduler data in the [JSON](guides/data-formats.md#json) format +1. Call the [`load()`](api/method/load.md) method and specify the URL that returns Scheduler data in the [JSON](guides/data-formats.md#json) format. -2) Call the [createDataProcessor](api/method/createdataprocessor.md) method and pass an object with configuration options as its parameter: +2. Create a DataProcessor instance using one of the two ways: -~~~js -const dp = scheduler.createDataProcessor({ - url: "apiUrl", - mode: "REST" -}); -~~~ +- Initialize DataProcessor and attach it to the dhtmlxScheduler object: -Alternatively, create a dataProcessor using the constructor and attach it to the dhtmlxScheduler object. The scheduler.DataProcessor() constructor accepts the path to the same server-side script: - ~~~js scheduler.init("scheduler_here", new Date(), "month"); scheduler.load("apiUrl"); const dp = new scheduler.DataProcessor("apiUrl"); dp.init(scheduler); +dp.setTransactionMode("REST"); +~~~ + +:::note +It is recommended to use the second method. +::: + +- Call the [`createDataProcessor()`](api/method/createdataprocessor.md) method and pass an object with configuration options as its parameter: + +~~~js +const dp = scheduler.createDataProcessor({ + url: "apiUrl", + mode: "REST" +}); ~~~ -Check the detailed info in the next section. +Check the detailed information in the next section.

Creating DataProcessor

-While creating a DataProcessor via the API method [createDataProcessor](api/method/createdataprocessor.md) you have several possible options for passing parameters. +While creating a DataProcessor via the [`createDataProcessor()`](api/method/createdataprocessor.md) API method, you have several possible options for passing parameters. 1. Use one of the predefined request modes, as in: @@ -67,10 +61,10 @@ const dp = scheduler.createDataProcessor({ where: -- **url** - the URL to the server side -- **mode** - the mode of sending data to the server: "JSON" | "REST-JSON" | "JSON" | "POST" | "GET" +- `url` - the server-side URL +- `mode` - the mode of sending data to the server: "REST" | "REST-JSON" | "JSON" | "POST" | "GET" -2. Provide a custom **router** object: +2. Provide a custom `router` object: ~~~js const dp = scheduler.createDataProcessor(router); @@ -85,53 +79,52 @@ const server = "/api"; // action - "create"|"update"|"delete" // data - an object with event data // id - the id of a processed object (event) -const dp = scheduler.createDataProcessor(function(entity, action, data, id) { - switch(action) { +const dp = scheduler.createDataProcessor((entity, action, data, id) => { + switch (action) { case "create": - return scheduler.ajax.post( + return scheduler.ajax.post( `${server}/${entity}`, data - ); - break; + ); case "update": - return scheduler.ajax.put( + return scheduler.ajax.put( `${server}/${entity}/${id}`, data ); - break; case "delete": - return scheduler.ajax.del( + return scheduler.ajax.del( `${server}/${entity}/${id}` - ); - break; - } + ); + default: + return Promise.resolve(); + } }); ~~~ or an object of the following structure: ~~~js -const dp = scheduler.createDataProcessor({ - event: { - create: function(data) {}, - update: function(data, id) {}, - delete: function(id) {} - } +const dp = scheduler.createDataProcessor({ + event: { + create(data) {}, + update(data, id) {}, + delete(id) {} + } }); ~~~ -All the functions of the router object should return either a Promise or a data response object. This is needed for the dataProcessor to apply the database id and to hook the **onAfterUpdate** event of the data processor. +All the functions of the router object should return either a Promise or a data response object. This is needed for the dataProcessor to apply the database id and to hook the `onAfterUpdate` event of the dataProcessor. ~~~js -const router = function(entity, action, data, id) { - return new Promise(function(resolve, reject) { +const router = (entity, action, data, id) => { + return new Promise((resolve, reject) => { // … some logic - return resolve({tid: databaseId}); + return resolve({ tid: databaseId }); }); -} +}; ~~~ -Thus you can use DataProcessor for saving data in localStorage, or any other storage which is not linked to a certain URL, or in case if there are two different servers (URLs) responsible for creation and deletion of objects. +Thus you can use DataProcessor for saving data in `localStorage` or any other storage that is not linked to a certain URL, or in case there are two different servers (URLs) responsible for creation and deletion of objects.

Request and response details

@@ -139,12 +132,12 @@ The URL is formed by the following rule: - api/eventId -where "api" is the url you've specified in the dataProcessor configuration. +where "api" is the URL you've specified in the dataProcessor configuration. #### REST mode -To enable the REST mode, set the `mode` property of the [createDataProcessor](api/method/createdataprocessor.md) configuration object to the "REST" value: +To enable the REST mode, set the `mode` property of the [`createDataProcessor()`](api/method/createdataprocessor.md) configuration object to the "REST" value: ~~~js const dp = scheduler.createDataProcessor({ @@ -187,13 +180,12 @@ The list of possible requests and responses is: #### REST-JSON mode -To use the REST-JSON mode, set the `mode` property of the [createDataProcessor](api/method/createdataprocessor.md) configuration object to the "REST-JSON" value: +To use the REST-JSON mode, set the `mode` property of the [`createDataProcessor()`](api/method/createdataprocessor.md) configuration object to the "REST-JSON" value: ~~~js const dp = scheduler.createDataProcessor({ url: "apiUrl", mode: "REST-JSON" }); - ~~~ In this mode, the scheduler will send POST/PUT/DELETE requests using the `application/json` content type. @@ -215,14 +207,14 @@ The list of possible requests and responses is: add a new event POST /apiUrl - ( "start_date":"2019-12-18 00:00", "end_date":"2019-12-18 00:05", "text":"New event", ... ) + ( "start_date":"2027-12-18 00:00", "end_date":"2027-12-18 00:05", "text":"New event", ... ) ( "action":"inserted", "tid":"eventId" ) update an event PUT /apiUrl/:id - ( "start_date":"2024-12-18 00:00", "end_date":"2024-12-18 00:05", "text":"New event", ... ) + ( "start_date":"2027-12-18 00:00", "end_date":"2027-12-18 00:05", "text":"New event", ... ) ("action":"updated") @@ -236,7 +228,7 @@ The list of possible requests and responses is: #### POST mode -To enable the POST mode, set the `mode` property of the [createDataProcessor](api/method/createdataprocessor.md) configuration object to the "POST" value: +To enable the POST mode, set the `mode` property of the [`createDataProcessor()`](api/method/createdataprocessor.md) configuration object to the "POST" value: ~~~js const dp = scheduler.createDataProcessor({ @@ -269,7 +261,7 @@ The list of possible requests and responses is: #### JSON mode -To enable the JSON mode, set the `mode` property of the [createDataProcessor](api/method/createdataprocessor.md) configuration object to the "JSON" value: +To enable the JSON mode, set the `mode` property of the [`createDataProcessor()`](api/method/createdataprocessor.md) configuration object to the "JSON" value: ~~~js const dp = scheduler.createDataProcessor({ @@ -295,19 +287,19 @@ The list of possible requests and responses is: add a new event POST - ( "id": temporaryId, "action":"inserted", "data":( "start_date":"2019-12-18 00:00", "end_date":"2019-12-18 00:05", "text":"New event", ... ) ) + ( "id": temporaryId, "action":"inserted", "data":( "start_date":"2027-12-18 00:00", "end_date":"2027-12-18 00:05", "text":"New event", ... ) ) ( "action":"inserted", "tid":"eventId" ) update an event POST - ( "id": id, "action":"updated", "data":( "start_date":"2019-12-18 00:00", "end_date":"2019-12-18 00:05", "text":"New event", ... ) ) + ( "id": id, "action":"updated", "data":( "start_date":"2027-12-18 00:00", "end_date":"2027-12-18 00:05", "text":"New event", ... ) ) ("action":"updated") delete an event POST - ( "id": id, "action":"deleted", "data":( "start_date":"2019-12-18 00:00", "end_date":"2019-12-18 00:05", "text":"New event", ... ) ) + ( "id": id, "action":"deleted", "data":( "start_date":"2027-12-18 00:00", "end_date":"2027-12-18 00:05", "text":"New event", ... ) ) ("action":"deleted") @@ -333,15 +325,15 @@ The request and response for dynamic loading are the following: Create/Update/Delete requests will contain all public properties of a client-side event object: -- **id**: 71 -- **start_date**: 2024-11-04 15:00 -- **end_date**: 2024-11-04 18:00 -- **text**: Recinto Ferial - Valencia -- **details**: Details for Recinto Ferial - Valencia -- **!nativeeditor_status**: updated +- `id`: 71 +- `start_date`: 2027-11-04 15:00 +- `end_date`: 2027-11-04 18:00 +- `text`: Recinto Ferial - Valencia +- `details`: Details for Recinto Ferial - Valencia +- `!nativeeditor_status`: updated :::note -The **!nativeeditor_status** parameter is relevant for the POST mode only. +The `!nativeeditor_status` parameter is relevant for the POST mode only. ::: ### Server side @@ -352,54 +344,49 @@ Each request contains all the data needed to save changes in the database. As we initialized dataProcessor in the REST mode, it will use different HTTP verbs for each type of operation. :::note -If REST API isn't a good fit, use a [custom router](#custom-routing) with `createDataProcessor()` or handle saving manually (see [Saving Changes without DataProcessor](#saving-changes-without-dataprocessor)). +If a REST API isn't a good fit, use a [custom router](#custom-routing) with `createDataProcessor()` or handle saving manually (see [Saving Changes without DataProcessor](#saving-changes-without-dataprocessor)). ::: ## Recurring events -Recurring events are stored in the database as records that contain both all [fields of a regular event](guides/loading-data.md#data-properties) and several additional fields: **rrule**, **duration**, **recurring_event_id**, **original_start**, **deleted**. +Recurring events are stored in the database as records that contain both all [fields of a regular event](guides/loading-data.md#data-properties) and several additional fields: `rrule`, `duration`, `recurring_event_id`, `original_start`, `deleted`. Read more in the [Recurring Events](guides/recurring-events.md#server-side-logic) article. In addition to extra fields, a specific logic needs to be added to the server-side controller: -- for the **insert** action: - - if **event.deleted === true**, the response must have the 'deleted' status -- for the **update** action: - - if **event.rrule** is not empty and **event.recurring_event_id** is empty, all events where **recurring_event_id == event.id** must be deleted -- for the **delete** action: - - if **event.rrule** is not empty and **event.recurring_event_id** is empty, all events where **recurring_event_id == event.id** must be deleted - - if **event.recurring_event_id** is not empty, the event should be updated with **event.deleted = true** instead of deleting. +- For the `insert` action, if `event.deleted === true`, the response must have the "deleted" status. +- For the `update` action, if `event.rrule` is not empty and `event.recurring_event_id` is empty, all events where `recurring_event_id == event.id` must be deleted. +- For the `delete` action, if `event.rrule` is not empty and `event.recurring_event_id` is empty, all events where `recurring_event_id == event.id` must be deleted. +- For the `delete` action, if `event.recurring_event_id` is not empty, the event should be updated with `event.deleted = true` instead of deleting. :::note You can have a look at the detailed example on editing and deleting recurring events in the [related section of the Recurring Events article](guides/recurring-events.md#editingdeleting-a-certain-occurrence-in-the-series). ::: -## Custom Request Headers and Parameters +## Custom Request Headers and Parameters ### Adding custom request headers -When you need Scheduler to send additional headers to your backend, you can specify them using the [dataProcessor.setTransactionMode](https://docs.dhtmlx.com/api__dataprocessor_settransactionmode.html) method. +When you need Scheduler to send additional headers to your backend, you can specify them using the `dataProcessor.setTransactionMode()` method. For example, let's suppose that you need to add an authorization token to your requests: ~~~js scheduler.init("scheduler_here"); scheduler.load("/api"); - + const dp = scheduler.createDataProcessor("/api"); -dp.init(scheduler); dp.setTransactionMode({ - mode:"REST", + mode: "REST", headers: { - "Authorization": "Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" + "Authorization": "Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" } }); ~~~ -Currently, [load](api/method/load.md) does not support header/payload parameters, so if you need them for GET request, -you'll have to send xhr manually and load data into scheduler using [parse](api/method/parse.md), for example: +Currently, [`load()`](api/method/load.md) does not support header/payload parameters, so if you need them for a GET request, you'll have to send XHR manually and load data into Scheduler using [`parse()`](api/method/parse.md), for example: ~~~js const authToken = '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b'; @@ -408,14 +395,14 @@ fetch("/api", { method: "GET", headers: { "Content-Type": "application/json", - "Authorization": `Token ${authToken}` + "Authorization": `Token ${authToken}` } }) -.then(response => response.json()) +.then((response) => response.json()) .then(data => { scheduler.parse(data); }) -.catch(error => { +.catch((error) => { console.error("Error:", error); }); ~~~ @@ -424,42 +411,40 @@ fetch("/api", { There are several ways to send additional parameters to requests. -As you know, scheduler sends all properties of the data object back to the backend. +As you know, Scheduler sends all properties of the data object back to the backend. Thus, you can add an extra property directly to the *data object* and it will be sent to the backend: ~~~js -scheduler.attachEvent("onEventCreated", function(id,e){ - const event = scheduler.getEvent(id); - event.userId = currentUser; +scheduler.attachEvent("onEventCreated", (id, event) => { + const createdEvent = scheduler.getEvent(id); + createdEvent.userId = currentUser; return true; }); ~~~ -Alternatively, you can add custom parameters to all requests sent by data processor, using the **payload** property of -the [setTransactionMode](https://docs.dhtmlx.com/api__dataprocessor_settransactionmode.html) parameter: +Alternatively, you can add custom parameters to all requests sent by the dataProcessor using the `payload` property of the `setTransactionMode()` parameter: ~~~js -scheduler.init("gantt_here"); +scheduler.init("scheduler_here"); scheduler.load("/api"); - + const dp = scheduler.createDataProcessor("/api"); -dp.init(scheduler); dp.setTransactionMode({ - mode:"REST", + mode: "REST", payload: { - token: "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" + token: "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" } }); ~~~ Payload will be added into the query string of the request. -One more way to add custom parameters to a request is to use the [onBeforeUpdate](https://docs.dhtmlx.com/api__dataprocessor_onbeforeupdate_event.html) event of DataProcessor: +One more way to add custom parameters to a request is to use the `onBeforeUpdate` event of DataProcessor: ~~~js const dp = scheduler.createDataProcessor("/api/events"); -dp.attachEvent("onBeforeUpdate", function(id, state, data){ +dp.attachEvent("onBeforeUpdate", (id, state, data) => { data.productName = "Product 2"; return true; }); @@ -476,56 +461,56 @@ The event is called for each record sent to the backend and a custom parameter w If you have dataProcessor initialized, any change made by the user or programmatically will be automatically saved in the data source. -Generally, to update a specific event programmatically, use the [addEvent](api/method/addevent.md) method: +Generally, to update a specific event programmatically, use the [`addEvent()`](api/method/addevent.md) method: ~~~js scheduler.parse([ - { id:1, start_date:"2017-05-13 6:00", end_date:"2017-05-13 8:00", text:"Event 1"}, - { id:2, start_date:"2017-06-09 6:00", end_date:"2017-06-09 8:00", text:"Event 2"} + { id: 1, start_date: "2027-05-13 6:00", end_date: "2027-05-13 8:00", text: "Event 1" }, + { id: 2, start_date: "2027-06-09 6:00", end_date: "2027-06-09 8:00", text: "Event 2" } ]); const event = scheduler.getEvent(1); -event.text = "Conference"; //changes event's data +event.text = "Conference"; // changes event's data scheduler.addEvent(event); // renders the updated event ~~~ -When called for an event that is already loaded into the scheduler, [addEvent](api/method/addevent.md) will trigger an *update* request, otherwise *insert* will be called. +When called for an event that is already loaded into the scheduler, [`addEvent()`](api/method/addevent.md) will trigger an *update* request. Otherwise, *insert* will be called. The methods that invoke sending an update to the backend: -- [addEvent](api/method/addevent.md) -- [deleteEvent](api/method/deleteevent.md) +- [`addEvent()`](api/method/addevent.md) +- [`deleteEvent()`](api/method/deleteevent.md) ## Saving Changes without DataProcessor -dhtmlxScheduler can be used without gantt.dataProcessor. In that case you'll have to monitor all changes made in the scheduler manually and then send them to your backend. +dhtmlxScheduler can be used without DataProcessor. In that case you'll have to monitor all changes made in the scheduler manually and then send them to your backend. Here is the list of events you'll need to listen to: -- [onEventAdded](api/event/oneventadded.md) -- [onEventChanged](api/event/oneventchanged.md) -- [onEventDeleted](api/event/oneventdeleted.md) +- [`onEventAdded`](api/event/oneventadded.md) +- [`onEventChanged`](api/event/oneventchanged.md) +- [`onEventDeleted`](api/event/oneventdeleted.md) When an event is created on the client side, it obtains a temporary id which is used until the item gets a permanent database id. -Once you insert a new item into the database, you'll need to pass it back to the client side and apply it to the related event using the [changeEventId](api/method/changeeventid.md) method: +Once you insert a new item into the database, you'll need to pass it back to the client side and apply it to the related event using the [`changeEventId()`](api/method/changeeventid.md) method: ~~~js // assume that eventService is some kind of CRUD service implementation -scheduler.attachEvent('onEventAdded', function(id, event) { - eventService.create(event) - .then(function(result){ - scheduler.changeEventId(id, result.databaseId); - }); +scheduler.attachEvent('onEventAdded', (id, event) => { + eventService.create(event) + .then((result) => { + scheduler.changeEventId(id, result.databaseId); + }); }); -scheduler.attachEvent('onEventChanged', function(id, event) { - eventService.update(event); +scheduler.attachEvent('onEventChanged', (id, event) => { + eventService.update(event); }); -scheduler.attachEvent('onEventDeleted', function(id) { - eventService.delete(id); +scheduler.attachEvent('onEventDeleted', (id) => { + eventService.delete(id); }); ~~~ @@ -535,55 +520,53 @@ In case RESTful AJAX API isn't what you need on the backend, or if you want to m For example, if you use Angular, React, or any other framework where a component on a page doesn't send changes directly to the server, but passes them to a different component which is responsible for data saving. -To provide custom routing options for DataProcessor, you should use the [**createDataProcessor()**](api/method/createdataprocessor.md) method: +To provide custom routing options for DataProcessor, you should use the [`createDataProcessor()`](api/method/createdataprocessor.md) method: ~~~js const server = "/api"; -scheduler.createDataProcessor(function(entity, action, data, id) { - switch(action) { +scheduler.createDataProcessor((entity, action, data, id) => { + switch (action) { case "create": return scheduler.ajax.post( `${server}/${entity}`, data ); - break; case "update": return scheduler.ajax.put( `${server}/${entity}/${id}`, data ); - break; case "delete": return scheduler.ajax.del( `${server}/${entity}/${id}` - ); - break; + ); + default: + return Promise.resolve(); } }); ~~~ ### Using AJAX for setting custom routers -[Scheduler AJAX module](api/other/ajax.md) can be useful for setting custom routes. Scheduler expects a custom router to return a Promise object as a result of an operation, which allows catching the end of an action. -The AJAX module supports promises and is suitable for usage inside of custom routers. Scheduler will get Promise and process the content of Promise, when it is resolved. +[Scheduler AJAX module](api/other/ajax.md) can be useful for setting custom routes. Scheduler expects a custom router to return a Promise object as a result of an operation, which allows catching the end of an action. +The AJAX module supports promises and is suitable for use inside custom routers. Scheduler will get the Promise and process its content when it is resolved. -In the example below a new task is created. If the server response includes the id of a newly created task, Scheduler will be able to apply it. +In the example below a new event is created. If the server response includes the id of a newly created event, Scheduler will be able to apply it. ~~~js -scheduler.createDataProcessor(function(entity, action, data, id){ +scheduler.createDataProcessor((entity, action, data, id) => { ... - + switch (action) { case "create": return scheduler.ajax.post({ - headers: { - "Content-Type": "application/json" + headers: { + "Content-Type": "application/json" }, - url: server + "/" + entity + "/" + id, + url: `${server}/${entity}/${id}`, data: JSON.stringify(data) }); - break; } }); ~~~ @@ -600,37 +583,36 @@ Such a response can be captured on the client with the help of dataProcessor: ~~~js const dp = scheduler.createDataProcessor("apiUrl"); -dp.init(scheduler); -dp.attachEvent("onAfterUpdate", function(id, action, tid, response){ - if(action === "error"){ +dp.attachEvent("onAfterUpdate", (id, action, tid, response) => { + if (action === "error") { // do something here } }); ~~~ -The response object may contain any number of additional properties, they can be accessed via the `response` argument of the onAfterUpdate handler. +The response object may contain any number of additional properties. They can be accessed via the `response` argument of the `onAfterUpdate` handler. If the server responded with an error on some of your action but the changes were saved on the client side, the best way to synchronize their states is to clear the client's state, and reload the correct data from the server side: ~~~js -dp.attachEvent("onAfterUpdate", function(id, action, tid, response){ - if(action === "error"){ +dp.attachEvent("onAfterUpdate", (id, action, tid, response) => { + if (action === "error") { scheduler.clearAll(); scheduler.load(url); } }); ~~~ -In cases when you don't want to fully reload the data, you can delete a single event from only the client-side using the **silent** parameter of the [deleteEvent](api/method/deleteevent.md) method: +In cases when you don't want to fully reload the data, you can delete a single event from only the client side using the `silent` parameter of the [`deleteEvent()`](api/method/deleteevent.md) method: ~~~js // removes the specified event only from the client-side, without server calls -scheduler.deleteEvent(id, true); +scheduler.deleteEvent(id, true); ~~~ ## XSS, CSRF and SQL Injection Attacks -Pay attention that Scheduler doesn't provide any means of preventing an application from various threats, such as SQL injections or XSS and CSRF attacks. -It is important that responsibility for keeping an application safe is on the developers implementing the backend. +Pay attention that Scheduler doesn't provide any means of preventing an application from various threats, such as SQL injections or XSS and CSRF attacks. +It is important that responsibility for keeping an application safe is on the developers implementing the backend. -Check the [Application Security](guides/app-security.md) article to learn the most vulnerable points of the component and the measures you can take to improve the safety of your application. +Check the [Application Security](guides/app-security.md) article to learn the most vulnerable points of the component and the measures you can take to improve the safety of your application.