Skip to content

Commit 146cd29

Browse files
authored
Merge pull request #60 from rezo-labs/feat/more_ops
New ops for array & string & json
2 parents e125f7d + 9e080ad commit 146cd29

File tree

7 files changed

+414
-22
lines changed

7 files changed

+414
-22
lines changed

README.md

+51-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ npm i directus-extension-computed-interface
2626
- **Read Only**: Show an input with the computed value and disallow manual editing.
2727
3. **Prefix**: a string to prefix the computed value.
2828
4. **Suffix**: a string to suffix the computed value.
29-
5. **Custom CSS**: an object for inline style binding. Only works with **Display Only** and **Read Only** mode. You can use this option to customize the appearance of the computed value such as font size, color, etc.
29+
5. **Custom CSS**: a JSON object for inline style binding. Only works with **Display Only** and **Read Only** mode. You can use this option to customize the appearance of the computed value such as font size, color, etc. Example: `{"color": "red", "font-size": "20px"}`.
3030
6. **Debug Mode**: Used for debugging the template. It will show an error message if the template is invalid. It will also log to console the result of each component of the template.
3131
7. **Compute If Empty**: Compute the value if the field is empty. This is useful if you want a value to be computed once such as the created date or a unique ID.
3232
8. **Initial Compute**: Compute the value when opening the form. This is useful if you want to compute a value based on the current date or other dynamic values.
@@ -68,6 +68,19 @@ Literal strings are enclosed by double quotes (`"`):
6868
{{ CONCAT(file, ".txt") }}
6969
```
7070

71+
Use `.` to access nested fields in M2O or M2M fields:
72+
```
73+
{{ CONCAT(CONCAT(user.first_name, " "), user.last_name) }}
74+
```
75+
76+
Combine `AT`, `FIRST`, `LAST`, `JSON_GET` to access nested fields in O2M or JSON fields:
77+
```
78+
{{ JSON_GET(AT(products, 0), "name") }}
79+
{{ JSON_GET(LAST(products), "price") }}
80+
```
81+
82+
**Note**: For M2O, O2M, M2M fields, you can only access the fields of the direct relation. For example, if you have a `user` field that is a M2O relation to the `users` collection, you can only access the fields of the `users` collection. You cannot access the fields of the `roles` collection even though the `users` collection has a M2O relation to the `roles` collection. On the other hand, JSON fields have no such limitation!
83+
7184
## Available operators
7285

7386
### Type conversion
@@ -102,6 +115,7 @@ Operator | Description
102115
`MINUTES(a)` | get minutes of a date object, similar to `getMinutes`
103116
`SECONDS(a)` | get seconds of a date object, similar to `getSeconds`
104117
`TIME(a)` | get time of a date object, similar to `getTime`
118+
`LOCALE_STR(a, locale, options)` | transform date or date-like object to string with locale format, `options` is a stringified JSON object. Example: `LOCALE_STR("2023-01-01", "en-US", "{\"weekday\": \"long\", \"year\": \"numeric\", \"month\": \"long\", \"day\": \"numeric\"}")` returns "Sunday, January 1, 2023".
105119

106120
### Arithmetic
107121

@@ -132,7 +146,11 @@ Operator | Description
132146

133147
Operator | Description
134148
--- | ---
135-
`STR_LEN(str)` | length of string
149+
`STR_LEN(str)` | length of string (deprecated, use `LENGTH` instead)
150+
`LENGTH(str)` | length of string
151+
`FIRST(str)` | first character of string
152+
`LAST(str)` | last character of string
153+
`REVERSE(str)` | reverse string
136154
`LOWER(str)` | to lower case
137155
`UPPER(str)` | to upper case
138156
`TRIM(str)` | removes whitespace at the beginning and end of string.
@@ -147,6 +165,10 @@ Operator | Description
147165
`SEARCH(str, keyword)` | search `keyword` in `str` and return the position of the first occurrence. Return -1 if not found.
148166
`SEARCH(str, keyword, startAt)` | search `keyword` in `str` and return the position of the first occurrence after `startAt`. Return -1 if not found.
149167
`SUBSTITUTE(str, old, new)` | replace all occurrences of `old` in `str` with `new`.
168+
`AT(str, index)` | get character at `index` of `str`.
169+
`INDEX_OF(str, keyword)` | get the position of the first occurrence of `keyword` in `str`. Return -1 if not found.
170+
`INCLUDES(str, keyword)` | check if `str` contains `keyword`.
171+
`SLICE(str, startAt, endAt)` | extract a part of `str` from `startAt` to `endAt`. `endAt` can be negative. Similar to `slice` method of `String`.
150172

151173
### Boolean
152174

@@ -168,7 +190,27 @@ Operator | Description
168190

169191
Operator | Description
170192
--- | ---
171-
`ARRAY_LEN(a)` | length of array
193+
`ARRAY_LEN(a)` | length of array (deprecated, use `LENGTH` instead)
194+
`LENGTH(a)` | length of array
195+
`FIRST(a)` | first element of array
196+
`LAST(a)` | last element of array
197+
`REVERSE(a)` | reverse array
198+
`CONCAT(a, b)` | concat 2 arrays `a` and `b`.
199+
`AT(a, index)` | get element at `index` of `a`.
200+
`INDEX_OF(a, element)` | get the position of the first occurrence of `element` in `a`. Return -1 if not found.
201+
`INCLUDES(a, element)` | check if `a` contains `element`.
202+
`SLICE(a, startAt, endAt)` | extract a part of `a` from `startAt` to `endAt`. `endAt` can be negative. Similar to `slice` method of `Array`.
203+
`MAP(a, expression)` | apply `expression` to each element of `a` and return a new array, each element of `a` must be an object. Example: `MAP(products, MULTIPLY(price, quantity))` returns an array of total price of each product.
204+
`FILTER(a, expression)` | filter `a` with `expression` and return a new array, each element of `a` must be an object. Example: `FILTER(products, GT(stock, 0))` returns an array of products that are in stock.
205+
`SORT(a, expression)` | sort `a` with `expression` and return a new array, each element of `a` must be an object. Example: `SORT(products, price)` returns an array of products sorted by price.
206+
207+
### JSON
208+
209+
Operator | Description
210+
--- | ---
211+
`JSON_GET(a, key)` | get value of `key` in JSON object `a`.
212+
`JSON_PARSE(a)` | parse string `a` to JSON object.
213+
`JSON_STRINGIFY(a)` | stringify JSON object `a`.
172214

173215
### Relational
174216

@@ -190,6 +232,12 @@ Operator | Description
190232
`IF(A, B, C)` | return `B` if `A` is `true`, otherwise `C`
191233
`IFS(A1, B1, A2, B2, ..., An, Bn)` | return `Bi` if `Ai` is the first to be `true`, if none of `Ai` is `true`, return `null`
192234

235+
### Others
236+
237+
Operator | Description
238+
--- | ---
239+
`RANGE(start, end, step)` | create an array of numbers from `start` to `end` with `step` increment/decrement. Example: `RANGE(1, 10, 2)` returns `[1, 3, 5, 7, 9]`.
240+
193241
## Dynamic Variables
194242

195243
There are 2 dynamic variables available that you can use in the expressions:

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "directus-extension-computed-interface",
3-
"version": "1.8.2",
3+
"version": "1.9.0",
44
"description": "Perform computed value based on other fields",
55
"author": {
66
"email": "duydvu98@gmail.com",

screenshots/screenshot2.jpeg

28.9 KB
Loading

src/operations.test.ts

+212
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ describe('Test parseExpression', () => {
103103
test('TIME op', () => {
104104
expect(parseExpression('TIME($NOW)', {})).toBe(new Date().getTime());
105105
});
106+
107+
test('LOCALE_STR op', () => {
108+
expect(parseExpression('LOCALE_STR($NOW, "en-US", "{}")', {})).toBe('1/1/2023, 12:00:00 AM');
109+
expect(parseExpression('LOCALE_STR($NOW, "en-US", "{\\"month\\": \\"long\\"}")', {})).toBe('January');
110+
expect(parseExpression('LOCALE_STR($NOW, "en-US", "{\\"weekday\\": \\"long\\", \\"year\\": \\"numeric\\", \\"month\\": \\"long\\", \\"day\\": \\"numeric\\"}")', {})).toBe('Sunday, January 1, 2023');
111+
});
106112
});
107113

108114
describe('Arithmetic ops', () => {
@@ -262,6 +268,26 @@ describe('Test parseExpression', () => {
262268
expect(parseExpression('STR_LEN(a)', { a: 1 })).toBe(1);
263269
});
264270

271+
test('LENGTH op', () => {
272+
expect(parseExpression('LENGTH(a)', { a: '123' })).toBe(3);
273+
expect(parseExpression('LENGTH(a)', { a: 1 })).toBe(null);
274+
});
275+
276+
test('FIRST op', () => {
277+
expect(parseExpression('FIRST(a)', { a: '123' })).toBe('1');
278+
expect(parseExpression('FIRST(a)', { a: 1 })).toBe(null);
279+
});
280+
281+
test('LAST op', () => {
282+
expect(parseExpression('LAST(a)', { a: '123' })).toBe('3');
283+
expect(parseExpression('LAST(a)', { a: 1 })).toBe(null);
284+
});
285+
286+
test('REVERSE op', () => {
287+
expect(parseExpression('REVERSE(a)', { a: '123' })).toBe('321');
288+
expect(parseExpression('REVERSE(a)', { a: 1 })).toBe(null);
289+
});
290+
265291
test('LOWER op', () => {
266292
expect(parseExpression('LOWER(a)', { a: 'ABCDEF' })).toBe('abcdef');
267293
});
@@ -320,13 +346,190 @@ describe('Test parseExpression', () => {
320346
expect(parseExpression('SEARCH(a, "b", 3)', { a: 'abcabc' })).toBe(4);
321347
expect(parseExpression('SEARCH(a, "d")', { a: 'abcabc' })).toBe(-1);
322348
});
349+
350+
test('AT op', () => {
351+
expect(parseExpression('AT(a, 1)', { a: 'abc' })).toBe('b');
352+
expect(parseExpression('AT(a, 1)', { a: 1 })).toBe(null);
353+
});
354+
355+
test('INDEX_OF op', () => {
356+
expect(parseExpression('INDEX_OF(a, "b")', { a: 'abcabc' })).toBe(1);
357+
expect(parseExpression('INDEX_OF(a, "c")', { a: 'abcabc' })).toBe(2);
358+
expect(parseExpression('INDEX_OF(a, "d")', { a: 'abcabc' })).toBe(-1);
359+
expect(parseExpression('INDEX_OF(a, "b")', { a: 1 })).toBe(null);
360+
});
361+
362+
test('INCLUDES op', () => {
363+
expect(parseExpression('INCLUDES(a, "b")', { a: 'abcabc' })).toBe(true);
364+
expect(parseExpression('INCLUDES(a, "d")', { a: 'abcabc' })).toBe(false);
365+
expect(parseExpression('INCLUDES(a, "b")', { a: 1 })).toBe(null);
366+
});
367+
368+
test('SLICE op', () => {
369+
expect(parseExpression('SLICE(a, 1, 2)', { a: 'abcdef' })).toBe('b');
370+
expect(parseExpression('SLICE(a, 1, -1)', { a: 'abcdef' })).toBe('bcde');
371+
});
323372
});
324373

325374
describe('Array ops', () => {
326375
test('ARRAY_LEN op', () => {
327376
expect(parseExpression('ARRAY_LEN(a)', { a: [1, 2, 3] })).toBe(3);
328377
expect(parseExpression('ARRAY_LEN(a)', { a: 1 })).toBe(0);
329378
});
379+
380+
test('LENGTH op', () => {
381+
expect(parseExpression('LENGTH(a)', { a: [1, 2, 3] })).toBe(3);
382+
expect(parseExpression('LENGTH(a)', { a: 1 })).toBe(null);
383+
});
384+
385+
test('FIRST op', () => {
386+
expect(parseExpression('FIRST(a)', { a: [1, 2, 3] })).toBe(1);
387+
expect(parseExpression('FIRST(a)', { a: 1 })).toBe(null);
388+
});
389+
390+
test('LAST op', () => {
391+
expect(parseExpression('LAST(a)', { a: [1, 2, 3] })).toBe(3);
392+
expect(parseExpression('LAST(a)', { a: 1 })).toBe(null);
393+
});
394+
395+
test('REVERSE op', () => {
396+
expect(parseExpression('REVERSE(a)', { a: [1, 2, 3] })).toEqual([3, 2, 1]);
397+
expect(parseExpression('REVERSE(a)', { a: 1 })).toBe(null);
398+
});
399+
400+
test('CONCAT op', () => {
401+
expect(parseExpression('CONCAT(a, b)', { a: [1, 2], b: [3, 4] })).toEqual([1, 2, 3, 4]);
402+
expect(parseExpression('CONCAT(a, b)', { a: [1, 2], b: 3 })).toEqual([1, 2, 3]);
403+
});
404+
405+
test('AT op', () => {
406+
expect(parseExpression('AT(a, 1)', { a: [1, 2, 3] })).toBe(2);
407+
expect(parseExpression('AT(a, 1)', { a: 1 })).toBe(null);
408+
});
409+
410+
test('INDEX_OF op', () => {
411+
expect(parseExpression('INDEX_OF(a, 1)', { a: [1, 2, 3] })).toBe(0);
412+
expect(parseExpression('INDEX_OF(a, 2)', { a: [1, 2, 3] })).toBe(1);
413+
expect(parseExpression('INDEX_OF(a, 3)', { a: [1, 2, 3] })).toBe(2);
414+
expect(parseExpression('INDEX_OF(a, 4)', { a: [1, 2, 3] })).toBe(-1);
415+
expect(parseExpression('INDEX_OF(a, 2)', { a: 1 })).toBe(null);
416+
});
417+
418+
test('INCLUDES op', () => {
419+
expect(parseExpression('INCLUDES(a, 1)', { a: [1, 2, 3] })).toBe(true);
420+
expect(parseExpression('INCLUDES(a, 4)', { a: [1, 2, 3] })).toBe(false);
421+
expect(parseExpression('INCLUDES(a, 2)', { a: 1 })).toBe(null);
422+
});
423+
424+
test('SLICE op', () => {
425+
expect(parseExpression('SLICE(a, 1, 2)', { a: [1, 2, 3, 4] })).toEqual([2]);
426+
expect(parseExpression('SLICE(a, 1, -1)', { a: [1, 2, 3, 4] })).toEqual([2, 3]);
427+
});
428+
429+
test('MAP op', () => {
430+
const arr = [
431+
{
432+
a: 1,
433+
b: 'x',
434+
},
435+
{
436+
a: 2,
437+
b: 'y',
438+
},
439+
{
440+
a: 3,
441+
b: 'z',
442+
},
443+
];
444+
expect(parseExpression('MAP(arr, SUM(a, 1))', { arr })).toEqual([2, 3, 4]);
445+
expect(parseExpression('MAP(arr, CONCAT(b, "a"))', { arr })).toEqual(['xa', 'ya', 'za']);
446+
expect(parseExpression('MAP(arr, REPT(b, a))', { arr })).toEqual(['x', 'yy', 'zzz']);
447+
448+
const arr2 = [
449+
{
450+
a: {
451+
b: 1,
452+
},
453+
c: ['x'],
454+
},
455+
{
456+
a: {
457+
b: 2,
458+
},
459+
c: ['y'],
460+
},
461+
{
462+
a: {
463+
b: 3,
464+
},
465+
c: ['z'],
466+
},
467+
];
468+
469+
expect(parseExpression('MAP(arr2, SUM(a.b, 1))', { arr2 })).toEqual([2, 3, 4]);
470+
expect(parseExpression('MAP(arr2, CONCAT(FIRST(c), "a"))', { arr2 })).toEqual(['xa', 'ya', 'za']);
471+
expect(parseExpression('MAP(arr2, REPT(FIRST(c), a.b))', { arr2 })).toEqual(['x', 'yy', 'zzz']);
472+
});
473+
474+
test('FILTER op', () => {
475+
const arr = [
476+
{
477+
a: 1,
478+
b: 'x',
479+
},
480+
{
481+
a: 2,
482+
b: 'y',
483+
},
484+
{
485+
a: 3,
486+
b: 'z',
487+
},
488+
];
489+
expect(parseExpression('FILTER(arr, EQUAL(a, 1))', { arr })).toEqual([arr[0]]);
490+
expect(parseExpression('FILTER(arr, EQUAL(b, "y"))', { arr })).toEqual([arr[1]]);
491+
expect(parseExpression('FILTER(arr, LT(a, 3))', { arr })).toEqual([arr[0], arr[1]]);
492+
});
493+
494+
test('SORT op', () => {
495+
const arr = [
496+
{
497+
a: 2,
498+
b: 'y',
499+
},
500+
{
501+
a: 1,
502+
b: 'x',
503+
},
504+
{
505+
a: 3,
506+
b: 'z',
507+
},
508+
];
509+
expect(parseExpression('SORT(arr, MULTIPLY(a, -1))', { arr })).toEqual([arr[2], arr[0], arr[1]]);
510+
expect(parseExpression('SORT(arr, b)', { arr })).toEqual([arr[1], arr[0], arr[2]]);
511+
});
512+
});
513+
514+
describe('JSON ops', () => {
515+
test('JSON_PARSE op', () => {
516+
expect(parseExpression('JSON_PARSE(a)', { a: '{"a": 1}' })).toStrictEqual({ a: 1 });
517+
expect(parseExpression('JSON_PARSE("{\\"a\\": 1}")', {})).toStrictEqual({ a: 1 });
518+
expect(parseExpression('JSON_PARSE("{\\"a\\": {\\"b\\": \\"c\\"}}")', {})).toStrictEqual({ a: { b: 'c' } });
519+
expect(() => parseExpression('JSON_PARSE(a)', { a: '{"a": 1' })).toThrow(SyntaxError)
520+
});
521+
522+
test('JSON_STRINGIFY op', () => {
523+
expect(parseExpression('JSON_STRINGIFY(a)', { a: { a: 1 } })).toBe('{"a":1}');
524+
});
525+
526+
test('JSON_GET op', () => {
527+
expect(parseExpression('JSON_GET(a, "a")', { a: { a: 1 } })).toBe(1);
528+
expect(parseExpression('JSON_GET(a, "b")', { a: { a: 1 } })).toBe(null);
529+
expect(parseExpression('JSON_GET(AT(a, 0), "b")', { a: [{ b: 2 }] })).toBe(2);
530+
expect(parseExpression('JSON_GET(a, "a")', { a: 1 })).toBe(null);
531+
expect(parseExpression('JSON_GET(a, "a")', { a: null })).toBe(null);
532+
});
330533
});
331534

332535
describe('Relational ops', () => {
@@ -399,6 +602,15 @@ describe('Test parseExpression', () => {
399602
});
400603
});
401604

605+
describe('Other ops', () => {
606+
test('RANGE op', () => {
607+
expect(parseExpression('RANGE(a, b, c)', { a: 1, b: 5, c: 1 })).toEqual([1, 2, 3, 4, 5]);
608+
expect(parseExpression('RANGE(a, b, c)', { a: 5, b: 1, c: -1 })).toEqual([5, 4, 3, 2 ,1]);
609+
expect(parseExpression('RANGE(a, b, c)', { a: 1, b: 6, c: 2 })).toEqual([1, 3, 5]);
610+
expect(parseExpression('RANGE(a, b, c)', { a: 5, b: 0, c: -2 })).toEqual([5, 3, 1]);
611+
});
612+
});
613+
402614
describe('Nested expressions', () => {
403615
test('Simple nested numeric expression', () => {
404616
expect(parseExpression('SUM(a, MULTIPLY(b, c))', { a: 1, b: 2, c: 3 })).toBe(7);

0 commit comments

Comments
 (0)