├── .gitignore
├── client
└── index.html
├── readme.md
├── server-dotnetcore
├── Comparators
│ └── MonthComparator.cs
├── Controllers
│ └── CubeController.cs
├── DataLoaders
│ ├── DataLoader.cs
│ ├── DatabaseConnectionFactory.cs
│ ├── IDataLoader.cs
│ └── ParserFactory.cs
├── DataStorages
│ ├── DataStorage.cs
│ ├── DataStorageOptions.cs
│ └── IDataStorage.cs
├── DatasourceOptions.cs
├── Extensions
│ └── DateTimeExtension.cs
├── IPrepopulatingService.cs
├── JsonConverters
│ ├── ColumnTypeJsonConverter.cs
│ ├── DataJsonConverter.cs
│ ├── MembersResponseJsonConverter.cs
│ ├── SelectResponseJsonConverter.cs
│ └── ValuesJsonConverter.cs
├── Models
│ ├── DataModels
│ │ ├── ColumnListDataStructure.cs
│ │ ├── DataColumn.cs
│ │ ├── DataSliceModel.cs
│ │ └── IDataStructure.cs
│ ├── FieldsRequest
│ │ ├── FieldModel.cs
│ │ ├── FieldsRequestModel.cs
│ │ ├── SchemaAggregation.cs
│ │ ├── SchemaFilter.cs
│ │ ├── SchemaFilterElement.cs
│ │ └── SchemaModel.cs
│ ├── HandshakeRequest
│ │ └── HandshakeRequst.cs
│ ├── MembersRequest
│ │ ├── MembersRequestModel.cs
│ │ └── MembersResponseModel.cs
│ ├── SelectRequest
│ │ ├── AggregationBy.cs
│ │ ├── AggregationModel.cs
│ │ ├── AggregationRequestModel.cs
│ │ ├── FieldFuncValue.cs
│ │ ├── FilterModel.cs
│ │ ├── HierarchyObject.cs
│ │ ├── QueryModel.cs
│ │ ├── SelectRequestModel.cs
│ │ └── SelectResponceModel.cs
│ └── Values
│ │ └── ValueModel.cs
├── NetCoreServer.csproj
├── Parsers
│ ├── CSVParser.cs
│ ├── CSVSerializerOptions.cs
│ ├── DatabaseParser.cs
│ ├── IParser.cs
│ └── JSONParser.cs
├── PrepopulatingCacheService.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── SettingsConfigurationHelper.cs
├── Startup.cs
├── appsettings.json
└── data
│ ├── data-types.csv
│ ├── data-types.json
│ ├── data.csv
│ ├── fm-product-sales.csv
│ └── fm-product-sales.json
├── server-nodejs
├── api
│ └── cube.js
├── data
│ ├── data-types.json
│ ├── data.json
│ └── fm-product-sales.json
├── package.json
└── server.js
└── tests
├── config.json
├── package-lock.json
├── package.json
└── test
├── FieldsRequestSpec.js
├── HandshakeRequestSpec.js
├── MembersRequestSpec.js
├── SelectRequestDrillThroughSpec.js
├── SelectRequestFlatSpec.js
└── SelectRequestSpec.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /server-nodejs/node_modules/
2 | /server-nodejs/package-lock.json
3 | /server-dotnetcore/build/
4 | /server-dotnetcore/[Bb]in/
5 | /server-dotnetcore/[Oo]bj/
6 | /server-dotnetcore/project.lock.json
7 | /tests/node_modules
8 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Flexmonster Pivot Table & Charts
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Flexmonster custom data source API server
2 | [](https://www.flexmonster.com?r=github)
3 | Website: [www.flexmonster.com](https://www.flexmonster.com?r=github)
4 |
5 | ## Flexmonster Pivot Table & Charts
6 |
7 | Flexmonster Pivot is a powerful JavaScript tool for interactive web reporting. It allows you to visualize and analyze data from JSON, CSV, SQL, NoSQL, Elasticsearch, and OLAP data sources quickly and conveniently. Flexmonster is designed to integrate seamlessly with any client-side framework and can be easily embedded into your application.
8 |
9 | This repository holds the source code for sample servers that demonstrate how to implement the [custom data source API](https://www.flexmonster.com/doc/introduction-to-custom-data-source-api?r=github).
10 |
11 | The custom data source API is a communication protocol designed to retrieve aggregated data from a server to Flexmonster Pivot.
12 | The server is responsible for fetching, processing, and aggregating data, which is then passed to Flexmonster in a ready-to-show format.
13 |
14 | Table of contents:
15 | - [Prerequisites](#prerequisites)
16 | - [Installation](#installation)
17 | - [Tests](#tests)
18 | - [Related Flexmonster docs](#related-flexmonster-docs)
19 |
20 | ## Prerequisites
21 |
22 | For the sample Node.js server:
23 | - [Node.js 10 or later](https://nodejs.org/en/)
24 |
25 | For the sample .NET Core server:
26 | - [.NET Core 6.0](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
27 |
28 | ## Installation
29 |
30 | 1. Download a `.zip` archive with the sample project or clone it from GitHub with the following command:
31 |
32 | ```bash
33 | git clone https://github.com/flexmonster/api-data-source.git && cd api-data-source
34 | ```
35 |
36 | 2. Run the project in one of the following ways:
37 |
38 | - To start the sample Node.js server:
39 |
40 | ```
41 | cd server-nodejs
42 | npm install
43 | npm start
44 | ```
45 |
46 | - To start the sample .NET Core server:
47 |
48 | ```
49 | cd server-dotnetcore
50 | dotnet restore
51 | dotnet run
52 | ```
53 |
54 | ## Tests
55 |
56 | If needed, you can check a custom data source API server with our [test suite](https://github.com/flexmonster/api-data-source/tree/master/tests). Run the tests with the following commands:
57 |
58 | ```
59 | cd tests
60 | npm install
61 | npm test
62 | ```
63 |
64 | Note that these tests will work only if one of the sample servers is running. To learn how your server can be tested, [see our documentation](https://www.flexmonster.com/doc/test-custom-data-source-api-server?r=github).
65 |
66 | ## Related Flexmonster docs
67 |
68 | For details on usage, refer to our documentation:
69 |
70 | - [A quick overview of the sample Node.js server](https://www.flexmonster.com/doc/pivot-table-with-node-js-server?r=github)
71 | - [A quick overview of the sample .NET Core server](https://www.flexmonster.com/doc/pivot-table-with-dot-net-core-server?r=github)
72 | - [Implementing the custom data source API server](https://www.flexmonster.com/doc/implement-custom-data-source-api?r=github)
73 | - [Custom data source API documentation](https://www.flexmonster.com/api/all-requests?r=github)
74 |
--------------------------------------------------------------------------------
/server-dotnetcore/Comparators/MonthComparator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics.CodeAnalysis;
4 | using NetCoreServer.Models;
5 |
6 | namespace NetCoreServer.Comparators
7 | {
8 | ///
9 | /// Compare months by its name
10 | ///
11 | public class MonthComparator : Comparer
12 | {
13 | public override int Compare([AllowNull] T x, [AllowNull] T y)
14 | {
15 |
16 | if (x == null || y == null) return -1;
17 | if (Enum.TryParse(x.ToString(), out Month xInEnum))
18 | {
19 | if (Enum.TryParse(y.ToString(), out Month yInEnum))
20 | {
21 | return xInEnum.CompareTo(yInEnum);
22 | }
23 | }
24 | if (Enum.TryParse(x.ToString(), out ShortMonth xInShortEnum))
25 | {
26 | if (Enum.TryParse(y.ToString(), out ShortMonth yInShortEnum))
27 | {
28 | return xInShortEnum.CompareTo(yInShortEnum);
29 | }
30 | }
31 | return -1;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server-dotnetcore/Controllers/CubeController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Microsoft.Extensions.Caching.Memory;
3 | using NetCoreServer.Comparators;
4 | using NetCoreServer.DataStorages;
5 | using NetCoreServer.Extensions;
6 | using NetCoreServer.JsonConverters;
7 | using NetCoreServer.Models;
8 | using NetCoreServer.Models.DataModels;
9 | using NetCoreServer.Models.Fields;
10 | using NetCoreServer.Models.Handshake;
11 | using NetCoreServer.Models.Members;
12 | using NetCoreServer.Models.Select;
13 | using System;
14 | using System.Collections.Concurrent;
15 | using System.Collections.Generic;
16 | using System.IO;
17 | using System.Linq;
18 | using System.Security.Cryptography;
19 | using System.Text;
20 | using System.Text.Json;
21 | using System.Threading.Tasks;
22 |
23 | namespace NetCoreServer.Controllers
24 | {
25 | [ApiController]
26 | public class CubeController : ControllerBase
27 | {
28 | private const string API_VERSION = "2.9.0";
29 | private const int MEMBERS_PAGE_SIZE = 50000;
30 | private const int SELECT_PAGE_SIZE = 50000;
31 |
32 | private readonly IMemoryCache _cache;
33 |
34 | private readonly IDataStorage _dataStorage;
35 |
36 | public CubeController(IMemoryCache cache, IDataStorage dataStorage)
37 | {
38 | _cache = cache;
39 | _dataStorage = dataStorage;
40 | }
41 |
42 | ///
43 | /// Handshake requst
44 | ///
45 | ///
46 | ///
47 | [Route("/api/cube/handshake")]
48 | [HttpPost]
49 | public IActionResult Handshake([FromBody]HandshakeRequst request)
50 | {
51 | object response = null;
52 | if (request.Type == RequestType.Handshake)
53 | {
54 | response = new { version = API_VERSION };
55 | }
56 | return new JsonResult(response);
57 | }
58 |
59 | ///
60 | /// Fields requst
61 | ///
62 | /// requst
63 | ///
64 | [Route("/api/cube/fields")]
65 | [HttpPost]
66 | public async Task PostFields([FromBody]FieldsRequest request)
67 | {
68 | object response = null;
69 | if (request.Index == null)
70 | {
71 | Response.StatusCode = 400;
72 | return new JsonResult("Index property is missing.");
73 | }
74 | if (request.Type == RequestType.Fields)
75 | {
76 | try
77 | {
78 | response = await GetShema(request.Index);
79 | }
80 | catch (Exception e)
81 | {
82 | Console.WriteLine(e.StackTrace);
83 | Response.StatusCode = 500;
84 | return Content(e.Message);
85 | }
86 | }
87 | if (response == null)
88 | {
89 | Response.StatusCode = 400;
90 | return new JsonResult("Incorrect request for this endpoint.");
91 | }
92 | return new JsonResult(response, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = { new ColumnTypeJsonConverter() } });
93 | }
94 |
95 | ///
96 | /// Members request
97 | ///
98 | ///
99 | ///
100 | [Route("/api/cube/members")]
101 | [HttpPost]
102 | public async Task PostMembers([FromBody]MembersRequest request)
103 | {
104 | if (request.Index == null)
105 | {
106 | Response.StatusCode = 400;
107 | return new JsonResult("Index property is missing.");
108 | }
109 | if (request.Type == RequestType.Members)
110 | {
111 | try
112 | {
113 | var response = await GetMembers(request.Index, request.Page, request.Field);
114 | return Content(response, "application/json");
115 | }
116 | catch (Exception e)
117 | {
118 | Console.WriteLine(e.StackTrace);
119 | Response.StatusCode = 500;
120 | return new JsonResult(e.Message);
121 | }
122 | }
123 | Response.StatusCode = 400;
124 | return Content("Incorrect request for this endpoint.");
125 | }
126 |
127 | ///
128 | /// Select request
129 | ///
130 | /// requst
131 | ///
132 | [Route("/api/cube/select")]
133 | [HttpPost]
134 | public async Task PostSelect([FromBody]SelectRequest request)
135 | {
136 | string response = null;
137 |
138 | if (request.Index == null)
139 | {
140 | Response.StatusCode = 400;
141 | return new JsonResult("Index property is missing");
142 | }
143 | if (request.Type == RequestType.Select)
144 | {
145 | try
146 | {
147 | response = await SelectData(request.Index, request.Query, request.Page);
148 | }
149 | catch (Exception e)
150 | {
151 | Console.WriteLine(e.StackTrace);
152 | Response.StatusCode = 500;
153 | return Content("Server error");
154 | }
155 | }
156 | if (response == null)
157 | {
158 | Response.StatusCode = 400;
159 | return new JsonResult("Incorrect request for this endpoint.");
160 | }
161 | return Content(response);
162 | }
163 |
164 | ///
165 | /// Load schema and create object based on it
166 | ///
167 | /// index
168 | ///
169 | private async Task GetShema(string index)
170 | {
171 | return await _cache.GetOrCreateAsync(index + "schema",
172 | async (cacheEntry) =>
173 | {
174 | cacheEntry.SetSize(1);
175 | cacheEntry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(240);
176 | Schema schema = new Schema();
177 | schema.Sorted = false;
178 | schema.Aggregations.Any = new List { "count", "distinctcount" };
179 | schema.Aggregations.Date = new List { "count", "distinctcount", "min", "max" };
180 | schema.Aggregations.Number = new List { "sum", "average", "count", "distinctcount", "min", "max" };
181 | schema.Filters.Any.Members = true;
182 | schema.Filters.Any.Query = true;
183 | schema.Filters.Any.ValueQuery = true;
184 | var data = await LoadData(index);
185 | var firstElement = data.GetNameAndTypes();
186 | foreach (var elem in firstElement)
187 | {
188 | FieldModel field = new FieldModel();
189 | field.UniqueName = elem.Key;
190 | field.Type = elem.Value;
191 | schema.Fields.Add(field);
192 | }
193 | return schema;
194 | });
195 | }
196 |
197 | ///
198 | /// Load data from file {index}.json
199 | ///
200 | /// index
201 | ///
202 | private async Task LoadData(string index)
203 | {
204 | return await _dataStorage.GetOrAddAsync(index);
205 | }
206 |
207 | private async Task GetMembers(string index, int page, FieldModel field)
208 | {
209 | return (await _cache.GetOrCreateAsync(index + field.UniqueName,
210 | async (cacheEntry) =>
211 | {
212 | cacheEntry.SetSize(1);
213 | JsonSerializerOptions options = new JsonSerializerOptions { Converters = { new MembersResponseJsonConverter() } };
214 | IDataStructure data = await LoadData(index);
215 | var namesAndTypes = data.GetNameAndTypes();
216 | DataSlice dataSlice = new DataSlice(data);
217 | List