@raphiniert/ra-data-postgrest 2.0.0 → 2.2.0

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/urlBuilder.ts CHANGED
@@ -1,311 +1,320 @@
1
- import { Identifier } from 'ra-core';
2
-
3
- /**
4
- * Interface to clearly seperate the operator like < oder >
5
- * from the name and the value of the filter
6
- */
7
- interface PostgRestFilter {
8
- name: string;
9
- operator?: PostgRestOperator;
10
- value: string;
11
- }
12
-
13
- /**
14
- * List of all possible operators in PostgRest
15
- * (https://postgrest.org/en/stable/api.html#operators)
16
- */
17
- const postgrestOperators = [
18
- 'eq',
19
- 'gt',
20
- 'gte',
21
- 'lt',
22
- 'lte',
23
- 'neq',
24
- 'like',
25
- 'ilike',
26
- 'match',
27
- 'imatch',
28
- 'in',
29
- 'is',
30
- 'fts',
31
- 'plfts',
32
- 'phfts',
33
- 'wfts',
34
- 'cs',
35
- 'cd',
36
- 'ov',
37
- 'sl',
38
- 'sr',
39
- 'nxr',
40
- 'nxl',
41
- 'adj',
42
- 'not',
43
- 'or',
44
- 'and',
45
- ] as const;
46
-
47
- type ParsedFiltersResult = {
48
- filter: any;
49
- select: any;
50
- };
51
-
52
- const isObject = obj =>
53
- typeof obj === 'object' && !Array.isArray(obj) && obj !== null;
54
-
55
- const resolveKeys = (filter: any, keys: Array<any>) => {
56
- let result = filter[keys[0]];
57
- for (let i = 1; i < keys.length; ++i) {
58
- result = result[keys[i]];
59
- }
60
-
61
- return result;
62
- };
63
-
64
- export type PostgRestOperator = (typeof postgrestOperators)[number];
65
-
66
- export const parseFilters = (
67
- params: any,
68
- defaultListOp: PostgRestOperator
69
- ): Partial<ParsedFiltersResult> => {
70
- const { filter, meta = {} } = params;
71
-
72
- let result: Partial<ParsedFiltersResult> = {};
73
- result.filter = {};
74
- Object.keys(filter).forEach(function (key) {
75
- // key: the name of the object key
76
-
77
- let keyArray = [key];
78
- let values: Array<string>;
79
-
80
- // in case of a nested object selection in a text field
81
- // RA returns object{ object } structure which is resolved here
82
- // see issue https://github.com/raphiniert-com/ra-data-postgrest/issues/58
83
- if (key.split('@')[0] !== '' && isObject(filter[key])) {
84
- let innerVal = filter[key];
85
-
86
- do {
87
- const inner = resolveKeys(filter, keyArray);
88
- const [innerKey] = Object.keys(inner);
89
-
90
- keyArray.push(innerKey);
91
- innerVal = inner[innerKey];
92
- } while (isObject(innerVal));
93
-
94
- key = keyArray.join('.');
95
- values = [innerVal];
96
- } else {
97
- values = [filter[key]];
98
- }
99
-
100
- const splitKey = key.split('@');
101
- const operation: PostgRestOperator =
102
- splitKey.length == 2
103
- ? (splitKey[1] as PostgRestOperator)
104
- : defaultListOp;
105
-
106
- if (['like', 'ilike'].includes(operation)) {
107
- // we split the search term in words
108
- values = resolveKeys(filter, keyArray).trim().split(/\s+/);
109
- }
110
- // CASE: Logical operator
111
- else if (['or', 'and'].includes(operation)) {
112
- // we extract each value entries and make it dot separated string.
113
- const { filter: subFilter } = parseFilters(
114
- { filter: resolveKeys(filter, keyArray) },
115
- defaultListOp
116
- );
117
-
118
- // something like { "age@lt": 18 } and { "age@gt": 21 }
119
- const filterExpressions: string[] = [];
120
- Object.entries(subFilter).forEach(([op, val]) => {
121
- if (Array.isArray(val))
122
- filterExpressions.push(...val.map(v => [op, v].join('.')));
123
- else filterExpressions.push([op, val].join('.'));
124
- });
125
-
126
- // finally we flatten all as single string and enclose with bracket.
127
- values = [`(${filterExpressions.join(',')})`];
128
- }
129
-
130
- values.forEach(value => {
131
- let op: string = (() => {
132
- // if operator is intentionally blank, rpc syntax
133
- if (operation.length === 0) return `${value}`;
134
-
135
- if (operation.includes('like'))
136
- return `${operation}.*${value}*`;
137
- if (['and', 'or'].includes(operation)) return `${value}`;
138
- return `${operation}.${value}`;
139
- })();
140
-
141
- // If resulting filter doesn't contain the fieldname, then add it.
142
- if (result.filter[splitKey[0]] === undefined) {
143
- // first operator for the key,
144
- if (['and', 'or'].includes(operation)) {
145
- result.filter[operation] = op;
146
- } else {
147
- // we add it to the dict
148
- result.filter[splitKey[0]] = op;
149
- }
150
- } else {
151
- if (!Array.isArray(result[splitKey[0]])) {
152
- // second operator, we transform to an array
153
- result.filter[splitKey[0]] = [
154
- result.filter[splitKey[0]],
155
- op,
156
- ];
157
- } else {
158
- // third and subsequent, we add to array
159
- result.filter[splitKey[0]].push(op);
160
- }
161
- }
162
- });
163
- });
164
-
165
- if (meta?.columns) {
166
- result.select = Array.isArray(meta.columns)
167
- ? meta.columns.join(',')
168
- : meta.columns;
169
- }
170
-
171
- return result;
172
- };
173
-
174
- // compound keys capability
175
- export type PrimaryKey = Array<string>;
176
-
177
- export const getPrimaryKey = (
178
- resource: string,
179
- primaryKeys: Map<string, PrimaryKey>
180
- ) => {
181
- return primaryKeys.get(resource) || ['id'];
182
- };
183
-
184
- export const decodeId = (
185
- id: Identifier,
186
- primaryKey: PrimaryKey
187
- ): string[] | number[] => {
188
- if (isCompoundKey(primaryKey)) {
189
- return JSON.parse(id.toString());
190
- } else {
191
- return [id.toString()];
192
- }
193
- };
194
-
195
- export const encodeId = (data: any, primaryKey: PrimaryKey): Identifier => {
196
- if (isCompoundKey(primaryKey)) {
197
- return JSON.stringify(primaryKey.map(key => data[key]));
198
- } else {
199
- return data[primaryKey[0]];
200
- }
201
- };
202
-
203
- export const removePrimaryKey = (data: any, primaryKey: PrimaryKey) => {
204
- const newData = { ...data };
205
- primaryKey.forEach(key => {
206
- delete newData[key];
207
- });
208
- return newData;
209
- };
210
-
211
- export const dataWithVirtualId = (data: any, primaryKey: PrimaryKey) => {
212
- if (primaryKey.length === 1 && primaryKey[0] === 'id') {
213
- return data;
214
- }
215
-
216
- return Object.assign(data, {
217
- id: encodeId(data, primaryKey),
218
- });
219
- };
220
-
221
- export const dataWithoutVirtualId = (data: any, primaryKey: PrimaryKey) => {
222
- if (primaryKey.length === 1 && primaryKey[0] === 'id') {
223
- return data;
224
- }
225
-
226
- const { id, ...dataWithoutId } = data;
227
- return dataWithoutId;
228
- };
229
-
230
- const isCompoundKey = (primaryKey: PrimaryKey): Boolean => {
231
- return primaryKey.length > 1;
232
- };
233
-
234
- export const getQuery = (
235
- primaryKey: PrimaryKey,
236
- ids: Identifier | Array<Identifier> | undefined,
237
- resource: string,
238
- meta: any = null
239
- ): any => {
240
- let result: any = {};
241
- if (Array.isArray(ids) && ids.length > 1) {
242
- // no standardized query with multiple ids possible for rpc endpoints which are api-exposed database functions
243
- if (resource.startsWith('rpc/')) {
244
- console.error(
245
- "PostgREST's rpc endpoints are not intended to be handled as views. Therefore, no query generation for multiple key values implemented!"
246
- );
247
-
248
- return;
249
- }
250
-
251
- if (isCompoundKey(primaryKey)) {
252
- result = {
253
- or: `(${ids.map(id => {
254
- const primaryKeyParams = decodeId(id, primaryKey);
255
- return `and(${primaryKey
256
- .map((key, i) => `${key}.eq.${primaryKeyParams[i]}`)
257
- .join(',')})`;
258
- })})`,
259
- };
260
- } else {
261
- result = {
262
- [primaryKey[0]]: `in.(${ids.join(',')})`,
263
- };
264
- }
265
- } else if (ids) {
266
- // if ids is one Identifier
267
- const id: Identifier = ids.toString();
268
- const primaryKeyParams = decodeId(id, primaryKey);
269
-
270
- if (isCompoundKey(primaryKey)) {
271
- if (resource.startsWith('rpc/')) {
272
- result = {};
273
- primaryKey.map(
274
- (key: string, i: any) =>
275
- (result[key] = `${primaryKeyParams[i]}`)
276
- );
277
- } else {
278
- result = {
279
- and: `(${primaryKey.map(
280
- (key: string, i: any) =>
281
- `${key}.eq.${primaryKeyParams[i]}`
282
- )})`,
283
- };
284
- }
285
- } else {
286
- result = {
287
- [primaryKey[0]]: `eq.${id}`,
288
- };
289
- }
290
- }
291
-
292
- if (meta && meta.columns) {
293
- result.select = Array.isArray(meta.columns)
294
- ? meta.columns.join(',')
295
- : meta.columns;
296
- }
297
-
298
- return result;
299
- };
300
-
301
- export const getOrderBy = (
302
- field: string,
303
- order: string,
304
- primaryKey: PrimaryKey
305
- ) => {
306
- if (field == 'id') {
307
- return primaryKey.map(key => `${key}.${order.toLowerCase()}`).join(',');
308
- } else {
309
- return `${field}.${order.toLowerCase()}`;
310
- }
311
- };
1
+ import { Identifier } from 'ra-core';
2
+
3
+ /**
4
+ * Interface to clearly seperate the operator like < oder >
5
+ * from the name and the value of the filter
6
+ */
7
+ interface PostgRestFilter {
8
+ name: string;
9
+ operator?: PostgRestOperator;
10
+ value: string;
11
+ }
12
+
13
+ /**
14
+ * List of all possible operators in PostgRest
15
+ * (https://postgrest.org/en/stable/api.html#operators)
16
+ */
17
+ const postgrestOperators = [
18
+ 'eq',
19
+ 'gt',
20
+ 'gte',
21
+ 'lt',
22
+ 'lte',
23
+ 'neq',
24
+ 'like',
25
+ 'ilike',
26
+ 'match',
27
+ 'imatch',
28
+ 'in',
29
+ 'is',
30
+ 'fts',
31
+ 'plfts',
32
+ 'phfts',
33
+ 'wfts',
34
+ 'cs',
35
+ 'cd',
36
+ 'ov',
37
+ 'sl',
38
+ 'sr',
39
+ 'nxr',
40
+ 'nxl',
41
+ 'adj',
42
+ 'not',
43
+ 'or',
44
+ 'and',
45
+ ] as const;
46
+
47
+ type ParsedFiltersResult = {
48
+ filter: any;
49
+ select: any;
50
+ };
51
+
52
+ const isObject = obj =>
53
+ typeof obj === 'object' && !Array.isArray(obj) && obj !== null;
54
+
55
+ const resolveKeys = (filter: any, keys: Array<any>) => {
56
+ let result = filter[keys[0]];
57
+ for (let i = 1; i < keys.length; ++i) {
58
+ result = result[keys[i]];
59
+ }
60
+
61
+ return result;
62
+ };
63
+
64
+ export type PostgRestOperator = (typeof postgrestOperators)[number];
65
+
66
+ export const parseFilters = (
67
+ params: any,
68
+ defaultListOp: PostgRestOperator
69
+ ): Partial<ParsedFiltersResult> => {
70
+ const { filter, meta = {} } = params;
71
+
72
+ let result: Partial<ParsedFiltersResult> = {};
73
+ result.filter = {};
74
+ Object.keys(filter).forEach(function (key) {
75
+ // key: the name of the object key
76
+
77
+ let keyArray = [key];
78
+ let values: Array<string>;
79
+
80
+ // in case of a nested object selection in a text field
81
+ // RA returns object{ object } structure which is resolved here
82
+ // see issue https://github.com/raphiniert-com/ra-data-postgrest/issues/58
83
+ if (key.split('@')[0] !== '' && isObject(filter[key])) {
84
+ let innerVal = filter[key];
85
+
86
+ do {
87
+ const inner = resolveKeys(filter, keyArray);
88
+ const [innerKey] = Object.keys(inner);
89
+
90
+ keyArray.push(innerKey);
91
+ innerVal = inner[innerKey];
92
+ } while (isObject(innerVal));
93
+
94
+ key = keyArray.join('.');
95
+ values = [innerVal];
96
+ } else {
97
+ values = [filter[key]];
98
+ }
99
+
100
+ const splitKey = key.split('@');
101
+ const operation: PostgRestOperator =
102
+ splitKey.length == 2
103
+ ? (splitKey[1] as PostgRestOperator)
104
+ : defaultListOp;
105
+
106
+ if (['like', 'ilike'].includes(operation)) {
107
+ // we split the search term in words
108
+ values = resolveKeys(filter, keyArray).trim().split(/\s+/);
109
+ }
110
+ // CASE: Logical operator
111
+ else if (['or', 'and'].includes(operation)) {
112
+ // we extract each value entries and make it dot separated string.
113
+ const { filter: subFilter } = parseFilters(
114
+ { filter: resolveKeys(filter, keyArray) },
115
+ defaultListOp
116
+ );
117
+
118
+ // something like { "age@lt": 18 } and { "age@gt": 21 }
119
+ const filterExpressions: string[] = [];
120
+ Object.entries(subFilter).forEach(([op, val]) => {
121
+ if (Array.isArray(val))
122
+ filterExpressions.push(...val.map(v => [op, v].join('.')));
123
+ else filterExpressions.push([op, val].join('.'));
124
+ });
125
+
126
+ // finally we flatten all as single string and enclose with bracket.
127
+ values = [`(${filterExpressions.join(',')})`];
128
+ }
129
+
130
+ values.forEach(value => {
131
+ let op: string = (() => {
132
+ // if operator is intentionally blank, rpc syntax
133
+ if (operation.length === 0) return `${value}`;
134
+
135
+ if (operation.includes('like'))
136
+ return `${operation}.*${value}*`;
137
+ if (['and', 'or'].includes(operation)) return `${value}`;
138
+ return `${operation}.${value}`;
139
+ })();
140
+
141
+ // If resulting filter doesn't contain the fieldname, then add it.
142
+ if (result.filter[splitKey[0]] === undefined) {
143
+ // first operator for the key,
144
+ if (['and', 'or'].includes(operation)) {
145
+ result.filter[operation] = op;
146
+ } else {
147
+ // we add it to the dict
148
+ result.filter[splitKey[0]] = op;
149
+ }
150
+ } else {
151
+ if (!Array.isArray(result[splitKey[0]])) {
152
+ // second operator, we transform to an array
153
+ result.filter[splitKey[0]] = [
154
+ result.filter[splitKey[0]],
155
+ op,
156
+ ];
157
+ } else {
158
+ // third and subsequent, we add to array
159
+ result.filter[splitKey[0]].push(op);
160
+ }
161
+ }
162
+ });
163
+ });
164
+
165
+ if (meta?.columns) {
166
+ result.select = Array.isArray(meta.columns)
167
+ ? meta.columns.join(',')
168
+ : meta.columns;
169
+ }
170
+
171
+ return result;
172
+ };
173
+
174
+ // compound keys capability
175
+ export type PrimaryKey = Array<string>;
176
+
177
+ export const getPrimaryKey = (
178
+ resource: string,
179
+ primaryKeys: Map<string, PrimaryKey>
180
+ ) => {
181
+ return primaryKeys.get(resource) || ['id'];
182
+ };
183
+
184
+ export const decodeId = (
185
+ id: Identifier,
186
+ primaryKey: PrimaryKey
187
+ ): string[] | number[] => {
188
+ if (isCompoundKey(primaryKey)) {
189
+ return JSON.parse(id.toString());
190
+ } else {
191
+ return [id.toString()];
192
+ }
193
+ };
194
+
195
+ export const encodeId = (data: any, primaryKey: PrimaryKey): Identifier => {
196
+ if (isCompoundKey(primaryKey)) {
197
+ return JSON.stringify(primaryKey.map(key => data[key]));
198
+ } else {
199
+ return data[primaryKey[0]];
200
+ }
201
+ };
202
+
203
+ export const removePrimaryKey = (data: any, primaryKey: PrimaryKey) => {
204
+ const newData = { ...data };
205
+ primaryKey.forEach(key => {
206
+ delete newData[key];
207
+ });
208
+ return newData;
209
+ };
210
+
211
+ export const dataWithVirtualId = (data: any, primaryKey: PrimaryKey) => {
212
+ if (primaryKey.length === 1 && primaryKey[0] === 'id') {
213
+ return data;
214
+ }
215
+
216
+ return Object.assign(data, {
217
+ id: encodeId(data, primaryKey),
218
+ });
219
+ };
220
+
221
+ export const dataWithoutVirtualId = (data: any, primaryKey: PrimaryKey) => {
222
+ if (primaryKey.length === 1 && primaryKey[0] === 'id') {
223
+ return data;
224
+ }
225
+
226
+ const { id, ...dataWithoutId } = data;
227
+ return dataWithoutId;
228
+ };
229
+
230
+ const isCompoundKey = (primaryKey: PrimaryKey): Boolean => {
231
+ return primaryKey.length > 1;
232
+ };
233
+
234
+ export const getQuery = (
235
+ primaryKey: PrimaryKey,
236
+ ids: Identifier | Array<Identifier> | undefined,
237
+ resource: string,
238
+ meta: any = null
239
+ ): any => {
240
+ let result: any = {};
241
+ if (Array.isArray(ids) && ids.length > 1) {
242
+ // no standardized query with multiple ids possible for rpc endpoints which are api-exposed database functions
243
+ if (resource.startsWith('rpc/')) {
244
+ console.error(
245
+ "PostgREST's rpc endpoints are not intended to be handled as views. Therefore, no query generation for multiple key values implemented!"
246
+ );
247
+
248
+ return;
249
+ }
250
+
251
+ if (isCompoundKey(primaryKey)) {
252
+ result = {
253
+ or: `(${ids.map(id => {
254
+ const primaryKeyParams = decodeId(id, primaryKey);
255
+ return `and(${primaryKey
256
+ .map((key, i) => `${key}.eq.${primaryKeyParams[i]}`)
257
+ .join(',')})`;
258
+ })})`,
259
+ };
260
+ } else {
261
+ result = {
262
+ [primaryKey[0]]: `in.(${ids.join(',')})`,
263
+ };
264
+ }
265
+ } else if (ids) {
266
+ // if ids is one Identifier
267
+ const id: Identifier = ids.toString();
268
+ const primaryKeyParams = decodeId(id, primaryKey);
269
+
270
+ if (isCompoundKey(primaryKey)) {
271
+ if (resource.startsWith('rpc/')) {
272
+ result = {};
273
+ primaryKey.map(
274
+ (key: string, i: any) =>
275
+ (result[key] = `${primaryKeyParams[i]}`)
276
+ );
277
+ } else {
278
+ result = {
279
+ and: `(${primaryKey.map(
280
+ (key: string, i: any) =>
281
+ `${key}.eq.${primaryKeyParams[i]}`
282
+ )})`,
283
+ };
284
+ }
285
+ } else {
286
+ result = {
287
+ [primaryKey[0]]: `eq.${id}`,
288
+ };
289
+ }
290
+ }
291
+
292
+ if (meta && meta.columns) {
293
+ result.select = Array.isArray(meta.columns)
294
+ ? meta.columns.join(',')
295
+ : meta.columns;
296
+ }
297
+
298
+ return result;
299
+ };
300
+
301
+ export enum PostgRestSortOrder {
302
+ AscendingNullsLastDescendingNullsFirst = 'asc,desc',
303
+ AscendingNullsLastDescendingNullsLast = 'asc,desc.nullslast',
304
+ AscendingNullsFirstDescendingNullsFirst = 'asc.nullsfirst,desc',
305
+ AscendingNullsFirstDescendingNullsLast = 'asc.nullsfirst,desc.nullslast',
306
+ }
307
+
308
+ export const getOrderBy = (
309
+ field: string,
310
+ order: string,
311
+ primaryKey: PrimaryKey,
312
+ sortOrder: PostgRestSortOrder = PostgRestSortOrder.AscendingNullsLastDescendingNullsFirst
313
+ ) => {
314
+ const postgRestOrder = sortOrder.split(',')[order === 'ASC' ? 0 : 1];
315
+ if (field == 'id') {
316
+ return primaryKey.map(key => `${key}.${postgRestOrder}`).join(',');
317
+ } else {
318
+ return `${field}.${postgRestOrder}`;
319
+ }
320
+ };