@flowcore/cli 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -2
- package/README.md +11 -8
- package/dist/commands/stream.d.ts +3 -0
- package/dist/commands/stream.js +59 -11
- package/dist/utils/queries/fetch-indexes.gql.d.ts +18 -0
- package/dist/utils/queries/fetch-indexes.gql.js +18 -0
- package/oclif.manifest.json +18 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [1.3.0](https://github.com/flowcore-io/flowcore-cli/compare/v1.2.0...v1.3.0) (2024-01-09)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* added timeout for destination as flag ([f731aac](https://github.com/flowcore-io/flowcore-cli/commit/f731aac025262179af1fd56e2ab2477a674635f9))
|
|
2
7
|
|
|
3
8
|
|
|
4
9
|
### Bug Fixes
|
|
5
10
|
|
|
6
|
-
*
|
|
11
|
+
* added description to start that includes week ([58687a7](https://github.com/flowcore-io/flowcore-cli/commit/58687a7bbb66aaa5d6da26af88e555cbb1e72467))
|
|
12
|
+
|
|
13
|
+
## [2.0.0](https://github.com/flowcore-io/flowcore-cli/compare/v1.3.0...v2.0.0) (2024-01-15)
|
|
7
14
|
|
|
8
15
|
|
|
16
|
+
### ⚠ BREAKING CHANGES
|
|
17
|
+
|
|
18
|
+
* default mode now does not scan all time buckets but fetches populated ones from the index
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* added fetch of time bucket indexes to speed up fetching events, set --scan to query all time buckets within the requested range. ([650a7cd](https://github.com/flowcore-io/flowcore-cli/commit/650a7cd120ab372148abc79e3c105eee2687b466))
|
|
9
23
|
|
|
10
24
|
## [1.2.0](https://github.com/flowcore-io/flowcore-cli/compare/v1.1.2...v1.2.0) (2024-01-09)
|
|
11
25
|
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ $ npm install -g @flowcore/cli
|
|
|
17
17
|
$ flowcore COMMAND
|
|
18
18
|
running command...
|
|
19
19
|
$ flowcore (--version)
|
|
20
|
-
@flowcore/cli/
|
|
20
|
+
@flowcore/cli/2.0.0 linux-x64 node-v20.10.0
|
|
21
21
|
$ flowcore --help [COMMAND]
|
|
22
22
|
USAGE
|
|
23
23
|
$ flowcore COMMAND
|
|
@@ -103,7 +103,7 @@ EXAMPLES
|
|
|
103
103
|
$ flowcore config set -l https://auth.flowcore.io/realms/flowcore/.well-known/openid-configuration -c my-client-id -p
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
_See code: [src/commands/config/set.ts](https://github.com/flowcore-io/flowcore-cli/blob/
|
|
106
|
+
_See code: [src/commands/config/set.ts](https://github.com/flowcore-io/flowcore-cli/blob/v2.0.0/src/commands/config/set.ts)_
|
|
107
107
|
|
|
108
108
|
## `flowcore config show`
|
|
109
109
|
|
|
@@ -123,7 +123,7 @@ EXAMPLES
|
|
|
123
123
|
$ flowcore config show
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
_See code: [src/commands/config/show.ts](https://github.com/flowcore-io/flowcore-cli/blob/
|
|
126
|
+
_See code: [src/commands/config/show.ts](https://github.com/flowcore-io/flowcore-cli/blob/v2.0.0/src/commands/config/show.ts)_
|
|
127
127
|
|
|
128
128
|
## `flowcore help [COMMANDS]`
|
|
129
129
|
|
|
@@ -166,7 +166,7 @@ EXAMPLES
|
|
|
166
166
|
$ flowcore login --port 8080
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
_See code: [src/commands/login.ts](https://github.com/flowcore-io/flowcore-cli/blob/
|
|
169
|
+
_See code: [src/commands/login.ts](https://github.com/flowcore-io/flowcore-cli/blob/v2.0.0/src/commands/login.ts)_
|
|
170
170
|
|
|
171
171
|
## `flowcore plugins`
|
|
172
172
|
|
|
@@ -449,18 +449,21 @@ Stream events from a datacore running on the Flowcore Platform
|
|
|
449
449
|
|
|
450
450
|
```
|
|
451
451
|
USAGE
|
|
452
|
-
$ flowcore stream STREAM [--profile <value>] [-d <value>] [-j] [-l] [-o http|log] [-s <value>]
|
|
452
|
+
$ flowcore stream STREAM [--profile <value>] [-d <value>] [-j] [-l] [-o http|log] [-c] [-s <value>] [-t
|
|
453
|
+
<value>]
|
|
453
454
|
|
|
454
455
|
ARGUMENTS
|
|
455
456
|
STREAM stream url to connect to
|
|
456
457
|
|
|
457
458
|
FLAGS
|
|
459
|
+
-c, --scan Scan the full time range
|
|
458
460
|
-d, --destination=<value> [default: http://localhost:3000/transform] Destination to send events to
|
|
459
461
|
-j, --json Output json only
|
|
460
462
|
-l, --[no-]live Change to live mode when reaching last time bucket
|
|
461
463
|
-o, --output=<option> [default: http] Output format
|
|
462
464
|
<options: http|log>
|
|
463
|
-
-s, --start=<value> Start time bucket to stream from, example: (1y, 1m, 1d, 1h)
|
|
465
|
+
-s, --start=<value> Start time bucket to stream from, example: (1y, 1m, 1w, 1d, 1h)
|
|
466
|
+
-t, --timeout=<value> [default: 5000] Timeout in milliseconds to wait for a response from the destination
|
|
464
467
|
--profile=<value> Specify the configuration profile to use
|
|
465
468
|
|
|
466
469
|
DESCRIPTION
|
|
@@ -476,7 +479,7 @@ EXAMPLES
|
|
|
476
479
|
$ flowcore stream https://flowcore.io/<org>/<data core>/<flow type>/[<event type1>,<event type2>,<event type3>].stream -o log -s 3m
|
|
477
480
|
```
|
|
478
481
|
|
|
479
|
-
_See code: [src/commands/stream.ts](https://github.com/flowcore-io/flowcore-cli/blob/
|
|
482
|
+
_See code: [src/commands/stream.ts](https://github.com/flowcore-io/flowcore-cli/blob/v2.0.0/src/commands/stream.ts)_
|
|
480
483
|
|
|
481
484
|
## `flowcore version`
|
|
482
485
|
|
|
@@ -513,5 +516,5 @@ DESCRIPTION
|
|
|
513
516
|
Check what user you are logged in as
|
|
514
517
|
```
|
|
515
518
|
|
|
516
|
-
_See code: [src/commands/whoami.ts](https://github.com/flowcore-io/flowcore-cli/blob/
|
|
519
|
+
_See code: [src/commands/whoami.ts](https://github.com/flowcore-io/flowcore-cli/blob/v2.0.0/src/commands/whoami.ts)_
|
|
517
520
|
<!-- commandsstop -->
|
|
@@ -10,9 +10,12 @@ export default class Stream extends BaseCommand<typeof Stream> {
|
|
|
10
10
|
json: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
11
|
live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
12
12
|
output: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
scan: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
13
14
|
start: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
15
|
+
timeout: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
14
16
|
};
|
|
15
17
|
run(): Promise<void>;
|
|
18
|
+
private fetchTimeBuckets;
|
|
16
19
|
private processEvents;
|
|
17
20
|
private setTimeBucket;
|
|
18
21
|
private streamEvents;
|
package/dist/commands/stream.js
CHANGED
|
@@ -3,13 +3,15 @@ import { Args, Flags, ux } from '@oclif/core';
|
|
|
3
3
|
import dayjs from "dayjs";
|
|
4
4
|
import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js";
|
|
5
5
|
import utc from "dayjs/plugin/utc.js";
|
|
6
|
+
import _ from "lodash";
|
|
6
7
|
import { Subject } from "rxjs";
|
|
7
8
|
import { BaseCommand } from "../base-command.js";
|
|
8
9
|
import { QueryGraphQL } from "../utils/graphql.util.js";
|
|
9
10
|
import { FETCH_DATA_CORE_GQL_QUERY } from "../utils/queries/fetch-data-core.gql.js";
|
|
10
11
|
import { FETCH_EVENT_TYPE_RANGE_GQL_QUERY } from "../utils/queries/fetch-event-type-range.gql.js";
|
|
11
12
|
import { FETCH_EVENTS_GQL_QUERY } from "../utils/queries/fetch-events.gql.js";
|
|
12
|
-
import {
|
|
13
|
+
import { FETCH_CATALOG_INDEXES_QUERY } from "../utils/queries/fetch-indexes.gql.js";
|
|
14
|
+
import { TIME_BUCKET_HOUR_PATTERN, TIME_BUCKET_PATTERN, createTimebucket, createTimebucketDayjs } from "../utils/timebucket.util.js";
|
|
13
15
|
import { LOGIN_CODES, ValidateLogin } from "../utils/validate-login.util.js";
|
|
14
16
|
dayjs.extend(utc);
|
|
15
17
|
dayjs.extend(isSameOrBefore);
|
|
@@ -29,7 +31,9 @@ export default class Stream extends BaseCommand {
|
|
|
29
31
|
json: Flags.boolean({ char: 'j', description: 'Output json only' }),
|
|
30
32
|
live: Flags.boolean({ allowNo: true, char: 'l', default: true, description: 'Change to live mode when reaching last time bucket' }),
|
|
31
33
|
output: Flags.string({ char: 'o', default: 'http', description: 'Output format', options: ['http', 'log'] }),
|
|
32
|
-
|
|
34
|
+
scan: Flags.boolean({ char: 'c', default: false, description: 'Scan the full time range' }),
|
|
35
|
+
start: Flags.string({ char: 's', description: 'Start time bucket to stream from, example: (1y, 1m, 1w, 1d, 1h)' }),
|
|
36
|
+
timeout: Flags.integer({ char: 't', default: 5000, description: 'Timeout in milliseconds to wait for a response from the destination' }),
|
|
33
37
|
};
|
|
34
38
|
async run() {
|
|
35
39
|
const config = this.cliConfiguration.getConfig();
|
|
@@ -61,6 +65,9 @@ export default class Stream extends BaseCommand {
|
|
|
61
65
|
dataCore,
|
|
62
66
|
organization: org,
|
|
63
67
|
});
|
|
68
|
+
if (fetchDataCoreResponse.organization.datacores.length === 0) {
|
|
69
|
+
ux.error(`Data core ${dataCore} not found for organization ${org}`);
|
|
70
|
+
}
|
|
64
71
|
const dataCoreId = fetchDataCoreResponse.organization.datacores[0].id;
|
|
65
72
|
const eventRangeRequest = await graphqlClient.client.request(FETCH_EVENT_TYPE_RANGE_GQL_QUERY, {
|
|
66
73
|
aggregator,
|
|
@@ -90,6 +97,40 @@ export default class Stream extends BaseCommand {
|
|
|
90
97
|
void this.streamEvents(dataCoreId, aggregator, eventTypes, firstTimeBucket, lastTimeBucket, graphqlClient, observer, flags.live);
|
|
91
98
|
await this.processEvents(observer, flags.destination, flags.output);
|
|
92
99
|
}
|
|
100
|
+
async fetchTimeBuckets(dataCoreId, aggregator, eventTypes, startTimeBucket, lastTimeBucket, graphqlClient) {
|
|
101
|
+
!this.flags.json && ux.action.start("Fetching time buckets");
|
|
102
|
+
let timeBuckets = [];
|
|
103
|
+
for (const eventType of eventTypes) {
|
|
104
|
+
let cursor = null;
|
|
105
|
+
do {
|
|
106
|
+
// eslint-disable-next-line no-await-in-loop
|
|
107
|
+
await graphqlClient.validateToken();
|
|
108
|
+
try {
|
|
109
|
+
// eslint-disable-next-line no-await-in-loop
|
|
110
|
+
const response = await graphqlClient.client.request(FETCH_CATALOG_INDEXES_QUERY, {
|
|
111
|
+
aggregator,
|
|
112
|
+
cursor: cursor || undefined,
|
|
113
|
+
dataCoreId,
|
|
114
|
+
eventType,
|
|
115
|
+
fromTimeBucket: startTimeBucket.format(TIME_BUCKET_PATTERN),
|
|
116
|
+
toTimeBucket: lastTimeBucket,
|
|
117
|
+
});
|
|
118
|
+
cursor = response.datacore.fetchIndexes.cursor;
|
|
119
|
+
timeBuckets.push(...response.datacore.fetchIndexes.timeBuckets);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
// don't display error if token is expired
|
|
123
|
+
if (error instanceof Error && error.message.includes("Token is expired")) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
this.warn(`Error fetching events: ${error}`);
|
|
127
|
+
}
|
|
128
|
+
} while (cursor);
|
|
129
|
+
}
|
|
130
|
+
timeBuckets = _.uniq(timeBuckets);
|
|
131
|
+
!this.flags.json && ux.action.stop(`Found ${timeBuckets.length} time buckets`);
|
|
132
|
+
return timeBuckets;
|
|
133
|
+
}
|
|
93
134
|
async processEvents(observer, destination, output) {
|
|
94
135
|
let done = false;
|
|
95
136
|
let events = [];
|
|
@@ -130,7 +171,7 @@ export default class Stream extends BaseCommand {
|
|
|
130
171
|
},
|
|
131
172
|
method: "POST",
|
|
132
173
|
}),
|
|
133
|
-
new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Timeout waiting for response")),
|
|
174
|
+
new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Timeout waiting for response")), this.flags.timeout); }),]);
|
|
134
175
|
if (!result.ok) {
|
|
135
176
|
!this.flags.json && this.warn(`Error sending event to ${destination}: ${result.statusText}`);
|
|
136
177
|
}
|
|
@@ -154,7 +195,7 @@ export default class Stream extends BaseCommand {
|
|
|
154
195
|
}
|
|
155
196
|
}
|
|
156
197
|
setTimeBucket(date, start) {
|
|
157
|
-
const parts = start.match(/(\d+)([
|
|
198
|
+
const parts = start.match(/(\d+)([dhmwy])/i);
|
|
158
199
|
if (!parts) {
|
|
159
200
|
ux.error(`Invalid start time bucket, generated from ${start}`);
|
|
160
201
|
}
|
|
@@ -180,13 +221,22 @@ export default class Stream extends BaseCommand {
|
|
|
180
221
|
}
|
|
181
222
|
}
|
|
182
223
|
async streamEvents(dataCoreId, aggregator, eventTypes, firstTimeBucket, lastTimeBucket, graphqlClient, observer, live) {
|
|
183
|
-
|
|
224
|
+
const startTimeBucket = dayjs(firstTimeBucket, TIME_BUCKET_HOUR_PATTERN);
|
|
184
225
|
const currentTime = dayjs.utc();
|
|
185
226
|
let liveMode = false;
|
|
186
227
|
let lastEventId = null;
|
|
228
|
+
const timediff = dayjs(lastTimeBucket, TIME_BUCKET_HOUR_PATTERN).diff(startTimeBucket, "hour");
|
|
229
|
+
if (timediff < 0) {
|
|
230
|
+
ux.error(`Invalid time range, start time bucket (${ux.colorize("yellow", firstTimeBucket)}) is after last time bucket (${ux.colorize("yellow", lastTimeBucket)})`);
|
|
231
|
+
}
|
|
232
|
+
let timeBuckets = [];
|
|
233
|
+
timeBuckets = this.flags.scan ? _.times(timediff + 1, (n) => dayjs(startTimeBucket).add(n, "hour").format(TIME_BUCKET_PATTERN)) : (await this.fetchTimeBuckets(dataCoreId, aggregator, eventTypes, startTimeBucket, lastTimeBucket, graphqlClient));
|
|
187
234
|
// eslint-disable-next-line no-constant-condition
|
|
188
235
|
while (true) {
|
|
189
|
-
const timeBucket =
|
|
236
|
+
const timeBucket = timeBuckets.shift();
|
|
237
|
+
if (!timeBucket) {
|
|
238
|
+
ux.error("No time bucket found");
|
|
239
|
+
}
|
|
190
240
|
if (!this.flags.json) {
|
|
191
241
|
ux.action.start(`Fetching events for ${startTimeBucket}`);
|
|
192
242
|
}
|
|
@@ -203,6 +253,7 @@ export default class Stream extends BaseCommand {
|
|
|
203
253
|
const response = await graphqlClient.client.request(FETCH_EVENTS_GQL_QUERY, {
|
|
204
254
|
afterEventId: lastEventId || undefined,
|
|
205
255
|
aggregator,
|
|
256
|
+
cursor: cursor || undefined,
|
|
206
257
|
dataCoreId,
|
|
207
258
|
eventTypes,
|
|
208
259
|
timeBucket,
|
|
@@ -222,10 +273,7 @@ export default class Stream extends BaseCommand {
|
|
|
222
273
|
this.warn(`Error fetching events: ${error}`);
|
|
223
274
|
}
|
|
224
275
|
} while (cursor);
|
|
225
|
-
if (
|
|
226
|
-
startTimeBucket = startTimeBucket.add(1, "hour");
|
|
227
|
-
}
|
|
228
|
-
if (timeBucket === lastTimeBucket) {
|
|
276
|
+
if (timeBuckets.length === 0) {
|
|
229
277
|
if (!live) {
|
|
230
278
|
observer.complete();
|
|
231
279
|
if (!this.flags.json) {
|
|
@@ -235,9 +283,9 @@ export default class Stream extends BaseCommand {
|
|
|
235
283
|
}
|
|
236
284
|
if (!liveMode) {
|
|
237
285
|
!this.flags.json && this.warn("Reached last time bucket, switching to live mode");
|
|
238
|
-
startTimeBucket = createTimebucketDayjs(currentTime);
|
|
239
286
|
liveMode = true;
|
|
240
287
|
}
|
|
288
|
+
timeBuckets.push(createTimebucketDayjs(currentTime).format(TIME_BUCKET_PATTERN));
|
|
241
289
|
}
|
|
242
290
|
if (liveMode && !found) {
|
|
243
291
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type FetchCatalogIndexesResponse = {
|
|
2
|
+
datacore: {
|
|
3
|
+
fetchIndexes: {
|
|
4
|
+
cursor: string;
|
|
5
|
+
timeBuckets: string[];
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export type FetchCatalogIndexesInput = {
|
|
10
|
+
aggregator: string;
|
|
11
|
+
cursor?: string;
|
|
12
|
+
dataCoreId: string;
|
|
13
|
+
eventType: string;
|
|
14
|
+
fromTimeBucket?: string;
|
|
15
|
+
pageSize?: number;
|
|
16
|
+
toTimeBucket?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare const FETCH_CATALOG_INDEXES_QUERY: string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { gql } from "graphql-request";
|
|
2
|
+
export const FETCH_CATALOG_INDEXES_QUERY = gql `
|
|
3
|
+
query FLOWCORE_CLI_FETCH_CATALOG_INDEXES($dataCoreId: ID!, $aggregator: String!, $eventType: String!, $cursor: String, $fromTimeBucket: String, $toTimeBucket: String, $pageSize: Int) {
|
|
4
|
+
datacore(search: {id: $dataCoreId}) {
|
|
5
|
+
fetchIndexes(input: {
|
|
6
|
+
aggregator: $aggregator,
|
|
7
|
+
eventType: $eventType,
|
|
8
|
+
cursor: $cursor
|
|
9
|
+
fromTimeBucket: $fromTimeBucket
|
|
10
|
+
toTimeBucket: $toTimeBucket
|
|
11
|
+
pageSize: $pageSize
|
|
12
|
+
}) {
|
|
13
|
+
timeBuckets
|
|
14
|
+
cursor
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
`;
|
package/oclif.manifest.json
CHANGED
|
@@ -101,13 +101,29 @@
|
|
|
101
101
|
],
|
|
102
102
|
"type": "option"
|
|
103
103
|
},
|
|
104
|
+
"scan": {
|
|
105
|
+
"char": "c",
|
|
106
|
+
"description": "Scan the full time range",
|
|
107
|
+
"name": "scan",
|
|
108
|
+
"allowNo": false,
|
|
109
|
+
"type": "boolean"
|
|
110
|
+
},
|
|
104
111
|
"start": {
|
|
105
112
|
"char": "s",
|
|
106
|
-
"description": "Start time bucket to stream from, example: (1y, 1m, 1d, 1h)",
|
|
113
|
+
"description": "Start time bucket to stream from, example: (1y, 1m, 1w, 1d, 1h)",
|
|
107
114
|
"name": "start",
|
|
108
115
|
"hasDynamicHelp": false,
|
|
109
116
|
"multiple": false,
|
|
110
117
|
"type": "option"
|
|
118
|
+
},
|
|
119
|
+
"timeout": {
|
|
120
|
+
"char": "t",
|
|
121
|
+
"description": "Timeout in milliseconds to wait for a response from the destination",
|
|
122
|
+
"name": "timeout",
|
|
123
|
+
"default": 5000,
|
|
124
|
+
"hasDynamicHelp": false,
|
|
125
|
+
"multiple": false,
|
|
126
|
+
"type": "option"
|
|
111
127
|
}
|
|
112
128
|
},
|
|
113
129
|
"hasDynamicHelp": false,
|
|
@@ -259,5 +275,5 @@
|
|
|
259
275
|
]
|
|
260
276
|
}
|
|
261
277
|
},
|
|
262
|
-
"version": "
|
|
278
|
+
"version": "2.0.0"
|
|
263
279
|
}
|
package/package.json
CHANGED
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"prestart": "npm run build",
|
|
87
87
|
"update-schema": "rover graph introspect https://graph.api.staging.flowcore.io/graphql -o schema.gql"
|
|
88
88
|
},
|
|
89
|
-
"version": "
|
|
89
|
+
"version": "2.0.0",
|
|
90
90
|
"bugs": "https://github.com/flowcore-io/flowcore-cli/issues",
|
|
91
91
|
"keywords": [
|
|
92
92
|
"flowcore",
|