@raphiniert/ra-data-postgrest 2.0.0-alpha.1 → 2.0.0-alpha.3

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/src/index.ts CHANGED
@@ -48,13 +48,24 @@ import qs from 'qs';
48
48
  * @example
49
49
  *
50
50
  * import * as React from 'react';
51
- * import { Admin, Resource } from 'react-admin';
52
- * import postgrestRestProvider from '@raphiniert/ra-data-postgrest';
51
+ * import { Admin, Resource, fetchUtils } from 'react-admin';
52
+ * import postgrestRestProvider,
53
+ * { IDataProviderConfig,
54
+ * defaultPrimaryKeys,
55
+ * defaultSchema } from '@raphiniert/ra-data-postgrest';
53
56
  *
54
57
  * import { PostList } from './posts';
55
58
  *
59
+ * const config: IDataProviderConfig = {
60
+ * apiUrl: 'http://path.to.my.api/',
61
+ * httpClient: fetchUtils.fetchJson,
62
+ * defaultListOp: 'eq',
63
+ * primaryKeys: defaultPrimaryKeys,
64
+ * schema: defaultSchema
65
+ * }
66
+ *
56
67
  * const App = () => (
57
- * <Admin dataProvider={postgrestRestProvider('http://path.to.my.api/')}>
68
+ * <Admin dataProvider={postgrestRestProvider(config)}>
58
69
  * <Resource name="posts" list={PostList} />
59
70
  * </Admin>
60
71
  * );
@@ -62,20 +73,50 @@ import qs from 'qs';
62
73
  * export default App;
63
74
  */
64
75
 
65
- const defaultPrimaryKeys = new Map<string, PrimaryKey>();
66
-
67
- export default (
68
- apiUrl: string,
69
- httpClient = fetchUtils.fetchJson,
70
- defaultListOp: PostgRestOperator = 'eq',
71
- primaryKeys: Map<string, PrimaryKey> = defaultPrimaryKeys
72
- ): DataProvider => ({
76
+ export type MetaOption = string | undefined;
77
+
78
+ export const defaultPrimaryKeys = new Map<string, PrimaryKey>();
79
+
80
+ export const defaultSchema = () => '';
81
+
82
+ export interface IDataProviderConfig {
83
+ apiUrl: string;
84
+ httpClient: (string, Options) => Promise<any>;
85
+ defaultListOp: PostgRestOperator;
86
+ primaryKeys: Map<string, PrimaryKey>;
87
+ schema: () => string;
88
+ }
89
+
90
+ const useCustomSchema = (
91
+ schema: () => string,
92
+ metaSchema: MetaOption,
93
+ method: string
94
+ ) => {
95
+ let funcHeaderSchema = schema;
96
+ if (metaSchema !== undefined) {
97
+ funcHeaderSchema = () => metaSchema;
98
+ }
99
+
100
+ if (funcHeaderSchema().length > 0) {
101
+ let schemaHeader = '';
102
+ if (['GET', 'HEAD'].includes(method)) {
103
+ schemaHeader = 'Accept-Profile';
104
+ } else if (['POST', 'PATCH', 'PUT', 'DELETE'].includes(method)) {
105
+ schemaHeader = 'Content-Profile';
106
+ } else return {};
107
+
108
+ return { [schemaHeader]: funcHeaderSchema() };
109
+ } else return {};
110
+ };
111
+
112
+ export default (config: IDataProviderConfig): DataProvider => ({
73
113
  getList: (resource, params: Partial<GetListParams> = {}) => {
74
- const primaryKey = getPrimaryKey(resource, primaryKeys);
114
+ const primaryKey = getPrimaryKey(resource, config.primaryKeys);
75
115
 
76
116
  const { page, perPage } = params.pagination;
77
117
  const { field, order } = params.sort || {};
78
- const { filter, select } = parseFilters(params, defaultListOp);
118
+ const { filter, select } = parseFilters(params, config.defaultListOp);
119
+ const metaSchema = params.meta?.schema;
79
120
 
80
121
  let query = {
81
122
  offset: String((page - 1) * perPage),
@@ -98,12 +139,13 @@ export default (
98
139
  Accept: 'application/json',
99
140
  Prefer: 'count=exact',
100
141
  ...(params.meta?.headers || {}),
142
+ ...useCustomSchema(config.schema, metaSchema, 'GET'),
101
143
  }),
102
144
  };
103
145
 
104
- const url = `${apiUrl}/${resource}?${qs.stringify(query)}`;
146
+ const url = `${config.apiUrl}/${resource}?${qs.stringify(query)}`;
105
147
 
106
- return httpClient(url, options).then(({ headers, json }) => {
148
+ return config.httpClient(url, options).then(({ headers, json }) => {
107
149
  if (!headers.has('content-range')) {
108
150
  throw new Error(
109
151
  `The Content-Range header is missing in the HTTP Response. The postgREST data provider expects
@@ -123,32 +165,44 @@ export default (
123
165
 
124
166
  getOne: (resource, params: Partial<GetOneParams> = {}) => {
125
167
  const { id, meta } = params;
126
- const primaryKey = getPrimaryKey(resource, primaryKeys);
168
+ const primaryKey = getPrimaryKey(resource, config.primaryKeys);
127
169
 
128
170
  const query = getQuery(primaryKey, id, resource, meta);
129
171
 
130
- const url = `${apiUrl}/${resource}?${qs.stringify(query)}`;
172
+ const url = `${config.apiUrl}/${resource}?${qs.stringify(query)}`;
173
+ const metaSchema = params.meta?.schema;
131
174
 
132
- return httpClient(url, {
133
- headers: new Headers({
134
- accept: 'application/vnd.pgrst.object+json',
135
- ...(params.meta?.headers || {}),
136
- }),
137
- }).then(({ json }) => ({
138
- data: dataWithId(json, primaryKey),
139
- }));
175
+ return config
176
+ .httpClient(url, {
177
+ headers: new Headers({
178
+ accept: 'application/vnd.pgrst.object+json',
179
+ ...(params.meta?.headers || {}),
180
+ ...useCustomSchema(config.schema, metaSchema, 'GET'),
181
+ }),
182
+ })
183
+ .then(({ json }) => ({
184
+ data: dataWithId(json, primaryKey),
185
+ }));
140
186
  },
141
187
 
142
188
  getMany: (resource, params: Partial<GetManyParams> = {}) => {
143
189
  const ids = params.ids;
144
- const primaryKey = getPrimaryKey(resource, primaryKeys);
190
+ const primaryKey = getPrimaryKey(resource, config.primaryKeys);
145
191
 
146
192
  const query = getQuery(primaryKey, ids, resource, params.meta);
147
- const url = `${apiUrl}/${resource}?${qs.stringify(query)}`;
148
193
 
149
- return httpClient(url).then(({ json }) => ({
150
- data: json.map(data => dataWithId(data, primaryKey)),
151
- }));
194
+ const url = `${config.apiUrl}/${resource}?${qs.stringify(query)}`;
195
+ const metaSchema = params.meta?.schema;
196
+
197
+ return config
198
+ .httpClient(url, {
199
+ headers: new Headers({
200
+ ...useCustomSchema(config.schema, metaSchema, 'GET'),
201
+ }),
202
+ })
203
+ .then(({ json }) => ({
204
+ data: json.map(data => dataWithId(data, primaryKey)),
205
+ }));
152
206
  },
153
207
 
154
208
  getManyReference: (
@@ -158,8 +212,10 @@ export default (
158
212
  const { page, perPage } = params.pagination;
159
213
  const { field, order } = params.sort;
160
214
 
161
- const { filter, select } = parseFilters(params, defaultListOp);
162
- const primaryKey = getPrimaryKey(resource, primaryKeys);
215
+ const { filter, select } = parseFilters(params, config.defaultListOp);
216
+ const primaryKey = getPrimaryKey(resource, config.primaryKeys);
217
+
218
+ const metaSchema = params.meta?.schema;
163
219
 
164
220
  let query = params.target
165
221
  ? {
@@ -186,12 +242,13 @@ export default (
186
242
  Accept: 'application/json',
187
243
  Prefer: 'count=exact',
188
244
  ...(params.meta?.headers || {}),
245
+ ...useCustomSchema(config.schema, metaSchema, 'GET'),
189
246
  }),
190
247
  };
191
248
 
192
- const url = `${apiUrl}/${resource}?${qs.stringify(query)}`;
249
+ const url = `${config.apiUrl}/${resource}?${qs.stringify(query)}`;
193
250
 
194
- return httpClient(url, options).then(({ headers, json }) => {
251
+ return config.httpClient(url, options).then(({ headers, json }) => {
195
252
  if (!headers.has('content-range')) {
196
253
  throw new Error(
197
254
  `The Content-Range header is missing in the HTTP Response. The postgREST data provider expects
@@ -211,34 +268,38 @@ export default (
211
268
 
212
269
  update: (resource, params: Partial<UpdateParams> = {}) => {
213
270
  const { id, data, meta } = params;
214
- const primaryKey = getPrimaryKey(resource, primaryKeys);
271
+ const primaryKey = getPrimaryKey(resource, config.primaryKeys);
215
272
 
216
273
  const query = getQuery(primaryKey, id, resource, meta);
217
274
 
218
275
  const primaryKeyData = getKeyData(primaryKey, data);
219
276
 
220
- const url = `${apiUrl}/${resource}?${qs.stringify(query)}`;
277
+ const url = `${config.apiUrl}/${resource}?${qs.stringify(query)}`;
278
+ const metaSchema = params.meta?.schema;
221
279
 
222
280
  const body = JSON.stringify({
223
281
  ...data,
224
282
  ...primaryKeyData,
225
283
  });
226
284
 
227
- return httpClient(url, {
228
- method: 'PATCH',
229
- headers: new Headers({
230
- Accept: 'application/vnd.pgrst.object+json',
231
- Prefer: 'return=representation',
232
- 'Content-Type': 'application/json',
233
- ...(params.meta?.headers || {}),
234
- }),
235
- body,
236
- }).then(({ json }) => ({ data: dataWithId(json, primaryKey) }));
285
+ return config
286
+ .httpClient(url, {
287
+ method: 'PATCH',
288
+ headers: new Headers({
289
+ Accept: 'application/vnd.pgrst.object+json',
290
+ Prefer: 'return=representation',
291
+ 'Content-Type': 'application/json',
292
+ ...(params.meta?.headers || {}),
293
+ ...useCustomSchema(config.schema, metaSchema, 'PATCH'),
294
+ }),
295
+ body,
296
+ })
297
+ .then(({ json }) => ({ data: dataWithId(json, primaryKey) }));
237
298
  },
238
299
 
239
300
  updateMany: (resource, params: Partial<UpdateManyParams> = {}) => {
240
301
  const ids = params.ids;
241
- const primaryKey = getPrimaryKey(resource, primaryKeys);
302
+ const primaryKey = getPrimaryKey(resource, config.primaryKeys);
242
303
 
243
304
  const body = JSON.stringify(
244
305
  params.ids.map(id => {
@@ -261,78 +322,94 @@ export default (
261
322
  })
262
323
  );
263
324
 
264
- const url = `${apiUrl}/${resource}`;
265
-
266
- return httpClient(url, {
267
- method: 'PATCH',
268
- headers: new Headers({
269
- Prefer: 'return=representation',
270
- 'Content-Type': 'application/json',
271
- ...(params.meta?.headers || {}),
272
- }),
273
- body,
274
- }).then(({ json }) => ({
275
- data: json.map(data => encodeId(data, primaryKey)),
276
- }));
325
+ const url = `${config.apiUrl}/${resource}`;
326
+ const metaSchema = params.meta?.schema;
327
+
328
+ return config
329
+ .httpClient(url, {
330
+ method: 'PATCH',
331
+ headers: new Headers({
332
+ Prefer: 'return=representation',
333
+ 'Content-Type': 'application/json',
334
+ ...(params.meta?.headers || {}),
335
+ ...useCustomSchema(config.schema, metaSchema, 'PATCH'),
336
+ }),
337
+ body,
338
+ })
339
+ .then(({ json }) => ({
340
+ data: json.map(data => encodeId(data, primaryKey)),
341
+ }));
277
342
  },
278
343
 
279
344
  create: (resource, params: Partial<CreateParams> = {}) => {
280
- const primaryKey = getPrimaryKey(resource, primaryKeys);
281
- const url = `${apiUrl}/${resource}`;
282
-
283
- return httpClient(url, {
284
- method: 'POST',
285
- headers: new Headers({
286
- Accept: 'application/vnd.pgrst.object+json',
287
- Prefer: 'return=representation',
288
- 'Content-Type': 'application/json',
289
- ...(params.meta?.headers || {}),
290
- }),
291
- body: JSON.stringify(params.data),
292
- }).then(({ json }) => ({
293
- data: {
294
- ...json,
295
- id: encodeId(json, primaryKey),
296
- },
297
- }));
345
+ const primaryKey = getPrimaryKey(resource, config.primaryKeys);
346
+ const url = `${config.apiUrl}/${resource}`;
347
+ const metaSchema = params.meta?.schema;
348
+
349
+ return config
350
+ .httpClient(url, {
351
+ method: 'POST',
352
+ headers: new Headers({
353
+ Accept: 'application/vnd.pgrst.object+json',
354
+ Prefer: 'return=representation',
355
+ 'Content-Type': 'application/json',
356
+ ...(params.meta?.headers || {}),
357
+ ...useCustomSchema(config.schema, metaSchema, 'POST'),
358
+ }),
359
+ body: JSON.stringify(params.data),
360
+ })
361
+ .then(({ json }) => ({
362
+ data: {
363
+ ...json,
364
+ id: encodeId(json, primaryKey),
365
+ },
366
+ }));
298
367
  },
299
368
 
300
369
  delete: (resource, params: Partial<DeleteParams> = {}) => {
301
370
  const { id, meta } = params;
302
- const primaryKey = getPrimaryKey(resource, primaryKeys);
371
+ const primaryKey = getPrimaryKey(resource, config.primaryKeys);
303
372
 
304
373
  const query = getQuery(primaryKey, id, resource, meta);
305
374
 
306
- const url = `${apiUrl}/${resource}?${qs.stringify(query)}`;
307
-
308
- return httpClient(url, {
309
- method: 'DELETE',
310
- headers: new Headers({
311
- Accept: 'application/vnd.pgrst.object+json',
312
- Prefer: 'return=representation',
313
- 'Content-Type': 'application/json',
314
- ...(params.meta?.headers || {}),
315
- }),
316
- }).then(({ json }) => ({ data: dataWithId(json, primaryKey) }));
375
+ const url = `${config.apiUrl}/${resource}?${qs.stringify(query)}`;
376
+ const metaSchema = params.meta?.schema;
377
+
378
+ return config
379
+ .httpClient(url, {
380
+ method: 'DELETE',
381
+ headers: new Headers({
382
+ Accept: 'application/vnd.pgrst.object+json',
383
+ Prefer: 'return=representation',
384
+ 'Content-Type': 'application/json',
385
+ ...(params.meta?.headers || {}),
386
+ ...useCustomSchema(config.schema, metaSchema, 'DELETE'),
387
+ }),
388
+ })
389
+ .then(({ json }) => ({ data: dataWithId(json, primaryKey) }));
317
390
  },
318
391
 
319
392
  deleteMany: (resource, params: Partial<DeleteManyParams> = {}) => {
320
393
  const { ids, meta } = params;
321
- const primaryKey = getPrimaryKey(resource, primaryKeys);
394
+ const primaryKey = getPrimaryKey(resource, config.primaryKeys);
322
395
 
323
396
  const query = getQuery(primaryKey, ids, resource, meta);
324
397
 
325
- const url = `${apiUrl}/${resource}?${qs.stringify(query)}`;
326
-
327
- return httpClient(url, {
328
- method: 'DELETE',
329
- headers: new Headers({
330
- Prefer: 'return=representation',
331
- 'Content-Type': 'application/json',
332
- ...(params.meta?.headers || {}),
333
- }),
334
- }).then(({ json }) => ({
335
- data: json.map(data => encodeId(data, primaryKey)),
336
- }));
398
+ const url = `${config.apiUrl}/${resource}?${qs.stringify(query)}`;
399
+ const metaSchema = params.meta?.schema;
400
+
401
+ return config
402
+ .httpClient(url, {
403
+ method: 'DELETE',
404
+ headers: new Headers({
405
+ Prefer: 'return=representation',
406
+ 'Content-Type': 'application/json',
407
+ ...(params.meta?.headers || {}),
408
+ ...useCustomSchema(config.schema, metaSchema, 'DELETE'),
409
+ }),
410
+ })
411
+ .then(({ json }) => ({
412
+ data: json.map(data => encodeId(data, primaryKey)),
413
+ }));
337
414
  },
338
415
  });
package/src/urlBuilder.ts CHANGED
@@ -72,21 +72,51 @@ export const parseFilters = (
72
72
  if (['like', 'ilike'].includes(operation)) {
73
73
  // we split the search term in words
74
74
  values = filter[key].trim().split(/\s+/);
75
+ }
76
+ // CASE: Logical operator
77
+ else if (['or', 'and'].includes(operation)) {
78
+ // we extract each value entries and make it dot separated string.
79
+ const { filter: subFilter } = parseFilters(
80
+ { filter: filter[key] },
81
+ defaultListOp
82
+ );
83
+
84
+ // something like { "age@lt": 18 } and { "age@gt": 21 }
85
+ const filterExpressions: string[] = [];
86
+ Object.entries(subFilter).forEach(([op, val]) => {
87
+ if (Array.isArray(val))
88
+ filterExpressions.push(...val.map(v => [op, v].join('.')));
89
+ else filterExpressions.push([op, val].join('.'));
90
+ });
91
+
92
+ // finally we flatten all as single string and enclose with bracket.
93
+ values = [`(${filterExpressions.join(',')})`];
75
94
  } else {
76
95
  values = [filter[key]];
77
96
  }
78
97
 
79
98
  values.forEach(value => {
80
- // if operator is intentionally blank, rpc syntax
81
- let op = operation.includes('like')
82
- ? `${operation}.*${value}*`
83
- : operation.length == 0
84
- ? `${value}`
85
- : `${operation}.${value}`;
99
+ let op: string = (() => {
100
+ // if operator is intentionally blank, rpc syntax
101
+ if (operation.length === 0) return `${value}`;
86
102
 
103
+ if (operation.includes('like'))
104
+ return `${operation}.*${value}*`;
105
+ if (['and', 'or'].includes(operation)) return `${value}`;
106
+ if (['cs', 'cd'].includes(operation))
107
+ return `${operation}.{${value}}`;
108
+ return `${operation}.${value}`;
109
+ })();
110
+
111
+ // If resulting filter haven't contains the fieldname, then add it.
87
112
  if (result.filter[splitKey[0]] === undefined) {
88
- // first operator for the key, we add it to the dict
89
- result.filter[splitKey[0]] = op;
113
+ // first operator for the key,
114
+ if (['and', 'or'].includes(operation)) {
115
+ result.filter[operation] = op;
116
+ } else {
117
+ // we add it to the dict
118
+ result.filter[splitKey[0]] = op;
119
+ }
90
120
  } else {
91
121
  if (!Array.isArray(result[splitKey[0]])) {
92
122
  // second operator, we transform to an array