Skip to content

Commit 5168178

Browse files
authored
Merge pull request #162 from JupiterOne/NO-TICKET-fix-sdk-to-use-proper-pagination
Replace skip limit pagination with cursor
2 parents 9a21ce2 + 59c8b1c commit 5168178

File tree

7 files changed

+93
-49
lines changed

7 files changed

+93
-49
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ and this project adheres to
88

99
## [Unreleased]
1010

11+
## [2.1.0] - 2024-07-23
12+
13+
- Update the `queryv1` function to use cursors instead of the legacy `LIMIT SKIP` pagination approach
14+
- Add support for a progress bar
15+
1116
## [2.0.1] - 2024-07-02
1217

1318
- Converted `Content-Type` request header to lower case. This conforms to https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2. And also addresses an issue where Content-Type and content-type headers were both being added.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@jupiterone/jupiterone-client-nodejs",
3-
"version": "2.0.1",
3+
"version": "2.1.0",
44
"description": "A node.js client wrapper for JupiterOne public API",
55
"repository": {
66
"type": "git",
@@ -48,6 +48,7 @@
4848
"apollo-link-retry": "^2.2.13",
4949
"bunyan-category": "^0.4.0",
5050
"chalk": "^4.1.2",
51+
"cli-progress": "^3.12.0",
5152
"commander": "^5.0.0",
5253
"graphql": "^14.6.0",
5354
"graphql-tag": "^2.10.1",

src/example-testing-data/example-data.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
"queryV1": {
44
"type": "deferred",
55
"data": null,
6-
"url": "https://an-s3-bucket/temp/j1dev/deferred/abc/state.json",
7-
"__typename": "QueryV1Response"
6+
"url": "https://an-s3-bucket/temp/j1dev/deferred/abc/state.json"
87
}
98
},
109
"loading": false,
Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
{
22
"status": "COMPLETED",
33
"url": "https://an-s3-bucket/temp/j1dev/deferred/abc/state.json",
4-
"correlationId": "abc"
4+
"correlationId": "abc",
5+
"data": {
6+
"id": "abc",
7+
"entity": {
8+
"_class": ["Class"],
9+
"_type": ["an_example_type"],
10+
"_key": "abc",
11+
"displayName": "display_name",
12+
"_integrationType": "github",
13+
"_integrationClass": ["ITS", "SCM", "VCS", "VersionControl"],
14+
"_integrationDefinitionId": "def",
15+
"_integrationName": "JupiterOne",
16+
"_beginOn": "2021-12-03T01:10:33.604Z",
17+
"_id": "ghi",
18+
"_integrationInstanceId": "nmi",
19+
"_version": 11,
20+
"_accountId": "an_account",
21+
"_deleted": false,
22+
"_source": "source",
23+
"_createdOn": "2021-08-20T20:15:09.583Z"
24+
},
25+
"properties": {
26+
"disabled": false,
27+
"empty": false,
28+
"fork": false,
29+
"forkingAllowed": false
30+
}
31+
}
532
}

src/index.ts

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { BatchHttpLink } from 'apollo-link-batch-http';
66
import fetch, { RequestInit, Response as FetchResponse } from 'node-fetch';
77
import { retry } from '@lifeomic/attempt';
88
import gql from 'graphql-tag';
9+
import cliProgress from 'cli-progress';
910

1011
import Logger, { createLogger } from 'bunyan-category';
1112

@@ -44,8 +45,6 @@ import {
4445
import { query, QueryTypes } from './util/query';
4546

4647
const QUERY_RESULTS_TIMEOUT = 1000 * 60 * 5; // Poll s3 location for 5 minutes before timeout.
47-
const J1QL_SKIP_COUNT = 250;
48-
const J1QL_LIMIT_COUNT = 250;
4948

5049
const JobStatus = {
5150
IN_PROGRESS: 'IN_PROGRESS',
@@ -75,10 +74,8 @@ export class FetchError extends Error {
7574
nameForLogging?: string;
7675
}) {
7776
super(
78-
`JupiterOne API error. Response not OK (requestName=${
79-
options.nameForLogging || '(none)'
80-
}, status=${options.response.status}, url=${options.url}, method=${
81-
options.method
77+
`JupiterOne API error. Response not OK (requestName=${options.nameForLogging || '(none)'
78+
}, status=${options.response.status}, url=${options.url}, method=${options.method
8279
}). Response: ${options.responseBody}`,
8380
);
8481
this.httpStatusCode = options.response.status;
@@ -327,7 +324,7 @@ export class JupiterOneClient {
327324
const token = this.accessToken;
328325
this.headers = {
329326
Authorization: `Bearer ${token}`,
330-
'LifeOmic-Account': this.account,
327+
'JupiterOne-Account': this.account,
331328
'content-type': 'application/json',
332329
};
333330

@@ -351,84 +348,88 @@ export class JupiterOneClient {
351348
async queryV1(
352349
j1ql: string,
353350
options: QueryOptions | Record<string, unknown> = {},
351+
/**
352+
* include a progress bar to show progress of getting data.
353+
*/
354+
showProgress = false,
354355
/** because this method queries repeatedly with its own LIMIT,
355-
* this limits the looping to stop after at least {stopAfter} results are found */
356+
* this limits the looping to stop after at least {stopAfter} results are found
357+
* @deprecated This property is no longer supported.
358+
*/
356359
stopAfter = Number.MAX_SAFE_INTEGER,
357360
/** same as above, this gives more fine-grained control over the starting point of the query,
358361
* since this method controls the `SKIP` clause in the query
362+
* @deprecated This property is no longer supported.
359363
*/
360364
startPage = 0,
361365
) {
366+
367+
let cursor: string;
362368
let complete = false;
363-
let page = startPage;
364369
let results: any[] = [];
365370

366-
while (!complete && results.length < stopAfter) {
367-
const j1qlForPage = `${j1ql} SKIP ${
368-
page * J1QL_SKIP_COUNT
369-
} LIMIT ${J1QL_LIMIT_COUNT}`;
371+
const limitCheck = j1ql.match(/limit (\d+)/i);
372+
373+
let progress: any;
370374

375+
do {
371376
const res = await this.graphClient.query({
372377
query: QUERY_V1,
373378
variables: {
374-
query: j1qlForPage,
379+
query: j1ql,
375380
deferredResponse: 'FORCE',
381+
flags: {
382+
variableResultSize: true
383+
},
384+
cursor
376385
},
377386
...options,
378387
});
379-
page++;
380388
if (res.errors) {
381389
throw new Error(`JupiterOne returned error(s) for query: '${j1ql}'`);
382390
}
383391

384392
const deferredUrl = res.data.queryV1.url;
385393
let status = JobStatus.IN_PROGRESS;
386-
let statusFile;
394+
let statusFile: any;
387395
const startTimeInMs = Date.now();
388396
do {
389397
if (Date.now() - startTimeInMs > QUERY_RESULTS_TIMEOUT) {
390398
throw new Error(
391-
`Exceeded request timeout of ${
392-
QUERY_RESULTS_TIMEOUT / 1000
399+
`Exceeded request timeout of ${QUERY_RESULTS_TIMEOUT / 1000
393400
} seconds.`,
394401
);
395402
}
396403
this.logger.trace('Sleeping to wait for JobCompletion');
397-
await sleep(200);
404+
await sleep(100);
398405
statusFile = await networkRequest(deferredUrl);
399406
status = statusFile.status;
407+
cursor = statusFile.cursor;
400408
} while (status === JobStatus.IN_PROGRESS);
401409

402-
let result;
403-
if (status === JobStatus.COMPLETED) {
404-
result = await networkRequest(statusFile.url);
405-
} else {
406-
// JobStatus.FAILED
407-
throw new Error(
408-
statusFile.error || 'Job failed without an error message.',
409-
);
410+
if (status === JobStatus.FAILED) {
411+
throw new Error(`JupiterOne returned error(s) for query: '${statusFile.error}'`);
410412
}
411413

412-
const { data } = result;
414+
const result = statusFile.data;
413415

414-
// data will assume tree shape if you specify 'return tree' in J1QL
415-
const isTree = data.vertices && data.edges;
416+
if (showProgress && !limitCheck) {
417+
if (results.length === 0) {
418+
progress = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
419+
progress.start(Number(statusFile.totalCount), 0);
420+
}
421+
progress.update(results.length);
422+
}
423+
424+
if (result) {
425+
results = results.concat(result)
426+
}
416427

417-
if (isTree) {
428+
if (status === JobStatus.COMPLETED && (cursor == null || limitCheck)) {
418429
complete = true;
419-
results = data;
420-
} else {
421-
// data is array-shaped, possibly paginated
422-
if (data.length < J1QL_SKIP_COUNT) {
423-
complete = true;
424-
}
425-
results = results.concat(data);
426430
}
427-
this.logger.debug(
428-
{ resultsCount: results.length, pageCount: data.length },
429-
'Query received page of results',
430-
);
431-
}
431+
432+
} while (complete === false);
432433
return results;
433434
}
434435

@@ -887,7 +888,7 @@ export class JupiterOneClient {
887888
const headers = this.headers;
888889
const response = await makeFetchRequest(
889890
this.apiUrl +
890-
`/persister/synchronization/jobs/${options.syncJobId}/upload`,
891+
`/persister/synchronization/jobs/${options.syncJobId}/upload`,
891892
{
892893
method: 'POST',
893894
headers,

src/queries.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,17 @@ export const QUERY_V1 = gql`
111111
$includeDeleted: Boolean
112112
$deferredResponse: DeferredResponseOption
113113
$deferredFormat: DeferredResponseFormat
114+
$cursor: String
115+
$flags: QueryV1Flags
114116
) {
115117
queryV1(
116118
query: $query
117119
variables: $variables
118120
includeDeleted: $includeDeleted
119121
deferredResponse: $deferredResponse
120122
deferredFormat: $deferredFormat
123+
cursor: $cursor
124+
flags: $flags
121125
) {
122126
type
123127
data

yarn.lock

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,13 @@ cli-cursor@^3.1.0:
13441344
dependencies:
13451345
restore-cursor "^3.1.0"
13461346

1347+
cli-progress@^3.12.0:
1348+
version "3.12.0"
1349+
resolved "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz#807ee14b66bcc086258e444ad0f19e7d42577942"
1350+
integrity sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==
1351+
dependencies:
1352+
string-width "^4.2.3"
1353+
13471354
cli-spinners@^2.5.0:
13481355
version "2.6.1"
13491356
resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz"
@@ -3589,7 +3596,7 @@ string-length@^4.0.1:
35893596
char-regex "^1.0.2"
35903597
strip-ansi "^6.0.0"
35913598

3592-
string-width@^4.1.0, string-width@^4.2.0:
3599+
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
35933600
version "4.2.3"
35943601
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
35953602
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==

0 commit comments

Comments
 (0)