ApiLogicServer 15.0.25__py3-none-any.whl → 15.0.27__py3-none-any.whl

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.
Files changed (57) hide show
  1. api_logic_server_cli/api_logic_server.py +2 -2
  2. api_logic_server_cli/api_logic_server_info.yaml +3 -3
  3. api_logic_server_cli/genai/genai_admin_app.py +29 -19
  4. api_logic_server_cli/prototypes/.DS_Store +0 -0
  5. api_logic_server_cli/prototypes/basic_demo/.DS_Store +0 -0
  6. api_logic_server_cli/prototypes/basic_demo/customizations/.DS_Store +0 -0
  7. api_logic_server_cli/prototypes/basic_demo/customizations/ui/.DS_Store +0 -0
  8. api_logic_server_cli/prototypes/basic_demo/customizations/ui/admin/.DS_Store +0 -0
  9. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/.DS_Store +0 -0
  10. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/README.md +17 -0
  11. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/README_create_react_app.md +70 -0
  12. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/package-lock.json +18469 -0
  13. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/package.json +47 -0
  14. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/public/favicon.ico +0 -0
  15. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/public/index.html +43 -0
  16. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/public/logo192.png +0 -0
  17. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/public/logo512.png +0 -0
  18. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/public/manifest.json +25 -0
  19. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/public/robots.txt +3 -0
  20. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/App.css +38 -0
  21. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/App.js +61 -0
  22. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/App.test.js +8 -0
  23. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/Config-reference.js +527 -0
  24. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/Config.js +527 -0
  25. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/Customer-reference.js +216 -0
  26. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/Customer.js +230 -0
  27. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/Item.js +170 -0
  28. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/Order.js +207 -0
  29. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/Product.js +140 -0
  30. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/SysEmail.js +157 -0
  31. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/SysMcp.js +110 -0
  32. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/app_loader.js +24 -0
  33. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/index.css +13 -0
  34. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/index.js +17 -0
  35. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/logo.svg +1 -0
  36. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/.eslintrc +5 -0
  37. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/.yarnrc.yml +4 -0
  38. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/default-settings.js +25 -0
  39. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/default-settings.ts +25 -0
  40. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/errors.js +116 -0
  41. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/errors.ts +116 -0
  42. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/index.test.tsx +7 -0
  43. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/index.tsx +11 -0
  44. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/ra-jsonapi-client.js +577 -0
  45. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/ra-jsonapi-client.ts +577 -0
  46. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/resourceLookup.js +124 -0
  47. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/resourceLookup.ts +124 -0
  48. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/rav4-jsonapi-client/styles.module.css +9 -0
  49. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/reportWebVitals.js +13 -0
  50. api_logic_server_cli/prototypes/basic_demo/customizations/ui/basic_demo_app/src/setupTests.js +5 -0
  51. api_logic_server_cli/prototypes/manager/system/genai/app_templates/app_learning/Admin-App-Resource-Learning-Prompt.md +37 -3
  52. {apilogicserver-15.0.25.dist-info → apilogicserver-15.0.27.dist-info}/METADATA +1 -1
  53. {apilogicserver-15.0.25.dist-info → apilogicserver-15.0.27.dist-info}/RECORD +57 -11
  54. {apilogicserver-15.0.25.dist-info → apilogicserver-15.0.27.dist-info}/WHEEL +0 -0
  55. {apilogicserver-15.0.25.dist-info → apilogicserver-15.0.27.dist-info}/entry_points.txt +0 -0
  56. {apilogicserver-15.0.25.dist-info → apilogicserver-15.0.27.dist-info}/licenses/LICENSE +0 -0
  57. {apilogicserver-15.0.25.dist-info → apilogicserver-15.0.27.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,577 @@
1
+ import { stringify } from 'query-string';
2
+ import { fetchUtils, DataProvider, HttpError } from 'react-admin';
3
+ import merge from 'deepmerge';
4
+ import { defaultSettings } from './default-settings';
5
+ import ResourceLookup from './resourceLookup';
6
+ import Keycloak from 'keycloak-js';
7
+ import {getConf} from '../Config'
8
+ import urlJoin from 'url-join';
9
+
10
+ const conf : { [ key: string] : any } = getConf();
11
+ const duration = 2000;
12
+
13
+ const prepareAttributes = (attributes : any, resource_en : any) => {
14
+ const resource = decodeURI(resource_en)
15
+ // temp: convert all numbers to string to allow FK lookups (jsonapi ids are strings, while FKs may be numbers :////)
16
+ const resource_attr_rels = conf.resources[resource].attributes?.map( (attr : any) => attr.relationship ? attr.name : null)
17
+ const m_attrs = Object.assign({}, attributes)
18
+ for(let [k, v] of Object.entries(attributes)){
19
+ m_attrs[k] = v
20
+ if(typeof v === 'number' && resource_attr_rels.find((n: string) => n === k)){
21
+ m_attrs[k] = v.toString();
22
+ }
23
+ }
24
+ return m_attrs
25
+ }
26
+
27
+ const prepareQueryFilter = (query: any, ids : any, fks : any) => {
28
+ if(ids.length === fks.length){
29
+ for(let i = 0; i<fks.length; i++){
30
+ let fk = fks[i]
31
+ let id = ids[i]
32
+ query[`filter[${fk}]`] = id
33
+ }
34
+ }
35
+ else{
36
+ // fk probably contains an underscore
37
+ // todo: how to fix???
38
+ console.warn("Wrong FK length.. ", ids, fks)
39
+ query[`filter[${fks[0]}]`] = ids && ids.length ? ids[0] : ""
40
+ }
41
+ }
42
+
43
+ export const getKeycloakHeaders = (
44
+ keycloak: Keycloak,
45
+ options: fetchUtils.Options | undefined
46
+ ): Headers => {
47
+ const headers = ((options && options.headers) ||
48
+ new Headers({
49
+ Accept: 'application/json',
50
+ })) as Headers;
51
+ if(keycloak?.isTokenExpired()){
52
+ keycloak.updateToken(30)
53
+ }
54
+ if (keycloak.token) {
55
+ headers.set('Authorization', `Bearer ${keycloak.token}`);
56
+ }
57
+ return headers;
58
+ };
59
+
60
+ export const httpAuthClient = (url: string, options : any) => {
61
+ if (!options.headers) {
62
+ options.headers = new Headers({ Accept: 'application/json' });
63
+ }
64
+ const token : string = localStorage.getItem('auth_token') || "";
65
+ if(token){
66
+ options.headers.set('Authorization', `Bearer ${token}`);
67
+ }
68
+ return fetchUtils.fetchJson(url, options)
69
+ .catch((e:HttpError ) => {
70
+ const msg = e.body?.msg || "Unknown Error."
71
+ console.warn('httpAuthClient httperror', e, e.status, e.body, msg)
72
+ if(e.body?.message?.startsWith('Booting project')){
73
+ // Custom error handling for booting, todo!!
74
+ console.log('WGserver_response: booting', e, e.status, e.body)
75
+ setTimeout(() => document.location.reload(), 3000);
76
+ throw new HttpError(e, e.status,
77
+ {title: "Booting",
78
+ detail:"Booting project, please wait",
79
+ message:"Booting project, please wait",
80
+ code:e.status})
81
+ }
82
+ throw new HttpError(e, e.status, e.body)
83
+ })
84
+ }
85
+
86
+ const createKCHttpAuthClient = (keycloak: Keycloak) => (
87
+ url: any,
88
+ options: fetchUtils.Options | undefined
89
+ ) => {
90
+ if(!keycloak){
91
+ console.error("No keycloak")
92
+ return
93
+ }
94
+ if(keycloak.token){
95
+ localStorage.setItem('auth_token', keycloak.token)
96
+ }
97
+ const requestHeaders = getKeycloakHeaders(keycloak, options);
98
+ return fetchUtils.fetchJson(url, {
99
+ ...options,
100
+ headers: requestHeaders,
101
+ })
102
+ .catch(e => {
103
+ console.warn('KC httpAuthClient httperror', e, e.body)
104
+ if(e.body?.message?.startsWith('Booting project')){
105
+ // Custom error handling for booting, todo!!
106
+ console.log('WGserver_response: booting', e, e.status, e.body)
107
+ setTimeout(() => document.location.reload(), 3000);
108
+ throw new HttpError(e, e.status,
109
+ {title: "Booting",
110
+ detail:"Booting project, please wait",
111
+ message:"Booting project, please wait",
112
+ code:e.status})
113
+ }
114
+ throw new HttpError(e, e.status, e.statusText)
115
+ })
116
+ };
117
+
118
+ /**
119
+ * Based on
120
+ *
121
+ * https://github.com/marmelab/react-admin/blob/master/packages/ra-data-simple-rest/src/index.ts
122
+ * @see https://github.com/marmelab/FakeRest
123
+ *
124
+ */
125
+ export const jsonapiClient = (
126
+ apiUrl: string,
127
+ userSettings = {conf : {}},
128
+ keycloak: Keycloak| null = null,
129
+ httpClient = httpAuthClient,//fetchUtils.fetchJson,
130
+ countHeader: string = 'Content-Range',
131
+ ): DataProvider => {
132
+
133
+ if(keycloak?.isTokenExpired()){
134
+ keycloak.login();
135
+ }
136
+ if(keycloak){
137
+ httpClient = createKCHttpAuthClient(keycloak)
138
+ }
139
+ const settings = merge(defaultSettings, userSettings);
140
+
141
+ return {
142
+ /*******************************************************************************************
143
+ * getList: fetch a collection
144
+ *******************************************************************************************/
145
+ getList: (resource_name_en, params = {}) => {
146
+ const conf = getConf();
147
+ const resource_name = decodeURI(resource_name_en);
148
+ const { page, perPage } = params?.pagination || { page: 1, perPage: 10 };
149
+ if(! conf.resources[resource_name]){
150
+ console.warn(`Invalid resource ${resource_name}`)
151
+ return Promise.reject(new Error(`Invalid resource ${resource_name}`))
152
+ }
153
+ const resource_conf : any = conf.resources[resource_name];
154
+ const sort : string = resource_conf.sort
155
+ // Create query with pagination params.
156
+ const query : {[k: string]: any} = {
157
+ 'page[number]': page,
158
+ 'page[size]': perPage,
159
+ 'page[offset]': (page - 1) * perPage,
160
+ 'page[limit]': perPage
161
+ };
162
+
163
+ if(sort){
164
+ query.sort = sort
165
+ }
166
+ // Add all filter params to query.
167
+ if(params.filter?.q && "resources" in conf){
168
+ // search is requested by react-admin
169
+ const search_cols = resource_conf.search_cols
170
+ const filter = search_cols?.map((col: any) => {
171
+ return {
172
+ "name":col.name,
173
+ "op": col.op? col.op : "ilike",
174
+ "val": col.val ? col.val.format(params.filter.q) : `%${params.filter.q}%`
175
+ };}) || ""
176
+ if(filter){
177
+ query['filter'] = JSON.stringify(filter)
178
+ }
179
+ }
180
+ else{
181
+ Object.keys(params.filter || {}).forEach((key) => {
182
+ query[`filter[${key}]`] = params.filter[key];
183
+ });
184
+ }
185
+
186
+ // Add sort parameter, first check the default configured sorting, then the customized sort
187
+ if (params.sort && params.sort.field) {
188
+ const prefix = params.sort.order?.toLowerCase() === 'desc' ? '-' : ''; // <> ASC
189
+ query.sort = `${prefix}${params.sort.field}`;
190
+ }
191
+ if(!query.sort){
192
+ query.sort = resource_conf.sort || "id"
193
+ }
194
+ const rel_conf = conf.resources[resource_name].relationships || []
195
+ // we only need "toone" rels in getList so we can show the join/user key
196
+ const includes: string[] = rel_conf.filter((rel : any) => rel.direction !== 'tomany').map((rel : any) => rel.name);
197
+ if(params.meta?.include?.length){
198
+ includes.push(...params.meta.include)
199
+ }
200
+ query['include'] = includes.join(',');
201
+
202
+ const url = `${apiUrl}/${resource_name_en}?${stringify(query)}`;
203
+ return httpClient(url, {})
204
+ .then(({ json }) => {
205
+ // const lookup = new ResourceLookup(json.data);
206
+ // When meta data and the 'total' setting is provided try
207
+ // to get the total count.
208
+ let total = 0;
209
+ if (json.meta && settings.total) {
210
+ total = json.meta[settings.total];
211
+ }
212
+ // Use the length of the data array as a fallback.
213
+ total = total || json.data.length;
214
+ const lookup = new ResourceLookup(json);
215
+ const jsonData = json.data.map((resource: any) =>{
216
+ return lookup.unwrapData(resource)
217
+ }
218
+ );
219
+ const validUntil = new Date();
220
+ validUntil.setTime(validUntil.getTime() + duration);
221
+ return {
222
+ data: jsonData,
223
+ included: json.included,
224
+ validUntil : validUntil,
225
+ total: total
226
+ };
227
+ })
228
+ .catch((err: HttpError) => {
229
+ console.warn('getList Error', err, err.body, err.status);
230
+ const errorHandler = settings.errorHandler;
231
+ return Promise.reject(errorHandler(err));
232
+ });
233
+ },
234
+
235
+ /*******************************************************************************************
236
+ getOne
237
+ ********************************************************************************************/
238
+ getOne: (resource_en: any, params: { id: any }) => {
239
+ const conf = getConf();
240
+ const resource = decodeURI(resource_en)
241
+ if(params.id === null || params.id === undefined){
242
+ return new Promise(()=>{return {data: {}}})
243
+ }
244
+ const resource_conf = conf["resources"][resource];
245
+ if(!resource_conf){
246
+ console.warn(`Invalid resource ${resource}`)
247
+ return new Promise(()=>{});
248
+ }
249
+ const rel_conf = resource_conf?.relationships || [];
250
+ const includes: string[] = rel_conf.map((rel : any) => rel.name).join(",")
251
+ const url = `${apiUrl}/${resource}/${params.id}?include=${includes}&page[limit]=1`; // we only need 1 include at most
252
+
253
+ return httpClient(url, {}).then(({ json }) => {
254
+
255
+ let { id, attributes, relationships, type } = json.data;
256
+ if(id === undefined){
257
+ return {data:{}}
258
+ }
259
+ Object.assign(attributes, relationships, {type: type}, {relationships: relationships}, {attributes: {...attributes} });
260
+ attributes = prepareAttributes(attributes, resource)
261
+ const validUntil = new Date();
262
+ validUntil.setTime(validUntil.getTime() + duration);
263
+ return {
264
+ data: {
265
+ id,
266
+ validUntil : validUntil,
267
+ ...attributes
268
+ }
269
+ };
270
+ });
271
+ },
272
+
273
+ /*******************************************************************************************
274
+ getMany
275
+ ********************************************************************************************/
276
+ getMany: (resource_en, params: any) => {
277
+ const conf = getConf();
278
+ if(resource_en === null || resource_en === undefined || resource_en === "" || resource_en === "Location"){
279
+ return new Promise(()=>{return {data: {}}})
280
+ }
281
+ const resource_de = decodeURI(resource_en)
282
+ const resource = capitalize(resource_de);
283
+ let query = `filter[id]=${params.ids instanceof Array ? params.ids.join(',') : JSON.stringify(params.ids)}`
284
+ if(params.meta?.include?.length){
285
+ query += `&include=${(params.meta.include).join(',')}`
286
+ }
287
+
288
+ const url = `${apiUrl}/${resource}?${query}`;
289
+ return httpClient(url, {}).then(({ json }) => {
290
+
291
+ // When meta data and the 'total' setting is provided try
292
+ // to get the total count.
293
+ let total = 0;
294
+ if (json.meta && settings.total) {
295
+ total = json.meta[settings.total];
296
+ }
297
+ // Use the length of the data array as a fallback.
298
+ total = total || json.data.length; // { id: any; attributes: any; }
299
+
300
+ const jsonData = json.data.map((value: any) => {
301
+ const result = Object.assign({ id: value.id, type: value.type, relationships: value.relationships }, prepareAttributes(value.attributes, resource))
302
+ //const related = json.included || [];
303
+ // TODO!!: this is not working, we need to find the included resources
304
+ // for(const [k,v] of Object.entries(value.relationships || {})){
305
+ // console.log('getMany', k, v)
306
+ // if(v instanceof Array){
307
+ // v.forEach((inc: any) => {
308
+ // const type = inc.data?.type
309
+ // if(!type){
310
+ // return
311
+ // }
312
+ // const related = json.included?.find((rel: any) => rel.type === type && rel.id === inc.id)
313
+ // Object.assign(result, { [k]: related})
314
+ // });
315
+ // }
316
+ // }
317
+ return result;
318
+ }
319
+ );
320
+ const validUntil = new Date();
321
+ validUntil.setTime(validUntil.getTime() + duration);
322
+ return {
323
+ data: jsonData,
324
+ validUntil : validUntil,
325
+ total: total
326
+ };
327
+ });
328
+ },
329
+
330
+ /*******************************************************************************************
331
+ getManyReference
332
+ ********************************************************************************************/
333
+ getManyReference: (resource_name_en, params : any) => {
334
+ const conf = getConf();
335
+ const resource_name = decodeURI(resource_name_en)
336
+ const { page, perPage } = params.pagination;
337
+ const { field, order } = params.sort;
338
+
339
+ const query: {[k: string]: any} = { };
340
+
341
+ if (field) {
342
+ const prefix = order === 'DESC' ? '-' : ''; // <> ASC
343
+ query.sort = `${prefix}${field}`;
344
+ }
345
+
346
+
347
+ let fks = params.target.split('_')
348
+ //const ids = fks.length > 1 ? params.id.split('_') : params.id
349
+ let ids = params.id.split('_')
350
+
351
+ if(ids.length !== fks.length){
352
+ console.warn("Wrong FK length ", ids, fks)
353
+ fks = [params.target]
354
+ ids = [params.id]
355
+ }
356
+
357
+ prepareQueryFilter(query, ids, fks);
358
+
359
+ query[`page[limit]`] = perPage
360
+ query[`page[offset]`] = (page - 1) * perPage
361
+
362
+ const options = {};
363
+ const resource_conf = conf["resources"][resource_name];
364
+ const rel_conf = resource_conf?.relationships || [];
365
+ const includes: string[] = rel_conf.map((rel : any) => rel.name).join(",")
366
+ const url = `${apiUrl}/${resource_name}?${stringify(query)}&include=${includes}`
367
+ return httpClient(url, options).then(({ headers, json }) => {
368
+ if (!headers.has(countHeader)) {
369
+ console.debug(
370
+ `The ${countHeader} header is missing in the HTTP Response. The simple REST 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 ${countHeader} in the Access-Control-Expose-Headers header?`
371
+ );
372
+ }
373
+ let total = json.meta?.total;
374
+ if (json.meta && settings.total) {
375
+ total = json.meta[settings.total];
376
+ }
377
+ // Use the length of the data array as a fallback.
378
+ total = total || json.data.length;
379
+ const lookup = new ResourceLookup(json);
380
+ const jsonData = json.data.map((resource: any) =>{
381
+ return lookup.unwrapData(resource)
382
+ }
383
+ );
384
+
385
+ return {
386
+ data: jsonData,
387
+ total: total
388
+ };
389
+ });
390
+ },
391
+
392
+ update: async(resource_name_en : string, params: any) => {
393
+ const conf = getConf();
394
+ const resource_name = decodeURI(resource_name_en)
395
+ let type = conf.resources[resource_name].type || resource_name;
396
+
397
+ const previousDataFiltered = Object.keys(params.previousData)
398
+ .filter(key => !(key in params.data))
399
+ .reduce((obj, key) => {
400
+ obj[key] = params.previousData[key];
401
+ return obj;
402
+ }, {});
403
+
404
+ let Sendingdata = { ...previousDataFiltered, ...params.data };
405
+ const data = {
406
+ data: {
407
+ id: params.id,
408
+ type: type,
409
+ attributes: Sendingdata
410
+ }
411
+ };
412
+
413
+ console.log('Update ', data)
414
+
415
+ return httpClient(`${apiUrl}/${resource_name}/${params.id}`, {
416
+ method: settings.updateMethod,
417
+ body: JSON.stringify(data)
418
+ })
419
+ .then(({ json }) => {
420
+ const { id, attributes } = json.data;
421
+ return {
422
+ data: {
423
+ id,
424
+ ...attributes
425
+ }
426
+ };
427
+ })
428
+ .catch((err: HttpError) => {
429
+ console.log('catch Error', err.body);
430
+ const errorHandler = settings.errorHandler;
431
+ return Promise.reject(errorHandler(err));
432
+ });
433
+ },
434
+
435
+ // },
436
+
437
+ // simple-rest doesn't handle provide an updateMany route, so we fallback to calling update n times instead
438
+ updateMany: (resource_name, params) => {
439
+ // todo : bulk update
440
+ const conf = getConf();
441
+ return Promise.all(
442
+ params.ids.map((id) => {
443
+ const data = {
444
+ data: {
445
+ attributes: params.data
446
+ }
447
+ };
448
+ return httpClient(`${apiUrl}/${decodeURI(resource_name)}/${id}`, {
449
+ method: 'PATCH',
450
+ body: JSON.stringify(params.data)
451
+ })
452
+ })
453
+ )
454
+ .then((responses) => ({ data: responses.map(({ json }) => json.id) }))
455
+ },
456
+
457
+ create: (resource_name_en : string, params: any) => {
458
+ const conf = getConf();
459
+ const resource_name = decodeURI(resource_name_en)
460
+ let type = conf.resources[resource_name].type || resource_name;
461
+ // use both current (in case of a form reference) and data in case of raw dataProvider.create() call
462
+ let item_data = params.data?.current?.data ?? params.data;
463
+ console.log('creating resource with params', params)
464
+ const data = {
465
+ data: {
466
+ type: type,
467
+ attributes: item_data
468
+ }
469
+ };
470
+ return httpClient(`${apiUrl}/${resource_name}`, {
471
+ method: 'POST',
472
+ body: JSON.stringify(data)
473
+ })
474
+ .then(({ json }) => {
475
+ const { id, attributes } = json.data;
476
+ return {
477
+ data: {
478
+ id,
479
+ ...attributes
480
+ }
481
+ };
482
+ })
483
+ .catch((err: HttpError) => {
484
+ console.log('catch Error', err.body);
485
+ const errorHandler = settings.errorHandler;
486
+ return Promise.reject(errorHandler(err));
487
+ })
488
+ },
489
+
490
+ delete: (resource, params) => {
491
+ const conf = getConf();
492
+ return httpClient(`${apiUrl}/${decodeURI(resource)}/${params.id}`, {
493
+ method: 'DELETE',
494
+ headers: new Headers({
495
+ 'Content-Type': 'text/plain'
496
+ })
497
+ }).then(({ json }) => ({ data: json }))},
498
+
499
+ // simple-rest doesn't handle filters on DELETE route, so we fallback to calling DELETE n times instead
500
+ deleteMany: (resource, params) => {
501
+ const conf = getConf();
502
+ return Promise.all(
503
+ params.ids.map((id) =>
504
+ httpClient(`${apiUrl}/${decodeURI(resource)}/${id}`, {
505
+ method: 'DELETE',
506
+ headers: new Headers({
507
+ 'Content-Type': 'text/plain'
508
+ })
509
+ })
510
+ )
511
+ ).then((responses) => ({
512
+ data: responses.map(({ json }) => json)
513
+ }))
514
+ },
515
+
516
+ getResources: () => {
517
+ const conf = getConf();
518
+ if(conf){
519
+ return Promise.resolve({data: conf});
520
+ };
521
+ return httpClient(`${apiUrl}/schema`, {
522
+ method: 'GET'
523
+ }).then(({json}) => {
524
+ localStorage.setItem('raconf', JSON.stringify(json));
525
+ return { data: json };
526
+ })
527
+ .catch(()=> {return {data : {}} })
528
+ },
529
+
530
+ execute: (resource_name_en: string, command: string, data: { id: string|undefined, data: any, args: any }) => {
531
+ const conf = getConf();
532
+ const resource_name = decodeURI(resource_name_en)
533
+ console.log(`execute rpc on resource ${resource_name} with params`, data)
534
+ const id = data?.id || ""
535
+ if(!command){
536
+ console.warn('No command provided')
537
+ return new Promise(()=>{})
538
+ }
539
+ const endpoint = id ? `${apiUrl}/${resource_name}/${id}/${command}` : `${apiUrl}/${resource_name}/${command}`
540
+ return httpClient(endpoint, {
541
+ method: 'POST',
542
+ body: JSON.stringify(data?.args || {})
543
+ })
544
+ },
545
+ };
546
+ };
547
+
548
+ function capitalize(s: string): string {
549
+ // todo
550
+ return s;
551
+ return s[0].toUpperCase() + s.slice(1);
552
+ }
553
+ export interface includeRelations {
554
+ resource: string;
555
+ includes: string[];
556
+ }
557
+
558
+ /*
559
+ Call safrs jsonapi rpc endpoints
560
+ */
561
+ export const jaRpc = async (endpoint: string, data?: any, options?: RequestInit) => {
562
+ const url = urlJoin(conf.api_root, endpoint);
563
+ console.log('jaRpc', url, data, options);
564
+ console.log('jaRpc', conf);
565
+
566
+ const defaultOptions: RequestInit = {
567
+ method: "POST",
568
+ headers: {
569
+ "Content-Type": "application/json",
570
+ Authorization: `Bearer ${localStorage.getItem("auth_token")}`,
571
+ },
572
+ body: JSON.stringify(data || {}),
573
+ };
574
+ const requestOptions = { ...defaultOptions, ...options };
575
+ const response = await fetch(url, requestOptions);
576
+ return response.json();
577
+ };
@@ -0,0 +1,124 @@
1
+ interface Relationship {
2
+ data: null | { id: string; type: string } | Array<{ id: string; type: string }>;
3
+ relationships?: { [key: string]: Relationship };
4
+ }
5
+
6
+ /**
7
+ * A map-like class that maps resource linkage objects {id: 1, type: "user"} to concrete resources with attributes and
8
+ * relationships
9
+ */
10
+ export default class ResourceLookup {
11
+ lookup: Map<any, any>;
12
+ includes: string[];
13
+
14
+ /**
15
+ * Constructs a new lookup map
16
+ * @param {Object} response A JSON API response, in JSON format
17
+ */
18
+ constructor(response: any) {
19
+ this.lookup = new Map();
20
+ this.includes = [];
21
+
22
+ // If the response wasn't a JSON dictionary, we can't and don't need to build a lookup
23
+ if (typeof response !== 'object') return;
24
+
25
+ let resources;
26
+ // if (response.hasOwnProperty('included')) {
27
+ if (Object.prototype.hasOwnProperty.call(response, 'included')) {
28
+ resources = [...response.data, ...response.included];
29
+ } else {
30
+ resources = response.data;
31
+ }
32
+
33
+ // Iterate over each resource returned and put each in the lookup
34
+ for (const entry of resources) {
35
+ const key = this.getKey(entry);
36
+ this.lookup.set(key, entry);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Calculates a hashable key for JSON API resources
42
+ * @param resource A resource linkage object
43
+ * @returns {string}
44
+ */
45
+ getKey(resource: any) {
46
+ return `${resource.type}:${resource.id}`;
47
+ }
48
+
49
+ /**
50
+ * Looks up a resource
51
+ * @param resource A resource linkage object
52
+ * @returns {Object}
53
+ */
54
+ get(resource: any) {
55
+ // If we don't have included data for this resource, just return the Resource Linkage object, since that's still
56
+ // useful
57
+ if (this.has(resource)) {
58
+ return this.lookup.get(this.getKey(resource));
59
+ } else {
60
+ return structuredClone(resource);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Returns true if the resource is in the lookup
66
+ * @param resource
67
+ * @returns {boolean}
68
+ */
69
+ has(resource: any) {
70
+ return this.lookup.has(this.getKey(resource));
71
+ }
72
+
73
+ /**
74
+ * Converts a JSON API data object (with id, type, and attributes fields) into a flattened object
75
+ * @param {Object} response A JSON API data object
76
+ */
77
+ unwrapData(response: any) {
78
+ // The base resource object, merge the attributes and the id/type fields
79
+ const result = Object.assign(
80
+ {
81
+ id: response?.id,
82
+ ja_type: response?.type,
83
+ type: response?.Type || response?.type,
84
+ attributes: response.attributes || {},
85
+ },
86
+ response.attributes
87
+ );
88
+
89
+ // Deal with relationships
90
+ if (Object.prototype.hasOwnProperty.call(response, 'relationships')) {
91
+ result.relationships = response.relationships;
92
+ for (const [relationshipName, relationship] of Object.entries(response.relationships)) {
93
+ if ((relationship ).data === null) {
94
+ result[relationshipName] = null;
95
+ }
96
+
97
+ if (!(relationship ).data) {
98
+ continue;
99
+ } else if (Array.isArray((relationship ).data && (relationship ).data)) {
100
+ result[relationshipName] = (relationship ).data?.map((linkage: any) => {
101
+ const resource = this.get(linkage);
102
+ const lresult = structuredClone(this.unwrapData(resource));
103
+ Object.entries(lresult.attributes || {}).map(([key, value]) => {
104
+ lresult[key] = value;
105
+ });
106
+ result.relationships[relationshipName].data = result[relationshipName];
107
+ return lresult;
108
+ });
109
+ } else if ((relationship ).data?.id) {
110
+ const resource = this.get((relationship ).data);
111
+ result[relationshipName] = this.unwrapData(resource);
112
+ result.relationships[relationshipName].data = result[relationshipName];
113
+ }
114
+ }
115
+ }
116
+
117
+ try {
118
+ JSON.stringify(structuredClone(result));
119
+ } catch (e) {
120
+ return null;
121
+ }
122
+ return structuredClone(result);
123
+ }
124
+ }