Skip to content

Commit

Permalink
feat: selective response for admin and search api (#659)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cloudinary-pkoniu authored Apr 22, 2024
1 parent 8cd9df3 commit 76c04c7
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 38 deletions.
14 changes: 7 additions & 7 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,21 @@ exports.resources = function resources(callback, options = {}) {
if ((options.start_at != null) && Object.prototype.toString.call(options.start_at) === '[object Date]') {
options.start_at = options.start_at.toUTCString();
}
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "prefix", "tags", "context", "direction", "moderations", "start_at", "metadata"), callback, options);
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "prefix", "tags", "context", "direction", "moderations", "start_at", "metadata", "fields"), callback, options);
};

exports.resources_by_tag = function resources_by_tag(tag, callback, options = {}) {
let resource_type, uri;
resource_type = options.resource_type || "image";
uri = ["resources", resource_type, "tags", tag];
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata"), callback, options);
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata", "fields"), callback, options);
};

exports.resources_by_context = function resources_by_context(key, value, callback, options = {}) {
let params, resource_type, uri;
resource_type = options.resource_type || "image";
uri = ["resources", resource_type, "context"];
params = pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata");
params = pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata", "fields");
params.key = key;
if (value != null) {
params.value = value;
Expand All @@ -71,7 +71,7 @@ exports.resources_by_moderation = function resources_by_moderation(kind, status,
let resource_type, uri;
resource_type = options.resource_type || "image";
uri = ["resources", resource_type, "moderations", kind, status];
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata"), callback, options);
return call_api("get", uri, pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "direction", "moderations", "metadata", "fields"), callback, options);
};

exports.resource_by_asset_id = function resource_by_asset_id(asset_id, callback, options = {}) {
Expand All @@ -82,15 +82,15 @@ exports.resource_by_asset_id = function resource_by_asset_id(asset_id, callback,
exports.resources_by_asset_folder = function resources_by_asset_folder(asset_folder, callback, options = {}) {
let params, uri;
uri = ["resources", 'by_asset_folder'];
params = pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "moderations");
params = pickOnlyExistingValues(options, "next_cursor", "max_results", "tags", "context", "moderations", "fields");
params.asset_folder = asset_folder;
return call_api("get", uri, params, callback, options);
};

exports.resources_by_asset_ids = function resources_by_asset_ids(asset_ids, callback, options = {}) {
let params, uri;
uri = ["resources", "by_asset_ids"];
params = pickOnlyExistingValues(options, "tags", "context", "moderations");
params = pickOnlyExistingValues(options, "tags", "context", "moderations", "fields");
params["asset_ids[]"] = asset_ids;
return call_api("get", uri, params, callback, options);
}
Expand All @@ -100,7 +100,7 @@ exports.resources_by_ids = function resources_by_ids(public_ids, callback, optio
resource_type = options.resource_type || "image";
type = options.type || "upload";
uri = ["resources", resource_type, type];
params = pickOnlyExistingValues(options, "tags", "context", "moderations");
params = pickOnlyExistingValues(options, "tags", "context", "moderations", "fields");
params["public_ids[]"] = public_ids;
return call_api("get", uri, params, callback, options);
};
Expand Down
25 changes: 21 additions & 4 deletions lib/v2/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const Search = class Search {
this.query_hash = {
sort_by: [],
aggregate: [],
with_field: []
with_field: [],
fields: []
};
this._ttl = 300;
}
Expand Down Expand Up @@ -44,6 +45,10 @@ const Search = class Search {
return this.instance().with_field(value);
}

static fields(value) {
return this.instance().fields(value);
}

static sort_by(field_name, dir = 'asc') {
return this.instance().sort_by(field_name, dir);
}
Expand Down Expand Up @@ -82,12 +87,24 @@ const Search = class Search {
}

with_field(value) {
const found = this.query_hash.with_field.find(v => v === value);

if (!found) {
if (Array.isArray(value)) {
this.query_hash.with_field = this.query_hash.with_field.concat(value);
} else {
this.query_hash.with_field.push(value);
}

this.query_hash.with_field = Array.from(new Set(this.query_hash.with_field));
return this;
}

fields(value) {
if (Array.isArray(value)) {
this.query_hash.fields = this.query_hash.fields.concat(value);
} else {
this.query_hash.fields.push(value);
}

this.query_hash.fields = Array.from(new Set(this.query_hash.fields));
return this;
}

Expand Down
70 changes: 69 additions & 1 deletion test/integration/api/admin/api_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const retry = require('../../../testUtils/helpers/retry');
const {shouldTestFeature} = require("../../../spechelper");
const API_V2 = cloudinary.v2.api;
const DYNAMIC_FOLDERS = helper.DYNAMIC_FOLDERS;
const assert = require('assert');
const {only} = require("../../../../lib/utils");

const {
TIMEOUT,
Expand Down Expand Up @@ -425,6 +427,51 @@ describe("api", function () {
}));
});
});

describe('selective response', () => {
const expectedKeys = ['public_id', 'asset_id', 'folder', 'tags'].sort();

it('should allow listing', async () => {
const {resources} = await cloudinary.v2.api.resources({fields: ['tags']})
const actualKeys = Object.keys(resources[0]);
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
});

it('should allow listing by public_ids', async () => {
const {resources} = await cloudinary.v2.api.resources_by_ids([PUBLIC_ID], {fields: ['tags']})
const actualKeys = Object.keys(resources[0]);
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
});

it('should allow listing by tag', async () => {
const {resources} = await cloudinary.v2.api.resources_by_tag(TEST_TAG, {fields: ['tags']})
const actualKeys = Object.keys(resources[0]);
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
});

it('should allow listing by context', async () => {
const {resources} = await cloudinary.v2.api.resources_by_context(contextKey, "test", {fields: ['tags']})
const actualKeys = Object.keys(resources[0]);
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
});

it('should allow listing by moderation', async () => {
await uploadImage({
moderation: 'manual',
tags: [TEST_TAG]
});
const {resources} = await cloudinary.v2.api.resources_by_moderation('manual', 'pending', {fields: ['tags']})
const actualKeys = Object.keys(resources[0]);
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
});

it('should allow listing by asset_ids', async () => {
const {asset_id} = await uploadImage();
const {resources} = await cloudinary.v2.api.resources_by_asset_ids([asset_id], {fields: ['tags']})
const actualKeys = Object.keys(resources[0]);
assert.deepStrictEqual(actualKeys.sort(), expectedKeys);
});
});
});
describe("backup resource", function () {
this.timeout(TIMEOUT.MEDIUM);
Expand Down Expand Up @@ -1530,5 +1577,26 @@ describe("api", function () {
arg => arg.agent instanceof https.Agent
));
});
})
});
describe('config hide_sensitive', () => {
it("should hide API key and secret upon error when `hide_sensitive` is true", async function () {
try {
cloudinary.config({hide_sensitive: true});
const result = await cloudinary.v2.api.resource("?");
expect(result).fail();
} catch (err) {
expect(err.request_options).not.to.have.property("auth");
}
});

it("should hide Authorization header upon error when `hide_sensitive` is true", async function () {
try {
cloudinary.config({hide_sensitive: true});
const result = await cloudinary.v2.api.resource("?", { oauth_token: 'irrelevant' });
expect(result).fail();
} catch (err) {
expect(err.request_options.headers).not.to.have.property("Authorization");
}
});
});
});
30 changes: 28 additions & 2 deletions test/integration/api/search/search_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const testConstants = require('../../../testUtils/testConstants');
const describe = require('../../../testUtils/suite');
const exp = require("constants");
const cluster = require("cluster");
const assert = require("assert");
const {
TIMEOUT,
TAGS,
Expand Down Expand Up @@ -122,7 +123,7 @@ describe("search_api", function () {
});
});

it('Should eliminate duplicate fields when using sort_by, aggregate or with_fields', function () {
it('Should eliminate duplicate fields when using sort_by, aggregate, with_field or fields', function () {
// This test ensures we can't push duplicate values into sort_by, aggregate or with_fields
const search_query = cloudinary.v2.search.max_results(10).expression(`tags:${SEARCH_TAG}`)
.sort_by('public_id', 'asc')
Expand All @@ -137,16 +138,26 @@ describe("search_api", function () {
.with_field('foo')
.with_field('foo')
.with_field('foo2')
.with_field(['foo', 'foo2', 'foo3'])
.fields('foo')
.fields('foo')
.fields('foo2')
.fields(['foo', 'foo2', 'foo3'])
.to_query();

expect(search_query.aggregate.length).to.be(2);
expect(search_query.with_field.length).to.be(2);
expect(search_query.with_field.length).to.be(3);
expect(search_query.fields.length).to.be(3);
expect(search_query.sort_by.length).to.be(1);

expect(search_query.aggregate[0]).to.be('foo');
expect(search_query.aggregate[1]).to.be('foo2');
expect(search_query.with_field[0]).to.be('foo');
expect(search_query.with_field[1]).to.be('foo2');
expect(search_query.with_field[2]).to.be('foo3');
expect(search_query.fields[0]).to.be('foo');
expect(search_query.fields[1]).to.be('foo2');
expect(search_query.fields[2]).to.be('foo3');

expect(search_query.sort_by[0].public_id).to.be('desc');
});
Expand Down Expand Up @@ -176,5 +187,20 @@ describe("search_api", function () {
});
});
});

it('should only include selected keys when using fields', function () {
return cloudinary.v2.search.expression(`tags:${SEARCH_TAG}`).fields('context')
.execute()
.then(function (results) {
expect(results.resources.length).to.eql(3);
results.resources.forEach(function (res) {
const alwaysIncluded = ['public_id', 'asset_id', 'created_at', 'status', 'type', 'resource_type', 'folder'];
const additionallyIncluded = ['context'];
const expectedKeys = [...alwaysIncluded, ...additionallyIncluded];
const actualKeys = Object.keys(res);
assert.deepStrictEqual(actualKeys.sort(), expectedKeys.sort());
});
});
});
});
});
20 changes: 0 additions & 20 deletions test/unit/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,6 @@ describe("config", function () {
expect(config.hide_sensitive).to.eql(true)
});

it("should hide API key and secret upon error when `hide_sensitive` is true", async function () {
try {
cloudinary.config({hide_sensitive: true});
const result = await cloudinary.v2.api.resource("?");
expect(result).fail();
} catch (err) {
expect(err.request_options).not.to.have.property("auth");
}
});

it("should hide Authorization header upon error when `hide_sensitive` is true", async function () {
try {
cloudinary.config({hide_sensitive: true});
const result = await cloudinary.v2.api.resource("?", { oauth_token: 'irrelevant' });
expect(result).fail();
} catch (err) {
expect(err.request_options.headers).not.to.have.property("Authorization");
}
});

it("should allow nested values in CLOUDINARY_URL", function () {
process.env.CLOUDINARY_URL = "cloudinary://key:secret@test123?foo[bar]=value";
cloudinary.config(true);
Expand Down
46 changes: 44 additions & 2 deletions test/unit/search/search_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ describe('Search', () => {
'max_results',
'next_cursor',
'aggregate',
'with_field'
'with_field',
'fields'
].forEach(method => expect(instance).to.eql(instance[method]('emptyarg')));
});

Expand Down Expand Up @@ -62,12 +63,53 @@ describe('Search', () => {
});

it('should add with_field to query', function () {
var query = cloudinary.v2.search.with_field('context').with_field('tags').to_query();
const query = cloudinary.v2.search.with_field('context').with_field('tags').to_query();
expect(query).to.eql({
with_field: ['context', 'tags']
});
});

it('should allow adding multiple with_field values to query', function () {
const query = cloudinary.v2.search.with_field(['context', 'tags']).to_query();
expect(query).to.eql({
with_field: ['context', 'tags']
});
});

it('should remove duplicates with_field values from query', () => {
const search = cloudinary.v2.search.with_field(['field1', 'field1', 'field2']);
search.with_field('field1');
search.with_field('field3');
const query = search.to_query();
expect(query).to.eql({
with_field: ['field1', 'field2', 'field3']
});
});

it('should add fields to query', function () {
const query = cloudinary.v2.search.fields('context').fields('tags').to_query();
expect(query).to.eql({
fields: ['context', 'tags']
});
});

it('should allow adding multiple fields values to query', function () {
const query = cloudinary.v2.search.fields(['context', 'tags']).to_query();
expect(query).to.eql({
fields: ['context', 'tags']
});
});

it('should remove duplicates fields values from query', () => {
const search = cloudinary.v2.search.fields(['field1', 'field1', 'field2']);
search.fields('field1');
search.fields('field3');
const query = search.to_query();
expect(query).to.eql({
fields: ['field1', 'field2', 'field3']
});
});

it('should run without an expression', function () {
assert.doesNotThrow(
() => {
Expand Down
8 changes: 6 additions & 2 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1395,7 +1395,9 @@ declare module 'cloudinary' {

to_query(value?: string): search;

with_field(value?: string): search;
with_field(value?: string | Array<string>): search;

fields(value?: string | Array<string>): search;

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

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

static ttl(newTtl: number): search;

static with_field(args?: string): search;
static with_field(args?: string | Array<string>): search;

static fields(args?: string | Array<string>): search;
}

/****************************** Provisioning API *************************************/
Expand Down

0 comments on commit 76c04c7

Please sign in to comment.