ra-spring-data-provider 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/package.json +66 -0
- package/src/index.d.ts +29 -0
- package/src/index.ts +191 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# ra-spring-data-provider
|
|
2
|
+
|
|
3
|
+
Te [React Admin](https://marmelab.com/react-admin/) data provider of [ra-spring-json-server].
|
|
4
|
+
|
|
5
|
+
This package provides a data provider that follows JSON Server API conventions, specifically adapted for Spring Boot backends. It supports efficient bulk operations and is designed to work seamlessly with Spring Boot controllers implementing the `IRAController` interface from [ra-spring-json-server] library.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install ra-spring-data-provider
|
|
11
|
+
# or
|
|
12
|
+
yarn add ra-spring-data-provider
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
import { Admin, Resource } from "react-admin";
|
|
20
|
+
import raSpringDataProvider from "ra-spring-data-provider";
|
|
21
|
+
|
|
22
|
+
const dataProvider = jsonServerProvider("http://localhost:8080/api");
|
|
23
|
+
|
|
24
|
+
const App = () => (
|
|
25
|
+
<Admin dataProvider={dataProvider}>
|
|
26
|
+
<Resource name="users" list={ListGuesser} />
|
|
27
|
+
</Admin>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export default App;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API Mapping
|
|
34
|
+
|
|
35
|
+
This data provider uses the JSON Server API format to communicate with the backend. Your Spring Boot API should follow these conventions:
|
|
36
|
+
|
|
37
|
+
| React Admin Method | HTTP Method | URL Example |
|
|
38
|
+
| ------------------ | ----------- | ------------------------------------------------------------- |
|
|
39
|
+
| `getList` | `GET` | `http://api.url/users?_sort=name&_order=ASC&_start=0&_end=24` |
|
|
40
|
+
| `getOne` | `GET` | `http://api.url/users/123` |
|
|
41
|
+
| `getMany` | `GET` | `http://api.url/users?id=123&id=456` |
|
|
42
|
+
| `getManyReference` | `GET` | `http://api.url/users?authorId=345` |
|
|
43
|
+
| `create` | `POST` | `http://api.url/users` |
|
|
44
|
+
| `update` | `PUT` | `http://api.url/users/123` |
|
|
45
|
+
| `updateMany` | `PUT` | `http://api.url/users?id=123&id=456` |
|
|
46
|
+
| `delete` | `DELETE` | `http://api.url/users/123` |
|
|
47
|
+
| `deleteMany` | `DELETE` | `http://api.url/users?id=123&id=456` |
|
|
48
|
+
|
|
49
|
+
## Backend Requirements
|
|
50
|
+
|
|
51
|
+
For the Spring Boot backend implementation, use the **[ra-spring-json-server]** library which provides all the necessary endpoints and configurations to work with this data provider.
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
### Running Integration Tests
|
|
56
|
+
|
|
57
|
+
You can run the complete integration test suite from the project root:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd .. && ./run-integration-tests.sh
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This script will start the Spring Boot backend, run the tests, and clean up automatically.
|
|
64
|
+
|
|
65
|
+
## Test Cases
|
|
66
|
+
|
|
67
|
+
The tests cover:
|
|
68
|
+
|
|
69
|
+
- ✅ Display users list
|
|
70
|
+
- ✅ Create a new user
|
|
71
|
+
- ✅ Edit an existing user
|
|
72
|
+
- ✅ Delete a user
|
|
73
|
+
- ✅ Filter/search users
|
|
74
|
+
- ✅ Sort users
|
|
75
|
+
|
|
76
|
+
## Requirements
|
|
77
|
+
|
|
78
|
+
- Node.js 18+
|
|
79
|
+
- Java 17+
|
|
80
|
+
|
|
81
|
+
[ra-spring-json-server]: https://github.com/femrek/ra-spring-json-server
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ra-spring-data-provider",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "React Admin data provider for Spring Boot REST APIs",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"test": "playwright test",
|
|
9
|
+
"test:ui": "playwright test --ui",
|
|
10
|
+
"test:headed": "playwright test --headed",
|
|
11
|
+
"test:debug": "playwright test --debug",
|
|
12
|
+
"test:users": "playwright test users.spec.js",
|
|
13
|
+
"test:data-provider": "playwright test data-provider.spec.js",
|
|
14
|
+
"test:error-handling": "playwright test error-handling.spec.js",
|
|
15
|
+
"test:performance": "playwright test performance.spec.js",
|
|
16
|
+
"test:ui-ux": "playwright test ui-ux.spec.js",
|
|
17
|
+
"test:report": "playwright show-report",
|
|
18
|
+
"install-browsers": "playwright install",
|
|
19
|
+
"release": "release-it"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"react-admin",
|
|
23
|
+
"data-provider",
|
|
24
|
+
"spring-boot",
|
|
25
|
+
"rest-api"
|
|
26
|
+
],
|
|
27
|
+
"main": "src/index.js",
|
|
28
|
+
"module": "src/index.js",
|
|
29
|
+
"files": [
|
|
30
|
+
"src",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/femrek/ra-spring-json-server.git",
|
|
37
|
+
"directory": "ra-spring-data-provider"
|
|
38
|
+
},
|
|
39
|
+
"author": "femrek",
|
|
40
|
+
"license": "Apache-2.0",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"query-string": "^9.1.0",
|
|
43
|
+
"ra-core": "^5.4.0",
|
|
44
|
+
"ra-data-json-server": "^5.13.6"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": "^18.3.1",
|
|
48
|
+
"react-admin": "^5.4.0",
|
|
49
|
+
"react-dom": "^18.3.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@emotion/react": "^11.13.3",
|
|
53
|
+
"@emotion/styled": "^11.13.0",
|
|
54
|
+
"@mui/icons-material": "^6.3.0",
|
|
55
|
+
"@mui/material": "^6.3.0",
|
|
56
|
+
"@playwright/experimental-ct-react": "^1.49.0",
|
|
57
|
+
"@playwright/test": "^1.49.0",
|
|
58
|
+
"@types/node": "^25.0.10",
|
|
59
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
60
|
+
"react": "^18.3.1",
|
|
61
|
+
"react-admin": "^5.4.0",
|
|
62
|
+
"react-dom": "^18.3.1",
|
|
63
|
+
"release-it": "^19.2.4",
|
|
64
|
+
"vite": "^6.0.5"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DataProvider } from "ra-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a React Admin data provider for Spring Boot REST APIs
|
|
5
|
+
*
|
|
6
|
+
* @param apiUrl - The base URL of your Spring Boot API (e.g., 'http://localhost:8081/api')
|
|
7
|
+
* @param httpClient - Optional custom HTTP client function (defaults to fetchUtils.fetchJson)
|
|
8
|
+
* @returns A React Admin DataProvider configured for Spring Boot
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import raSpringDataProvider from 'ra-spring-data-provider';
|
|
12
|
+
*
|
|
13
|
+
* const dataProvider = raSpringDataProvider('http://localhost:8081/api');
|
|
14
|
+
*
|
|
15
|
+
* const App = () => (
|
|
16
|
+
* <Admin dataProvider={dataProvider}>
|
|
17
|
+
* <Resource name="users" list={UserList} />
|
|
18
|
+
* </Admin>
|
|
19
|
+
* );
|
|
20
|
+
*/
|
|
21
|
+
declare const raSpringDataProvider: (
|
|
22
|
+
apiUrl: string,
|
|
23
|
+
httpClient?: (
|
|
24
|
+
url: string,
|
|
25
|
+
options?: any,
|
|
26
|
+
) => Promise<{ headers: Headers; json: any }>,
|
|
27
|
+
) => DataProvider;
|
|
28
|
+
|
|
29
|
+
export default raSpringDataProvider;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import queryString from "query-string";
|
|
2
|
+
import { fetchUtils, DataProvider } from "ra-core";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a React Admin data provider for Spring Boot REST APIs following JSON Server conventions.
|
|
6
|
+
*
|
|
7
|
+
* This data provider is designed to work with Spring Boot controllers that implement the
|
|
8
|
+
* IRAController interface, supporting JSON Server-style query parameters and response formats.
|
|
9
|
+
*
|
|
10
|
+
* @param apiUrl - The base URL of your Spring Boot API (e.g., 'http://localhost:8081/api')
|
|
11
|
+
* @param httpClient - Optional custom HTTP client function (defaults to fetchUtils.fetchJson)
|
|
12
|
+
*
|
|
13
|
+
* @returns A React Admin DataProvider instance
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* import { Admin, Resource } from 'react-admin';
|
|
18
|
+
* import raSpringDataProvider from 'ra-spring-data-provider';
|
|
19
|
+
*
|
|
20
|
+
* const dataProvider = raSpringDataProvider('http://localhost:8081/api');
|
|
21
|
+
*
|
|
22
|
+
* const App = () => (
|
|
23
|
+
* <Admin dataProvider={dataProvider}>
|
|
24
|
+
* <Resource name="users" list={UserList} edit={UserEdit} create={UserCreate} />
|
|
25
|
+
* </Admin>
|
|
26
|
+
* );
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* **API Requirements:**
|
|
31
|
+
* - GET endpoints must return X-Total-Count header for pagination
|
|
32
|
+
* - List queries use _start, _end, _sort, _order query parameters
|
|
33
|
+
* - Bulk operations (updateMany, deleteMany) use multiple id query parameters
|
|
34
|
+
* - CORS must expose the X-Total-Count header
|
|
35
|
+
*
|
|
36
|
+
* **Supported Operations:**
|
|
37
|
+
* - `getList`: GET /resource?_start=0&_end=10&_sort=id&_order=ASC
|
|
38
|
+
* - `getOne`: GET /resource/123
|
|
39
|
+
* - `getMany`: GET /resource?id=123&id=456&id=789
|
|
40
|
+
* - `getManyReference`: GET /resource?author_id=12&_start=0&_end=10
|
|
41
|
+
* - `create`: POST /resource with JSON body
|
|
42
|
+
* - `update`: PUT /resource/123 with JSON body
|
|
43
|
+
* - `updateMany`: PUT /resource?id=123&id=456 with JSON body (bulk update)
|
|
44
|
+
* - `delete`: DELETE /resource/123
|
|
45
|
+
* - `deleteMany`: DELETE /resource?id=123&id=456 (bulk delete)
|
|
46
|
+
*
|
|
47
|
+
* **Spring Boot Adaptations:**
|
|
48
|
+
* - Bulk operations (updateMany, deleteMany) send single requests with multiple id parameters
|
|
49
|
+
* - updateMany sends data fields in request body to update all specified records
|
|
50
|
+
* - This differs from standard ra-data-json-server which sends individual requests for bulk operations
|
|
51
|
+
*
|
|
52
|
+
* **Embedded Resources:**
|
|
53
|
+
* Use the `meta.embed` parameter to request related records:
|
|
54
|
+
* ```tsx
|
|
55
|
+
* useGetOne('posts', { id: 1, meta: { embed: 'author' } })
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export default (
|
|
59
|
+
apiUrl: string,
|
|
60
|
+
httpClient = fetchUtils.fetchJson,
|
|
61
|
+
): DataProvider => ({
|
|
62
|
+
getList: async (resource, params) => {
|
|
63
|
+
const { page, perPage } = params.pagination || {};
|
|
64
|
+
const { field, order } = params.sort || {};
|
|
65
|
+
const query = {
|
|
66
|
+
...fetchUtils.flattenObject(params.filter),
|
|
67
|
+
_sort: field,
|
|
68
|
+
_order: order,
|
|
69
|
+
_start:
|
|
70
|
+
page != null && perPage != null ? (page - 1) * perPage : undefined,
|
|
71
|
+
_end: page != null && perPage != null ? page * perPage : undefined,
|
|
72
|
+
_embed: params?.meta?.embed,
|
|
73
|
+
};
|
|
74
|
+
const url = `${apiUrl}/${resource}?${queryString.stringify(query)}`;
|
|
75
|
+
|
|
76
|
+
const { headers, json } = await httpClient(url, {
|
|
77
|
+
signal: params?.signal,
|
|
78
|
+
});
|
|
79
|
+
if (!headers.has("x-total-count")) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"The X-Total-Count header is missing in the HTTP Response. The jsonServer Data Provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare X-Total-Count in the Access-Control-Expose-Headers header?",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const totalString = headers.get("x-total-count")!.split("/").pop();
|
|
85
|
+
if (totalString == null) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"The X-Total-Count header is invalid in the HTTP Response.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return { data: json, total: parseInt(totalString, 10) };
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
getOne: async (resource, params) => {
|
|
94
|
+
let url = `${apiUrl}/${resource}/${params.id}`;
|
|
95
|
+
if (params?.meta?.embed) {
|
|
96
|
+
url += `?_embed=${params.meta.embed}`;
|
|
97
|
+
}
|
|
98
|
+
const { json } = await httpClient(url, { signal: params?.signal });
|
|
99
|
+
return { data: json };
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
getMany: async (resource, params) => {
|
|
103
|
+
const query = {
|
|
104
|
+
id: params.ids,
|
|
105
|
+
_embed: params?.meta?.embed,
|
|
106
|
+
};
|
|
107
|
+
const url = `${apiUrl}/${resource}?${queryString.stringify(query)}`;
|
|
108
|
+
const { json } = await httpClient(url, { signal: params?.signal });
|
|
109
|
+
return { data: json };
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
getManyReference: async (resource, params) => {
|
|
113
|
+
const { page, perPage } = params.pagination;
|
|
114
|
+
const { field, order } = params.sort;
|
|
115
|
+
const query = {
|
|
116
|
+
...fetchUtils.flattenObject(params.filter),
|
|
117
|
+
[params.target]: params.id,
|
|
118
|
+
_sort: field,
|
|
119
|
+
_order: order,
|
|
120
|
+
_start: (page - 1) * perPage,
|
|
121
|
+
_end: page * perPage,
|
|
122
|
+
_embed: params?.meta?.embed,
|
|
123
|
+
};
|
|
124
|
+
const url = `${apiUrl}/${resource}?${queryString.stringify(query)}`;
|
|
125
|
+
|
|
126
|
+
const { headers, json } = await httpClient(url, {
|
|
127
|
+
signal: params?.signal,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!headers.has("x-total-count")) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"The X-Total-Count header is missing in the HTTP Response. The jsonServer Data Provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare X-Total-Count in the Access-Control-Expose-Headers header?",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const totalString = headers.get("x-total-count")!.split("/").pop();
|
|
136
|
+
if (totalString == null) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"The X-Total-Count header is invalid in the HTTP Response.",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return { data: json, total: parseInt(totalString, 10) };
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
update: async (resource, params) => {
|
|
145
|
+
const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}`, {
|
|
146
|
+
method: "PUT",
|
|
147
|
+
body: JSON.stringify(params.data),
|
|
148
|
+
});
|
|
149
|
+
return { data: json };
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// Spring Boot bulk update: PUT /resource?id=1&id=2&id=3 with data in body
|
|
153
|
+
updateMany: async (resource, params) => {
|
|
154
|
+
const query = {
|
|
155
|
+
id: params.ids,
|
|
156
|
+
};
|
|
157
|
+
const url = `${apiUrl}/${resource}?${queryString.stringify(query)}`;
|
|
158
|
+
const { json } = await httpClient(url, {
|
|
159
|
+
method: "PUT",
|
|
160
|
+
body: JSON.stringify(params.data),
|
|
161
|
+
});
|
|
162
|
+
return { data: json };
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
create: async (resource, params) => {
|
|
166
|
+
const { json } = await httpClient(`${apiUrl}/${resource}`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
body: JSON.stringify(params.data),
|
|
169
|
+
});
|
|
170
|
+
return { data: { ...params.data, ...json } as any };
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
delete: async (resource, params) => {
|
|
174
|
+
const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}`, {
|
|
175
|
+
method: "DELETE",
|
|
176
|
+
});
|
|
177
|
+
return { data: json };
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// Spring Boot bulk delete: DELETE /resource?id=1&id=2&id=3
|
|
181
|
+
deleteMany: async (resource, params) => {
|
|
182
|
+
const query = {
|
|
183
|
+
id: params.ids,
|
|
184
|
+
};
|
|
185
|
+
const url = `${apiUrl}/${resource}?${queryString.stringify(query)}`;
|
|
186
|
+
const { json } = await httpClient(url, {
|
|
187
|
+
method: "DELETE",
|
|
188
|
+
});
|
|
189
|
+
return { data: json };
|
|
190
|
+
},
|
|
191
|
+
});
|