|
| 1 | +# Admin On REST generator |
| 2 | + |
| 3 | +## Summary |
| 4 | +This generator is alternative to api-platform/admin [api-platform/admin](https://github.com/api-platform/admin) |
| 5 | + |
| 6 | +**api-platform/admin** allows to generate all resources on the fly and is immune to API changes. |
| 7 | + |
| 8 | +**Generator** allows more configuration to the resource components by generated config file (config file per resource). In case of API changes resource components have to be regenerated to expose changes |
| 9 | + |
| 10 | +Example configuration of the resource component at the bottom of this document. |
| 11 | + |
| 12 | +## Usage |
| 13 | +Create a React application using [Facebook's Create React App](https://github.com/facebookincubator/create-react-app): |
| 14 | + |
| 15 | + $ create-react-app my-app |
| 16 | + $ cd my-app |
| 17 | + |
| 18 | +React and React DOM will be directly provided as dependencies of Admin On REST. As having different versions of React |
| 19 | +causes issues, remove `react` and `react-dom` from the `dependencies` section of the generated `package.json` file: |
| 20 | + |
| 21 | +```patch |
| 22 | +- "react": "^16.0.0", |
| 23 | +- "react-dom": "^16.0.0", |
| 24 | +``` |
| 25 | +Install admin-on-rest |
| 26 | + |
| 27 | + $ yarn add admin-on-rest |
| 28 | + |
| 29 | +Install the generator globally: |
| 30 | + |
| 31 | + $ yarn global add @api-platform/client-generator |
| 32 | + |
| 33 | +In the app directory, generate the files for the resource you want: |
| 34 | + |
| 35 | + $ generate-api-platform-client https://demo.api-platform.com src/ -g admin-on-rest --resource foo |
| 36 | + # Replace the URL by the entrypoint of your Hydra-enabled API |
| 37 | + # Omit the resource flag to generate files for all resource types exposed by the API |
| 38 | + |
| 39 | +Create *apiPlatformRestClient.js* |
| 40 | + |
| 41 | +```javascript |
| 42 | +import { |
| 43 | + GET_LIST, |
| 44 | + GET_ONE, |
| 45 | + GET_MANY, |
| 46 | + GET_MANY_REFERENCE, |
| 47 | + CREATE, |
| 48 | + UPDATE, |
| 49 | + DELETE, |
| 50 | + fetchUtils |
| 51 | +} from 'admin-on-rest'; |
| 52 | +import {stringify} from 'query-string'; |
| 53 | + |
| 54 | +const {fetchJson} = fetchUtils; |
| 55 | + |
| 56 | +export default (API_URL, authTokenName = null, authTokenValue = null, httpClient = fetchJson) => { |
| 57 | + /** |
| 58 | + * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE' |
| 59 | + * @param {String} resource Name of the resource to fetch, e.g. 'posts' |
| 60 | + * @param {Object} params The REST request params, depending on the type |
| 61 | + * @returns {Object} { url, options } The HTTP request parameters |
| 62 | + */ |
| 63 | + const convertRESTRequestToHTTP = (type, resource, params) => { |
| 64 | + let url = ''; |
| 65 | + const options = {}; |
| 66 | + options.headers = new Headers({'Accept': 'application/ld+json'}); |
| 67 | + if( authTokenName && authTokenValue) { |
| 68 | + let authHeader = {}; |
| 69 | + authHeader[authTokenName] = authTokenValue; |
| 70 | + options.headers = new Headers(authHeader); |
| 71 | + } |
| 72 | + switch (type) { |
| 73 | + case GET_LIST: { |
| 74 | + const {page, perPage} = params.pagination; |
| 75 | + url = `${API_URL}/${resource}?page=${page}&itemsPerPage=${perPage}`; |
| 76 | + break; |
| 77 | + } |
| 78 | + case GET_ONE: |
| 79 | + url = `${API_URL}/${resource}/${params.id}`; |
| 80 | + break; |
| 81 | + case GET_MANY: { |
| 82 | + const query = { |
| 83 | + filter: JSON.stringify({id: params.ids}), |
| 84 | + }; |
| 85 | + url = `${API_URL}/${resource}?${stringify(query)}`; |
| 86 | + break; |
| 87 | + } |
| 88 | + case GET_MANY_REFERENCE: { |
| 89 | + const {page, perPage} = params.pagination; |
| 90 | + const {field, order} = params.sort; |
| 91 | + const query = { |
| 92 | + sort: JSON.stringify([field, order]), |
| 93 | + range: JSON.stringify([(page - 1) * perPage, (page * perPage) - 1]), |
| 94 | + filter: JSON.stringify({...params.filter, [params.target]: params.id}), |
| 95 | + }; |
| 96 | + url = `${API_URL}/${resource}?${stringify(query)}`; |
| 97 | + break; |
| 98 | + } |
| 99 | + case UPDATE: |
| 100 | + url = `${API_URL}/${resource}/${params.id}`; |
| 101 | + options.method = 'PUT'; |
| 102 | + options.body = JSON.stringify(params.data); |
| 103 | + break; |
| 104 | + case CREATE: |
| 105 | + url = `${API_URL}/${resource}`; |
| 106 | + options.method = 'POST'; |
| 107 | + options.body = JSON.stringify(params.data); |
| 108 | + break; |
| 109 | + case DELETE: |
| 110 | + url = `${API_URL}/${resource}/${params.id}`; |
| 111 | + options.method = 'DELETE'; |
| 112 | + break; |
| 113 | + default: |
| 114 | + throw new Error(`Unsupported fetch action type ${type}`); |
| 115 | + } |
| 116 | + return { url, options }; |
| 117 | + }; |
| 118 | + |
| 119 | + /** |
| 120 | + * @param {Object} response HTTP response from fetch() |
| 121 | + * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE' |
| 122 | + * @param {String} resource Name of the resource to fetch, e.g. 'posts' |
| 123 | + * @param {Object} params The REST request params, depending on the type |
| 124 | + * @returns {Object} REST response |
| 125 | + */ |
| 126 | + const convertHTTPResponseToREST = (response, type, resource, params) => { |
| 127 | + const { json } = response; |
| 128 | + switch (type) { |
| 129 | + case GET_LIST: |
| 130 | + case GET_MANY_REFERENCE: |
| 131 | + return { |
| 132 | + data: json["hydra:member"].map(x => x), |
| 133 | + total: json["hydra:totalItems"] |
| 134 | + }; |
| 135 | + case CREATE: |
| 136 | + return { data: { ...params.data, id: json.id } }; |
| 137 | + case DELETE: |
| 138 | + return { data: { ...params.data } }; |
| 139 | + default: |
| 140 | + return { data: json }; |
| 141 | + } |
| 142 | + }; |
| 143 | + |
| 144 | + /** |
| 145 | + * @param {string} type Request type, e.g GET_LIST |
| 146 | + * @param {string} resource Resource name, e.g. "posts" |
| 147 | + * @param {Object} payload Request parameters. Depends on the request type |
| 148 | + * @returns {Promise} the Promise for a REST response |
| 149 | + */ |
| 150 | + return (type, resource, params) => { |
| 151 | + // json-server doesn't handle WHERE IN requests, so we fallback to calling GET_ONE n times instead |
| 152 | + if (type === GET_MANY) { |
| 153 | + return Promise.all(params.ids.map(id => httpClient(`${API_URL}/${resource}/${id}`))) |
| 154 | + .then(responses => ({ data: responses.map(response => response.json) })); |
| 155 | + } |
| 156 | + const { url, options } = convertRESTRequestToHTTP(type, resource, params); |
| 157 | + return httpClient(url, options) |
| 158 | + .then(response => convertHTTPResponseToREST(response, type, resource, params)); |
| 159 | + }; |
| 160 | +}; |
| 161 | +``` |
| 162 | + |
| 163 | +Replace App.js content with |
| 164 | + |
| 165 | +```javascript |
| 166 | +import React, { Component } from 'react'; |
| 167 | +import { Admin } from "admin-on-rest"; |
| 168 | +import restClient from './apiPlatformRestClient'; |
| 169 | +import { API_HOST, API_PATH } from "./config/_entrypoint"; |
| 170 | +import * as resources from "./resource-import"; |
| 171 | + |
| 172 | +const API_URL = (API_HOST + API_PATH).replace(/\/$/, ""); |
| 173 | + |
| 174 | +class App extends Component { |
| 175 | + render() { |
| 176 | + return ( |
| 177 | + <Admin restClient={restClient(API_URL)}> |
| 178 | + {Object.keys(resources).map( (key) => resources[key] )} |
| 179 | + </Admin> |
| 180 | + ); |
| 181 | + } |
| 182 | +} |
| 183 | + |
| 184 | +export default App; |
| 185 | + |
| 186 | + |
| 187 | +```` |
| 188 | +The code is ready to be executed! |
| 189 | + |
| 190 | +Resource configuration example config/book.js: |
| 191 | + |
| 192 | +Each resource component has fields and buttons config options. |
| 193 | +False setting hides field or button. |
| 194 | +```javascript |
| 195 | +export const configList = { |
| 196 | + '@id': true, |
| 197 | + id: true, |
| 198 | + isbn: true, |
| 199 | + description: true, |
| 200 | + author: true, |
| 201 | + title: true, |
| 202 | + publicationDate: true, |
| 203 | + buttons: { |
| 204 | + show: true, |
| 205 | + edit: true, |
| 206 | + create: true, |
| 207 | + refresh: true, |
| 208 | + delete: true, |
| 209 | + } |
| 210 | +} |
| 211 | +
|
| 212 | +export const configEdit = { |
| 213 | + '@id': true, |
| 214 | + id: true, |
| 215 | + isbn: true, |
| 216 | + description: true, |
| 217 | + author: true, |
| 218 | + title: true, |
| 219 | + publicationDate: true, |
| 220 | + buttons: { |
| 221 | + show: true, |
| 222 | + list: true, |
| 223 | + delete: true, |
| 224 | + refresh: true, |
| 225 | + } |
| 226 | +} |
| 227 | +
|
| 228 | +export const configCreate = { |
| 229 | + '@id': true, |
| 230 | + id: true, |
| 231 | + isbn: true, |
| 232 | + description: true, |
| 233 | + author: true, |
| 234 | + title: true, |
| 235 | + publicationDate: true, |
| 236 | + buttons: { |
| 237 | + list: true, |
| 238 | + } |
| 239 | +} |
| 240 | +
|
| 241 | +export const configShow = { |
| 242 | + '@id': true, |
| 243 | + id: true, |
| 244 | + isbn: true, |
| 245 | + description: true, |
| 246 | + author: true, |
| 247 | + title: true, |
| 248 | + publicationDate: true, |
| 249 | + buttons: { |
| 250 | + edit: true, |
| 251 | + list: true, |
| 252 | + delete: true, |
| 253 | + refresh: true, |
| 254 | + } |
| 255 | +} |
| 256 | +
|
| 257 | +``` |
| 258 | + |
| 259 | +Previous chapter: [Vue.js generator](vuejs.md) |
| 260 | + |
| 261 | +Next chapter: [Troubleshooting](troubleshooting.md) |
0 commit comments