Skip to content

Commit 76c04c7

Browse files
feat: selective response for admin and search api (#659)
* feat: selective response for admin and search api * chore: pr fixes * chore: pr fixes * feat: allow adding multiple fields and with_field values * test: fixed specs
1 parent 8cd9df3 commit 76c04c7

File tree

7 files changed

+175
-38
lines changed

7 files changed

+175
-38
lines changed

lib/api.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,21 @@ exports.resources = function resources(callback, options = {}) {
4545
if ((options.start_at != null) && Object.prototype.toString.call(options.start_at) === '[object Date]') {
4646
options.start_at = options.start_at.toUTCString();
4747
}
48-
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "prefix", "tags", "context", "direction", "moderations", "start_at", "metadata"), callback, options);
48+
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "prefix", "tags", "context", "direction", "moderations", "start_at", "metadata", "fields"), callback, options);
4949
};
5050

5151
exports.resources_by_tag = function resources_by_tag(tag, callback, options = {}) {
5252
let resource_type, uri;
5353
resource_type = options.resource_type || "image";
5454
uri = ["resources", resource_type, "tags", tag];
55-
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata"), callback, options);
55+
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata", "fields"), callback, options);
5656
};
5757

5858
exports.resources_by_context = function resources_by_context(key, value, callback, options = {}) {
5959
let params, resource_type, uri;
6060
resource_type = options.resource_type || "image";
6161
uri = ["resources", resource_type, "context"];
62-
params = pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata");
62+
params = pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata", "fields");
6363
params.key = key;
6464
if (value != null) {
6565
params.value = value;
@@ -71,7 +71,7 @@ exports.resources_by_moderation = function resources_by_moderation(kind, status,
7171
let resource_type, uri;
7272
resource_type = options.resource_type || "image";
7373
uri = ["resources", resource_type, "moderations", kind, status];
74-
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata"), callback, options);
74+
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata", "fields"), callback, options);
7575
};
7676

7777
exports.resource_by_asset_id = function resource_by_asset_id(asset_id, callback, options = {}) {
@@ -82,15 +82,15 @@ exports.resource_by_asset_id = function resource_by_asset_id(asset_id, callback,
8282
exports.resources_by_asset_folder = function resources_by_asset_folder(asset_folder, callback, options = {}) {
8383
let params, uri;
8484
uri = ["resources", 'by_asset_folder'];
85-
params = pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "moderations");
85+
params = pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "moderations", "fields");
8686
params.asset_folder = asset_folder;
8787
return call_api("get", uri, params, callback, options);
8888
};
8989

9090
exports.resources_by_asset_ids = function resources_by_asset_ids(asset_ids, callback, options = {}) {
9191
let params, uri;
9292
uri = ["resources", "by_asset_ids"];
93-
params = pickOnlyExistingValues(options, "tags", "context", "moderations");
93+
params = pickOnlyExistingValues(options, "tags", "context", "moderations", "fields");
9494
params["asset_ids[]"] = asset_ids;
9595
return call_api("get", uri, params, callback, options);
9696
}
@@ -100,7 +100,7 @@ exports.resources_by_ids = function resources_by_ids(public_ids, callback, optio
100100
resource_type = options.resource_type || "image";
101101
type = options.type || "upload";
102102
uri = ["resources", resource_type, type];
103-
params = pickOnlyExistingValues(options, "tags", "context", "moderations");
103+
params = pickOnlyExistingValues(options, "tags", "context", "moderations", "fields");
104104
params["public_ids[]"] = public_ids;
105105
return call_api("get", uri, params, callback, options);
106106
};

lib/v2/search.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const Search = class Search {
1515
this.query_hash = {
1616
sort_by: [],
1717
aggregate: [],
18-
with_field: []
18+
with_field: [],
19+
fields: []
1920
};
2021
this._ttl = 300;
2122
}
@@ -44,6 +45,10 @@ const Search = class Search {
4445
return this.instance().with_field(value);
4546
}
4647

48+
static fields(value) {
49+
return this.instance().fields(value);
50+
}
51+
4752
static sort_by(field_name, dir = 'asc') {
4853
return this.instance().sort_by(field_name, dir);
4954
}
@@ -82,12 +87,24 @@ const Search = class Search {
8287
}
8388

8489
with_field(value) {
85-
const found = this.query_hash.with_field.find(v => v === value);
86-
87-
if (!found) {
90+
if (Array.isArray(value)) {
91+
this.query_hash.with_field = this.query_hash.with_field.concat(value);
92+
} else {
8893
this.query_hash.with_field.push(value);
8994
}
9095

96+
this.query_hash.with_field = Array.from(new Set(this.query_hash.with_field));
97+
return this;
98+
}
99+
100+
fields(value) {
101+
if (Array.isArray(value)) {
102+
this.query_hash.fields = this.query_hash.fields.concat(value);
103+
} else {
104+
this.query_hash.fields.push(value);
105+
}
106+
107+
this.query_hash.fields = Array.from(new Set(this.query_hash.fields));
91108
return this;
92109
}
93110

test/integration/api/admin/api_spec.js

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const retry = require('../../../testUtils/helpers/retry');
1717
const {shouldTestFeature} = require("../../../spechelper");
1818
const API_V2 = cloudinary.v2.api;
1919
const DYNAMIC_FOLDERS = helper.DYNAMIC_FOLDERS;
20+
const assert = require('assert');
21+
const {only} = require("../../../../lib/utils");
2022

2123
const {
2224
TIMEOUT,
@@ -425,6 +427,51 @@ describe("api", function () {
425427
}));
426428
});
427429
});
430+
431+
describe('selective response', () => {
432+
const expectedKeys = ['public_id', 'asset_id', 'folder', 'tags'].sort();
433+
434+
it('should allow listing', async () => {
435+
const {resources} = await cloudinary.v2.api.resources({fields: ['tags']})
436+
const actualKeys = Object.keys(resources[0]);
437+
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
438+
});
439+
440+
it('should allow listing by public_ids', async () => {
441+
const {resources} = await cloudinary.v2.api.resources_by_ids([PUBLIC_ID], {fields: ['tags']})
442+
const actualKeys = Object.keys(resources[0]);
443+
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
444+
});
445+
446+
it('should allow listing by tag', async () => {
447+
const {resources} = await cloudinary.v2.api.resources_by_tag(TEST_TAG, {fields: ['tags']})
448+
const actualKeys = Object.keys(resources[0]);
449+
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
450+
});
451+
452+
it('should allow listing by context', async () => {
453+
const {resources} = await cloudinary.v2.api.resources_by_context(contextKey, "test", {fields: ['tags']})
454+
const actualKeys = Object.keys(resources[0]);
455+
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
456+
});
457+
458+
it('should allow listing by moderation', async () => {
459+
await uploadImage({
460+
moderation: 'manual',
461+
tags: [TEST_TAG]
462+
});
463+
const {resources} = await cloudinary.v2.api.resources_by_moderation('manual', 'pending', {fields: ['tags']})
464+
const actualKeys = Object.keys(resources[0]);
465+
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
466+
});
467+
468+
it('should allow listing by asset_ids', async () => {
469+
const {asset_id} = await uploadImage();
470+
const {resources} = await cloudinary.v2.api.resources_by_asset_ids([asset_id], {fields: ['tags']})
471+
const actualKeys = Object.keys(resources[0]);
472+
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
473+
});
474+
});
428475
});
429476
describe("backup resource", function () {
430477
this.timeout(TIMEOUT.MEDIUM);
@@ -1530,5 +1577,26 @@ describe("api", function () {
15301577
arg => arg.agent instanceof https.Agent
15311578
));
15321579
});
1533-
})
1580+
});
1581+
describe('config hide_sensitive', () => {
1582+
it("should hide API key and secret upon error when `hide_sensitive` is true", async function () {
1583+
try {
1584+
cloudinary.config({hide_sensitive: true});
1585+
const result = await cloudinary.v2.api.resource("?");
1586+
expect(result).fail();
1587+
} catch (err) {
1588+
expect(err.request_options).not.to.have.property("auth");
1589+
}
1590+
});
1591+
1592+
it("should hide Authorization header upon error when `hide_sensitive` is true", async function () {
1593+
try {
1594+
cloudinary.config({hide_sensitive: true});
1595+
const result = await cloudinary.v2.api.resource("?", { oauth_token: 'irrelevant' });
1596+
expect(result).fail();
1597+
} catch (err) {
1598+
expect(err.request_options.headers).not.to.have.property("Authorization");
1599+
}
1600+
});
1601+
});
15341602
});

test/integration/api/search/search_spec.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const testConstants = require('../../../testUtils/testConstants');
55
const describe = require('../../../testUtils/suite');
66
const exp = require("constants");
77
const cluster = require("cluster");
8+
const assert = require("assert");
89
const {
910
TIMEOUT,
1011
TAGS,
@@ -122,7 +123,7 @@ describe("search_api", function () {
122123
});
123124
});
124125

125-
it('Should eliminate duplicate fields when using sort_by, aggregate or with_fields', function () {
126+
it('Should eliminate duplicate fields when using sort_by, aggregate, with_field or fields', function () {
126127
// This test ensures we can't push duplicate values into sort_by, aggregate or with_fields
127128
const search_query = cloudinary.v2.search.max_results(10).expression(`tags:${SEARCH_TAG}`)
128129
.sort_by('public_id', 'asc')
@@ -137,16 +138,26 @@ describe("search_api", function () {
137138
.with_field('foo')
138139
.with_field('foo')
139140
.with_field('foo2')
141+
.with_field(['foo', 'foo2', 'foo3'])
142+
.fields('foo')
143+
.fields('foo')
144+
.fields('foo2')
145+
.fields(['foo', 'foo2', 'foo3'])
140146
.to_query();
141147

142148
expect(search_query.aggregate.length).to.be(2);
143-
expect(search_query.with_field.length).to.be(2);
149+
expect(search_query.with_field.length).to.be(3);
150+
expect(search_query.fields.length).to.be(3);
144151
expect(search_query.sort_by.length).to.be(1);
145152

146153
expect(search_query.aggregate[0]).to.be('foo');
147154
expect(search_query.aggregate[1]).to.be('foo2');
148155
expect(search_query.with_field[0]).to.be('foo');
149156
expect(search_query.with_field[1]).to.be('foo2');
157+
expect(search_query.with_field[2]).to.be('foo3');
158+
expect(search_query.fields[0]).to.be('foo');
159+
expect(search_query.fields[1]).to.be('foo2');
160+
expect(search_query.fields[2]).to.be('foo3');
150161

151162
expect(search_query.sort_by[0].public_id).to.be('desc');
152163
});
@@ -176,5 +187,20 @@ describe("search_api", function () {
176187
});
177188
});
178189
});
190+
191+
it('should only include selected keys when using fields', function () {
192+
return cloudinary.v2.search.expression(`tags:${SEARCH_TAG}`).fields('context')
193+
.execute()
194+
.then(function (results) {
195+
expect(results.resources.length).to.eql(3);
196+
results.resources.forEach(function (res) {
197+
const alwaysIncluded = ['public_id', 'asset_id', 'created_at', 'status', 'type', 'resource_type', 'folder'];
198+
const additionallyIncluded = ['context'];
199+
const expectedKeys = [...alwaysIncluded, ...additionallyIncluded];
200+
const actualKeys = Object.keys(res);
201+
assert.deepStrictEqual(actualKeys.sort(), expectedKeys.sort());
202+
});
203+
});
204+
});
179205
});
180206
});

test/unit/config.spec.js

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,6 @@ describe("config", function () {
3030
expect(config.hide_sensitive).to.eql(true)
3131
});
3232

33-
it("should hide API key and secret upon error when `hide_sensitive` is true", async function () {
34-
try {
35-
cloudinary.config({hide_sensitive: true});
36-
const result = await cloudinary.v2.api.resource("?");
37-
expect(result).fail();
38-
} catch (err) {
39-
expect(err.request_options).not.to.have.property("auth");
40-
}
41-
});
42-
43-
it("should hide Authorization header upon error when `hide_sensitive` is true", async function () {
44-
try {
45-
cloudinary.config({hide_sensitive: true});
46-
const result = await cloudinary.v2.api.resource("?", { oauth_token: 'irrelevant' });
47-
expect(result).fail();
48-
} catch (err) {
49-
expect(err.request_options.headers).not.to.have.property("Authorization");
50-
}
51-
});
52-
5333
it("should allow nested values in CLOUDINARY_URL", function () {
5434
process.env.CLOUDINARY_URL = "cloudinary://key:secret@test123?foo[bar]=value";
5535
cloudinary.config(true);

test/unit/search/search_spec.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ describe('Search', () => {
1515
'max_results',
1616
'next_cursor',
1717
'aggregate',
18-
'with_field'
18+
'with_field',
19+
'fields'
1920
].forEach(method => expect(instance).to.eql(instance[method]('emptyarg')));
2021
});
2122

@@ -62,12 +63,53 @@ describe('Search', () => {
6263
});
6364

6465
it('should add with_field to query', function () {
65-
var query = cloudinary.v2.search.with_field('context').with_field('tags').to_query();
66+
const query = cloudinary.v2.search.with_field('context').with_field('tags').to_query();
6667
expect(query).to.eql({
6768
with_field: ['context', 'tags']
6869
});
6970
});
7071

72+
it('should allow adding multiple with_field values to query', function () {
73+
const query = cloudinary.v2.search.with_field(['context', 'tags']).to_query();
74+
expect(query).to.eql({
75+
with_field: ['context', 'tags']
76+
});
77+
});
78+
79+
it('should remove duplicates with_field values from query', () => {
80+
const search = cloudinary.v2.search.with_field(['field1', 'field1', 'field2']);
81+
search.with_field('field1');
82+
search.with_field('field3');
83+
const query = search.to_query();
84+
expect(query).to.eql({
85+
with_field: ['field1', 'field2', 'field3']
86+
});
87+
});
88+
89+
it('should add fields to query', function () {
90+
const query = cloudinary.v2.search.fields('context').fields('tags').to_query();
91+
expect(query).to.eql({
92+
fields: ['context', 'tags']
93+
});
94+
});
95+
96+
it('should allow adding multiple fields values to query', function () {
97+
const query = cloudinary.v2.search.fields(['context', 'tags']).to_query();
98+
expect(query).to.eql({
99+
fields: ['context', 'tags']
100+
});
101+
});
102+
103+
it('should remove duplicates fields values from query', () => {
104+
const search = cloudinary.v2.search.fields(['field1', 'field1', 'field2']);
105+
search.fields('field1');
106+
search.fields('field3');
107+
const query = search.to_query();
108+
expect(query).to.eql({
109+
fields: ['field1', 'field2', 'field3']
110+
});
111+
});
112+
71113
it('should run without an expression', function () {
72114
assert.doesNotThrow(
73115
() => {

types/index.d.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,7 +1395,9 @@ declare module 'cloudinary' {
13951395

13961396
to_query(value?: string): search;
13971397

1398-
with_field(value?: string): search;
1398+
with_field(value?: string | Array<string>): search;
1399+
1400+
fields(value?: string | Array<string>): search;
13991401

14001402
to_url(newTtl?: number, next_cursor?: string, options?: ConfigOptions): string;
14011403

@@ -1413,7 +1415,9 @@ declare module 'cloudinary' {
14131415

14141416
static ttl(newTtl: number): search;
14151417

1416-
static with_field(args?: string): search;
1418+
static with_field(args?: string | Array<string>): search;
1419+
1420+
static fields(args?: string | Array<string>): search;
14171421
}
14181422

14191423
/****************************** Provisioning API *************************************/

0 commit comments

Comments
 (0)