From 252d2ac4e1c1bbb801c5ec570cbd207d30901b7c Mon Sep 17 00:00:00 2001 From: Sviatozar Petrenko Date: Fri, 16 Feb 2024 11:16:59 +0200 Subject: [PATCH] feat: add `runs()` and `builds()` top level endpoints (#468) Closes #296 --- src/apify_client.ts | 33 +- src/resource_clients/build_collection.ts | 4 +- src/resource_clients/run_collection.ts | 4 +- test/builds.test.js | 2 +- test/mock_server/routes/builds.js | 1 + test/mock_server/routes/runs.js | 1 + test/runs.test.js | 396 ++++++++++++----------- 7 files changed, 232 insertions(+), 209 deletions(-) diff --git a/src/apify_client.ts b/src/apify_client.ts index 1e18adaa..1df2927c 100644 --- a/src/apify_client.ts +++ b/src/apify_client.ts @@ -8,7 +8,7 @@ import { RequestInterceptorFunction } from './interceptors'; import { ActorClient } from './resource_clients/actor'; import { ActorCollectionClient } from './resource_clients/actor_collection'; import { BuildClient } from './resource_clients/build'; -// import { BuildCollectionClient } from './resource_clients/build_collection'; +import { BuildCollectionClient } from './resource_clients/build_collection'; import { DatasetClient } from './resource_clients/dataset'; import { DatasetCollectionClient } from './resource_clients/dataset_collection'; import { KeyValueStoreClient } from './resource_clients/key_value_store'; @@ -17,7 +17,7 @@ import { LogClient } from './resource_clients/log'; import { RequestQueueClient, RequestQueueUserOptions } from './resource_clients/request_queue'; import { RequestQueueCollectionClient } from './resource_clients/request_queue_collection'; import { RunClient } from './resource_clients/run'; -// import { RunCollectionClient } from './resource_clients/run_collection'; +import { RunCollectionClient } from './resource_clients/run_collection'; import { ScheduleClient } from './resource_clients/schedule'; import { ScheduleCollectionClient } from './resource_clients/schedule_collection'; import { StoreCollectionClient } from './resource_clients/store_collection'; @@ -107,13 +107,12 @@ export class ApifyClient { }); } - // TODO we don't have this endpoint yet - // /** - // * @return {BuildCollectionClient} - // */ - // builds() { - // return new BuildCollectionClient(this._options()); - // } + /** + * https://docs.apify.com/api/v2#/reference/actor-builds/build-collection + */ + builds(): BuildCollectionClient { + return new BuildCollectionClient(this._options()); + } /** * https://docs.apify.com/api/v2#/reference/actor-builds/build-object @@ -203,13 +202,15 @@ export class ApifyClient { return new RequestQueueClient(apiClientOptions, options); } - // TODO we don't have this endpoint yet - // /** - // * @return {RunCollectionClient} - // */ - // runs() { - // return new RunCollectionClient(this._options()); - // } + /** + * https://docs.apify.com/api/v2#/reference/actor-runs/run-collection + */ + runs(): RunCollectionClient { + return new RunCollectionClient({ + ...this._options(), + resourcePath: 'actor-runs', + }); + } /** * https://docs.apify.com/api/v2#/reference/actor-runs/run-object-and-its-storages diff --git a/src/resource_clients/build_collection.ts b/src/resource_clients/build_collection.ts index b56cd0f9..561fc0b1 100644 --- a/src/resource_clients/build_collection.ts +++ b/src/resource_clients/build_collection.ts @@ -1,7 +1,7 @@ import ow from 'ow'; import { Build } from './build'; -import { ApiClientOptions } from '../base/api_client'; +import { ApiClientOptionsWithOptionalResourcePath } from '../base/api_client'; import { ResourceCollectionClient } from '../base/resource_collection_client'; import { PaginatedList } from '../utils'; @@ -9,7 +9,7 @@ export class BuildCollectionClient extends ResourceCollectionClient { /** * @hidden */ - constructor(options: ApiClientOptions) { + constructor(options: ApiClientOptionsWithOptionalResourcePath) { super({ ...options, resourcePath: options.resourcePath || 'actor-builds', diff --git a/src/resource_clients/run_collection.ts b/src/resource_clients/run_collection.ts index c4484d69..43bdbca7 100644 --- a/src/resource_clients/run_collection.ts +++ b/src/resource_clients/run_collection.ts @@ -2,7 +2,7 @@ import { ACT_JOB_STATUSES } from '@apify/consts'; import ow from 'ow'; import { ActorRunListItem } from './actor'; -import { ApiClientSubResourceOptions } from '../base/api_client'; +import { ApiClientOptionsWithOptionalResourcePath } from '../base/api_client'; import { ResourceCollectionClient } from '../base/resource_collection_client'; import { PaginatedList } from '../utils'; @@ -10,7 +10,7 @@ export class RunCollectionClient extends ResourceCollectionClient { /** * @hidden */ - constructor(options: ApiClientSubResourceOptions) { + constructor(options: ApiClientOptionsWithOptionalResourcePath) { super({ resourcePath: 'runs', ...options, diff --git a/test/builds.test.js b/test/builds.test.js index 6feea9a0..f78590ca 100644 --- a/test/builds.test.js +++ b/test/builds.test.js @@ -35,7 +35,7 @@ describe('Build methods', () => { }); describe('builds()', () => { - test.skip('list() works', async () => { + test('list() works', async () => { const query = { limit: 5, offset: 3, diff --git a/test/mock_server/routes/builds.js b/test/mock_server/routes/builds.js index 3980f3f4..3545ed7b 100644 --- a/test/mock_server/routes/builds.js +++ b/test/mock_server/routes/builds.js @@ -5,6 +5,7 @@ const { addRoutes } = require('./add_routes'); const builds = express.Router(); const ROUTES = [ + { id: 'list-builds', method: 'GET', path: '/' }, { id: 'get-build', method: 'GET', path: '/:buildId', type: 'responseJsonMock' }, { id: 'abort-build', method: 'POST', path: '/:buildId/abort' }, { id: 'build-log', method: 'GET', path: '/:buildId/log', type: 'text' }, diff --git a/test/mock_server/routes/runs.js b/test/mock_server/routes/runs.js index 42db349e..2debcbe0 100644 --- a/test/mock_server/routes/runs.js +++ b/test/mock_server/routes/runs.js @@ -5,6 +5,7 @@ const { addRoutes } = require('./add_routes'); const runs = express.Router(); const ROUTES = [ + { id: 'list-runs', method: 'GET', path: '/' }, { id: 'get-run', method: 'GET', path: '/:runId', type: 'responseJsonMock' }, { id: 'abort-run', method: 'POST', path: '/:runId/abort' }, { id: 'metamorph-run', method: 'POST', path: '/:runId/metamorph' }, diff --git a/test/runs.test.js b/test/runs.test.js index 57f23c82..371996f9 100644 --- a/test/runs.test.js +++ b/test/runs.test.js @@ -31,237 +31,257 @@ describe('Run methods', () => { }); afterEach(async () => { client = null; - page.close().catch(() => {}); + page.close().catch(() => { }); }); - test('get() works', async () => { - const runId = 'some-run-id'; + describe('runs()', () => { + test('list() works', async () => { + const query = { + limit: 5, + offset: 3, + desc: true, + }; + + const res = await client.runs().list(query); + expect(res.id).toEqual('list-runs'); + validateRequest(query); + + const browserRes = await page.evaluate((opts) => client.runs().list(opts), query); + expect(browserRes).toEqual(res); + validateRequest(query); + }); + }); - const res = await client.run(runId).get(); - expect(res.id).toEqual('get-run'); - validateRequest({}, { runId }); + describe('run()', () => { + test('get() works', async () => { + const runId = 'some-run-id'; - const browserRes = await page.evaluate((rId) => client.run(rId).get(), runId); - expect(browserRes).toEqual(res); - validateRequest({}, { runId }); - }); + const res = await client.run(runId).get(); + expect(res.id).toEqual('get-run'); + validateRequest({}, { runId }); - test('get() returns undefined on 404 status code (RECORD_NOT_FOUND)', async () => { - const runId = '404'; + const browserRes = await page.evaluate((rId) => client.run(rId).get(), runId); + expect(browserRes).toEqual(res); + validateRequest({}, { runId }); + }); - const res = await client.run(runId).get(); - expect(res).toBeUndefined(); - validateRequest({}, { runId }); + test('get() returns undefined on 404 status code (RECORD_NOT_FOUND)', async () => { + const runId = '404'; - const browserRes = await page.evaluate((rId) => client.run(rId).get(), runId); - expect(browserRes).toEqual(res); - validateRequest({}, { runId }); - }); + const res = await client.run(runId).get(); + expect(res).toBeUndefined(); + validateRequest({}, { runId }); - test('abort() works', async () => { - const runId = 'some-run-id'; + const browserRes = await page.evaluate((rId) => client.run(rId).get(), runId); + expect(browserRes).toEqual(res); + validateRequest({}, { runId }); + }); - const res = await client.run(runId).abort(); - expect(res.id).toEqual('abort-run'); - validateRequest({}, { runId }); + test('abort() works', async () => { + const runId = 'some-run-id'; - const browserRes = await page.evaluate((rId) => client.run(rId).abort(), runId); - expect(browserRes).toEqual(res); - validateRequest({}, { runId }); - }); + const res = await client.run(runId).abort(); + expect(res.id).toEqual('abort-run'); + validateRequest({}, { runId }); - test('resurrect() works', async () => { - const runId = 'some-run-id'; + const browserRes = await page.evaluate((rId) => client.run(rId).abort(), runId); + expect(browserRes).toEqual(res); + validateRequest({}, { runId }); + }); - const options = { - build: 'some-build', - memory: 1024, - timeout: 400, - }; + test('resurrect() works', async () => { + const runId = 'some-run-id'; - const res = await client.run(runId).resurrect(options); - expect(res.id).toEqual('resurrect-run'); - validateRequest(options, { runId }); + const options = { + build: 'some-build', + memory: 1024, + timeout: 400, + }; - const browserRes = await page.evaluate((rId, opts) => client.run(rId).resurrect(opts), runId, options); - expect(browserRes).toEqual(res); - validateRequest(options, { runId }); - }); + const res = await client.run(runId).resurrect(options); + expect(res.id).toEqual('resurrect-run'); + validateRequest(options, { runId }); - test('metamorph() works', async () => { - const runId = 'some-run-id'; - const targetActorId = 'some-target-id'; - const contentType = 'application/x-www-form-urlencoded'; - const input = 'some=body'; - const build = '1.2.0'; - - const options = { - build, - contentType, - }; - - const actualQuery = { - targetActorId, - build, - }; - - const res = await client.run(runId).metamorph(targetActorId, input, options); - expect(res.id).toEqual('metamorph-run'); - validateRequest(actualQuery, { runId }, { some: 'body' }, { 'content-type': contentType }); - - const browserRes = await page.evaluate((rId, targetId, i, opts) => { - return client.run(rId).metamorph(targetId, i, opts); - }, runId, targetActorId, input, options); - expect(browserRes).toEqual(res); - validateRequest(actualQuery, { runId }, { some: 'body' }, { 'content-type': contentType }); - }); + const browserRes = await page.evaluate((rId, opts) => client.run(rId).resurrect(opts), runId, options); + expect(browserRes).toEqual(res); + validateRequest(options, { runId }); + }); - test('metamorph() works with pre-stringified JSON input', async () => { - const runId = 'some-run-id'; - const targetActorId = 'some-target-id'; - const contentType = 'application/json; charset=utf-8'; - const input = JSON.stringify({ foo: 'bar' }); - - const expectedRequest = [ - { targetActorId }, - { runId }, - { foo: 'bar' }, - { 'content-type': contentType }, - ]; - - const res = await client.run(runId).metamorph(targetActorId, input, { contentType }); - expect(res.id).toEqual('metamorph-run'); - validateRequest(...expectedRequest); - - const browserRes = await page.evaluate((rId, tId, i, cType) => { - return client.run(rId).metamorph(tId, i, { contentType: cType }); - }, runId, targetActorId, input, contentType); - expect(browserRes).toEqual(res); - validateRequest(...expectedRequest); - }); + test('metamorph() works', async () => { + const runId = 'some-run-id'; + const targetActorId = 'some-target-id'; + const contentType = 'application/x-www-form-urlencoded'; + const input = 'some=body'; + const build = '1.2.0'; + + const options = { + build, + contentType, + }; + + const actualQuery = { + targetActorId, + build, + }; + + const res = await client.run(runId).metamorph(targetActorId, input, options); + expect(res.id).toEqual('metamorph-run'); + validateRequest(actualQuery, { runId }, { some: 'body' }, { 'content-type': contentType }); + + const browserRes = await page.evaluate((rId, targetId, i, opts) => { + return client.run(rId).metamorph(targetId, i, opts); + }, runId, targetActorId, input, options); + expect(browserRes).toEqual(res); + validateRequest(actualQuery, { runId }, { some: 'body' }, { 'content-type': contentType }); + }); + + test('metamorph() works with pre-stringified JSON input', async () => { + const runId = 'some-run-id'; + const targetActorId = 'some-target-id'; + const contentType = 'application/json; charset=utf-8'; + const input = JSON.stringify({ foo: 'bar' }); + + const expectedRequest = [ + { targetActorId }, + { runId }, + { foo: 'bar' }, + { 'content-type': contentType }, + ]; + + const res = await client.run(runId).metamorph(targetActorId, input, { contentType }); + expect(res.id).toEqual('metamorph-run'); + validateRequest(...expectedRequest); + + const browserRes = await page.evaluate((rId, tId, i, cType) => { + return client.run(rId).metamorph(tId, i, { contentType: cType }); + }, runId, targetActorId, input, contentType); + expect(browserRes).toEqual(res); + validateRequest(...expectedRequest); + }); - test('metamorph() works with functions in input', async () => { - const runId = 'some-run-id'; - const targetActorId = 'some-target-id'; - const input = { - foo: 'bar', - fn: async (a, b) => a + b, - }; - - const expectedRequest = [ - { targetActorId }, - { runId }, - { foo: 'bar', fn: input.fn.toString() }, - { 'content-type': 'application/json' }, - ]; - - const res = await client.run(runId).metamorph(targetActorId, input); - expect(res.id).toEqual('metamorph-run'); - validateRequest(...expectedRequest); - - const browserRes = await page.evaluate((rId, tId) => { - return client.run(rId).metamorph(tId, { + test('metamorph() works with functions in input', async () => { + const runId = 'some-run-id'; + const targetActorId = 'some-target-id'; + const input = { foo: 'bar', fn: async (a, b) => a + b, - }); - }, runId, targetActorId); - expect(browserRes).toEqual(res); - validateRequest(...expectedRequest); - }); + }; + + const expectedRequest = [ + { targetActorId }, + { runId }, + { foo: 'bar', fn: input.fn.toString() }, + { 'content-type': 'application/json' }, + ]; + + const res = await client.run(runId).metamorph(targetActorId, input); + expect(res.id).toEqual('metamorph-run'); + validateRequest(...expectedRequest); + + const browserRes = await page.evaluate((rId, tId) => { + return client.run(rId).metamorph(tId, { + foo: 'bar', + fn: async (a, b) => a + b, + }); + }, runId, targetActorId); + expect(browserRes).toEqual(res); + validateRequest(...expectedRequest); + }); - test('reboot() works', async () => { - const runId = 'some-run-id'; + test('reboot() works', async () => { + const runId = 'some-run-id'; - const res = await client.run(runId).reboot(); - expect(res.id).toEqual('reboot-run'); - validateRequest({}, { runId }); + const res = await client.run(runId).reboot(); + expect(res.id).toEqual('reboot-run'); + validateRequest({}, { runId }); - const browserRes = await page.evaluate((rId) => client.run(rId).reboot(), runId); - expect(browserRes).toEqual(res); - validateRequest({}, { runId }); - }); + const browserRes = await page.evaluate((rId) => client.run(rId).reboot(), runId); + expect(browserRes).toEqual(res); + validateRequest({}, { runId }); + }); - test('waitForFinish() works', async () => { - const runId = 'some-run-id'; - const waitSecs = 0.1; - const data = { status: 'SUCCEEDED' }; - const body = { data }; + test('waitForFinish() works', async () => { + const runId = 'some-run-id'; + const waitSecs = 0.1; + const data = { status: 'SUCCEEDED' }; + const body = { data }; - setTimeout(() => mockServer.setResponse({ body }), (waitSecs * 1000) / 2); - const res = await client.run(runId).waitForFinish({ waitSecs }); - expect(res).toEqual(data); - validateRequest({ waitForFinish: 0 }, { runId }); + setTimeout(() => mockServer.setResponse({ body }), (waitSecs * 1000) / 2); + const res = await client.run(runId).waitForFinish({ waitSecs }); + expect(res).toEqual(data); + validateRequest({ waitForFinish: 0 }, { runId }); - const browserRes = await page.evaluate((rId, ws) => client.run(rId).waitForFinish({ waitSecs: ws }), runId, waitSecs); - expect(browserRes).toEqual(res); - validateRequest({ waitForFinish: 0 }, { runId }); - }); + const browserRes = await page.evaluate((rId, ws) => client.run(rId).waitForFinish({ waitSecs: ws }), runId, waitSecs); + expect(browserRes).toEqual(res); + validateRequest({ waitForFinish: 0 }, { runId }); + }); - test('waitForFinish() resolves immediately with waitSecs: 0', async () => { - const runId = 'some-run-id'; - const waitSecs = 0; - const data = { status: 'SUCCEEDED' }; - const body = { data }; + test('waitForFinish() resolves immediately with waitSecs: 0', async () => { + const runId = 'some-run-id'; + const waitSecs = 0; + const data = { status: 'SUCCEEDED' }; + const body = { data }; - setTimeout(() => mockServer.setResponse({ body }), 10); - const res = await client.run(runId).waitForFinish({ waitSecs }); - expect(res).toEqual(data); - validateRequest({ waitForFinish: 0 }, { runId }); + setTimeout(() => mockServer.setResponse({ body }), 10); + const res = await client.run(runId).waitForFinish({ waitSecs }); + expect(res).toEqual(data); + validateRequest({ waitForFinish: 0 }, { runId }); - const browserRes = await page.evaluate((rId, ws) => client.run(rId).waitForFinish({ waitSecs: ws }), runId, waitSecs); - expect(browserRes).toEqual(res); - validateRequest({ waitForFinish: 0 }, { runId }); - }); + const browserRes = await page.evaluate((rId, ws) => client.run(rId).waitForFinish({ waitSecs: ws }), runId, waitSecs); + expect(browserRes).toEqual(res); + validateRequest({ waitForFinish: 0 }, { runId }); + }); - test('dataset().get() works', async () => { - const runId = 'some-run-id'; + test('dataset().get() works', async () => { + const runId = 'some-run-id'; - const res = await client.run(runId).dataset().get(); - expect(res.id).toEqual('run-dataset'); + const res = await client.run(runId).dataset().get(); + expect(res.id).toEqual('run-dataset'); - validateRequest({}, { runId }); + validateRequest({}, { runId }); - const browserRes = await page.evaluate((rId) => client.run(rId).dataset().get(), runId); - expect(browserRes).toEqual(res); - validateRequest({}, { runId }); - }); + const browserRes = await page.evaluate((rId) => client.run(rId).dataset().get(), runId); + expect(browserRes).toEqual(res); + validateRequest({}, { runId }); + }); - test('keyValueStore().get() works', async () => { - const runId = 'some-run-id'; + test('keyValueStore().get() works', async () => { + const runId = 'some-run-id'; - const res = await client.run(runId).keyValueStore().get(); - expect(res.id).toEqual('run-keyValueStore'); + const res = await client.run(runId).keyValueStore().get(); + expect(res.id).toEqual('run-keyValueStore'); - validateRequest({}, { runId }); + validateRequest({}, { runId }); - const browserRes = await page.evaluate((rId) => client.run(rId).keyValueStore().get(), runId); - expect(browserRes).toEqual(res); - validateRequest({}, { runId }); - }); + const browserRes = await page.evaluate((rId) => client.run(rId).keyValueStore().get(), runId); + expect(browserRes).toEqual(res); + validateRequest({}, { runId }); + }); - test('requestQueue().get() works', async () => { - const runId = 'some-run-id'; + test('requestQueue().get() works', async () => { + const runId = 'some-run-id'; - const res = await client.run(runId).requestQueue().get(); - expect(res.id).toEqual('run-requestQueue'); + const res = await client.run(runId).requestQueue().get(); + expect(res.id).toEqual('run-requestQueue'); - validateRequest({}, { runId }); + validateRequest({}, { runId }); - const browserRes = await page.evaluate((rId) => client.run(rId).requestQueue().get(), runId); - expect(browserRes).toEqual(res); - validateRequest({}, { runId }); - }); + const browserRes = await page.evaluate((rId) => client.run(rId).requestQueue().get(), runId); + expect(browserRes).toEqual(res); + validateRequest({}, { runId }); + }); - test('log().get() works', async () => { - const runId = 'some-run-id'; + test('log().get() works', async () => { + const runId = 'some-run-id'; - const res = await client.run(runId).log().get(); - expect(res).toEqual('run-log'); + const res = await client.run(runId).log().get(); + expect(res).toEqual('run-log'); - validateRequest({}, { runId }); + validateRequest({}, { runId }); - const browserRes = await page.evaluate((rId) => client.run(rId).log().get(), runId); - expect(browserRes).toEqual(res); - validateRequest({}, { runId }); + const browserRes = await page.evaluate((rId) => client.run(rId).log().get(), runId); + expect(browserRes).toEqual(res); + validateRequest({}, { runId }); + }); }); });