@flowcore/cli 1.0.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +308 -0
  2. package/README.md +510 -0
  3. package/bin/dev.cmd +3 -0
  4. package/bin/dev.js +9 -0
  5. package/bin/run.cmd +3 -0
  6. package/bin/run.js +8 -0
  7. package/dist/base-command.d.ts +17 -0
  8. package/dist/base-command.js +32 -0
  9. package/dist/commands/config/set.d.ts +13 -0
  10. package/dist/commands/config/set.js +45 -0
  11. package/dist/commands/config/show.d.ts +6 -0
  12. package/dist/commands/config/show.js +11 -0
  13. package/dist/commands/login.d.ts +19 -0
  14. package/dist/commands/login.js +146 -0
  15. package/dist/commands/stream.d.ts +18 -0
  16. package/dist/commands/stream.js +220 -0
  17. package/dist/commands/whoami.d.ts +5 -0
  18. package/dist/commands/whoami.js +25 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/interfaces/source-event.interface.d.ts +18 -0
  22. package/dist/interfaces/source-event.interface.js +1 -0
  23. package/dist/utils/config.util.d.ts +31 -0
  24. package/dist/utils/config.util.js +108 -0
  25. package/dist/utils/graphql.util.d.ts +8 -0
  26. package/dist/utils/graphql.util.js +25 -0
  27. package/dist/utils/object-path.util.d.ts +1 -0
  28. package/dist/utils/object-path.util.js +17 -0
  29. package/dist/utils/queries/fetch-data-core.gql.d.ts +12 -0
  30. package/dist/utils/queries/fetch-data-core.gql.js +10 -0
  31. package/dist/utils/queries/fetch-event-type-range.gql.d.ts +21 -0
  32. package/dist/utils/queries/fetch-event-type-range.gql.js +19 -0
  33. package/dist/utils/queries/fetch-events.gql.d.ts +28 -0
  34. package/dist/utils/queries/fetch-events.gql.js +28 -0
  35. package/dist/utils/timebucket.util.d.ts +5 -0
  36. package/dist/utils/timebucket.util.js +11 -0
  37. package/dist/utils/validate-login.util.d.ts +24 -0
  38. package/dist/utils/validate-login.util.js +111 -0
  39. package/oclif.manifest.json +253 -0
  40. package/package.json +102 -0
@@ -0,0 +1,146 @@
1
+ import { Flags } from '@oclif/core';
2
+ import express from 'express';
3
+ import open from "open";
4
+ import { Issuer, generators } from "openid-client";
5
+ import { BaseCommand } from "../base-command.js";
6
+ export default class Login extends BaseCommand {
7
+ static description = 'login to the Flowcore Platform';
8
+ static examples = [
9
+ '<%= config.bin %> <%= command.id %>',
10
+ '<%= config.bin %> <%= command.id %> --port 8080',
11
+ ];
12
+ static flags = {
13
+ port: Flags.integer({ char: 'p', default: 3000, description: 'port to listen for the callback' }),
14
+ };
15
+ client;
16
+ server;
17
+ catch(err) {
18
+ this.server?.close();
19
+ return super.catch(err);
20
+ }
21
+ async init() {
22
+ await super.init();
23
+ try {
24
+ const { login } = this.cliConfiguration.getConfig();
25
+ const oidcIssuer = await Issuer.discover(login.url);
26
+ this.client = new oidcIssuer.Client({
27
+ // eslint-disable-next-line camelcase
28
+ client_id: login.clientId,
29
+ // eslint-disable-next-line camelcase
30
+ ...(login.clientSecret && { client_secret: login.clientSecret }),
31
+ // eslint-disable-next-line camelcase
32
+ redirect_uris: [`http://localhost:${login.callbackPort}/callback`],
33
+ // eslint-disable-next-line camelcase
34
+ response_types: ['code'],
35
+ // eslint-disable-next-line camelcase
36
+ token_endpoint_auth_method: 'none',
37
+ });
38
+ }
39
+ catch (error) {
40
+ this.error(`Failed to discover the openid configuration: ${error}`);
41
+ }
42
+ }
43
+ async run() {
44
+ if (!this.client) {
45
+ this.error("No client configured");
46
+ }
47
+ const { login } = this.cliConfiguration.getConfig();
48
+ const codeVerifier = generators.codeVerifier();
49
+ const codeChallenge = generators.codeChallenge(codeVerifier);
50
+ const loginUrl = this.client.authorizationUrl({
51
+ // eslint-disable-next-line camelcase
52
+ code_challenge: codeChallenge,
53
+ // eslint-disable-next-line camelcase
54
+ code_challenge_method: 'S256',
55
+ scope: 'openid profile email flowcore_user offline_access',
56
+ });
57
+ const app = express();
58
+ app.get('/callback', async (req, res) => {
59
+ const params = this.client.callbackParams(req);
60
+ // eslint-disable-next-line camelcase
61
+ const tokenSet = await this.client.callback(`http://localhost:${port}/callback`, params, { code_verifier: codeVerifier });
62
+ if (!tokenSet.claims()) {
63
+ this.error("No claims in token set");
64
+ }
65
+ if (tokenSet.claims()) {
66
+ this.log(`Logged in as ${tokenSet.claims().preferred_username}`);
67
+ }
68
+ this.writeToken(tokenSet);
69
+ const claims = tokenSet.claims();
70
+ res.send(`
71
+ <!DOCTYPE html>
72
+ <html lang="en">
73
+ <head>
74
+ <meta charset="UTF-8">
75
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
76
+ <link rel="icon" href="https://flowcore.com/favicon.ico" type="image/x-icon" sizes="any">
77
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
78
+ <title>Login Successful</title>
79
+ <style>
80
+ body {
81
+ font-family: Arial, sans-serif;
82
+ background-color: #000000;
83
+ color: #FFFFFF;
84
+ text-align: center;
85
+ padding: 50px;
86
+ }
87
+
88
+ h1 {
89
+ font-size: 32px;
90
+ margin-bottom: 20px;
91
+ }
92
+
93
+ p {
94
+ font-size: 16px;
95
+ margin-bottom: 30px;
96
+ }
97
+
98
+ .success-icon {
99
+ color: #66bd6a;
100
+ font-size: 48px;
101
+ margin-bottom: 20px;
102
+ }
103
+
104
+ .button {
105
+ background-color: #ff4ba2;
106
+ color: #FFFFFF;
107
+ padding: 10px 20px;
108
+ font-size: 18px;
109
+ border: none;
110
+ border-radius: 4px;
111
+ cursor: pointer;
112
+ }
113
+
114
+ .button:hover {
115
+ background-color: #1976D2;
116
+ }
117
+ </style>
118
+ </head>
119
+ <body>
120
+ <div class="success-icon">
121
+ <i class="fa fa-check-circle"></i>
122
+ </div>
123
+ <h1>Login Successful</h1>
124
+ <p>Hello ${claims.preferred_username}! You have successfully logged in. You can now close this window.</p>
125
+ <button class="button" onclick="window.close();">Close Window</button>
126
+ </body>
127
+ </html>
128
+ `);
129
+ this.server?.close();
130
+ });
131
+ const port = login.callbackPort;
132
+ await open(loginUrl, { wait: false });
133
+ this.server = app.listen(port, () => {
134
+ this.log(`Listening for authentication callback on port ${port}`);
135
+ });
136
+ }
137
+ writeToken(tokenSet) {
138
+ this.cliConfiguration.setConfig({
139
+ auth: {
140
+ accessToken: tokenSet.access_token,
141
+ idToken: tokenSet.id_token,
142
+ refreshToken: tokenSet.refresh_token,
143
+ }
144
+ });
145
+ }
146
+ }
@@ -0,0 +1,18 @@
1
+ import { BaseCommand } from "../base-command.js";
2
+ export default class Stream extends BaseCommand<typeof Stream> {
3
+ static args: {
4
+ STREAM: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ destination: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
10
+ live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ output: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
+ start: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
+ };
14
+ run(): Promise<void>;
15
+ private processEvents;
16
+ private setTimeBucket;
17
+ private streanEvents;
18
+ }
@@ -0,0 +1,220 @@
1
+ import { Queue } from "@datastructures-js/queue";
2
+ import { Args, Flags, ux } from '@oclif/core';
3
+ import dayjs from "dayjs";
4
+ import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js";
5
+ import utc from "dayjs/plugin/utc.js";
6
+ import { Subject } from "rxjs";
7
+ import { BaseCommand } from "../base-command.js";
8
+ import { QueryGraphQL } from "../utils/graphql.util.js";
9
+ import { FETCH_DATA_CORE_GQL_QUERY } from "../utils/queries/fetch-data-core.gql.js";
10
+ import { FETCH_EVENT_TYPE_RANGE_GQL_QUERY } from "../utils/queries/fetch-event-type-range.gql.js";
11
+ import { FETCH_EVENTS_GQL_QUERY } from "../utils/queries/fetch-events.gql.js";
12
+ import { TIME_BUCKET_HOUR_PATTERN, createTimebucket, createTimebucketDayjs } from "../utils/timebucket.util.js";
13
+ import { LOGIN_CODES, ValidateLogin } from "../utils/validate-login.util.js";
14
+ dayjs.extend(utc);
15
+ dayjs.extend(isSameOrBefore);
16
+ export default class Stream extends BaseCommand {
17
+ static args = {
18
+ STREAM: Args.string({ description: 'stream url to connect to', required: true }),
19
+ };
20
+ static description = 'Stream events from a datacore running on the Flowcore Platform';
21
+ static examples = [
22
+ '<%= config.bin %> <%= command.id %> https://staging.flowcore.io/flowcore/flowcore-platform/organization.0/event.organization.subscription.updated-requested.0.stream',
23
+ ];
24
+ static flags = {
25
+ destination: Flags.string({ char: 'd', default: "http://localhost:3000/transform", description: 'Destination to send events to' }),
26
+ live: Flags.boolean({ char: 'l', default: true, description: 'Change to live mode when reaching last time bucket' }),
27
+ output: Flags.string({ char: 'o', default: 'http', description: 'Output format', options: ['http', 'log'] }),
28
+ start: Flags.string({ char: 's', description: 'Start time bucket to stream from, example: (1y, 1m, 1d, 1h)' }),
29
+ };
30
+ async run() {
31
+ const config = this.cliConfiguration.getConfig();
32
+ const loginValidator = new ValidateLogin(config.login.url);
33
+ const validateLogin = await loginValidator.validate(config, this.cliConfiguration);
34
+ const { api, auth } = config;
35
+ if (validateLogin.status !== LOGIN_CODES.LOGIN_SUCCESS || !auth) {
36
+ ux.error(`Not logged in, please run ${ux.colorize("yellow", "flowcore login")} first`);
37
+ }
38
+ if (!api.url) {
39
+ ux.error("No api url configured");
40
+ }
41
+ const graphqlClient = new QueryGraphQL(api.url, auth.accessToken, async () => {
42
+ if (!await loginValidator.isExpired(config, this.cliConfiguration)) {
43
+ return false;
44
+ }
45
+ return config.auth.accessToken;
46
+ });
47
+ const { args, flags } = await this.parse(Stream);
48
+ const parts = args.STREAM.replace(/https?:\/\//i, "").split("/");
49
+ if (parts.length !== 5) {
50
+ ux.error(`Invalid stream url, ${args.STREAM}`);
51
+ }
52
+ const org = parts[1];
53
+ const dataCore = parts[2];
54
+ const aggregator = parts[3];
55
+ const eventType = parts[4].replace(/\.stream$/i, "");
56
+ const fetchDataCoreResponse = await graphqlClient.client.request(FETCH_DATA_CORE_GQL_QUERY, {
57
+ dataCore,
58
+ organization: org,
59
+ });
60
+ const dataCoreId = fetchDataCoreResponse.organization.datacores[0].id;
61
+ const eventRangeRequest = await graphqlClient.client.request(FETCH_EVENT_TYPE_RANGE_GQL_QUERY, {
62
+ aggregator,
63
+ dataCoreId,
64
+ });
65
+ let firstTimeBucket = eventRangeRequest.datacore.flowtypes.find(ft => ft.aggregator === aggregator)?.events.find(e => e.name === eventType)?.catalog.range.firstTimeBucket;
66
+ if (!firstTimeBucket && !flags.start) {
67
+ firstTimeBucket = createTimebucket(dayjs());
68
+ ux.warn(`First time bucket not found, setting to current time (${ux.colorize("yellowBright", firstTimeBucket)})`);
69
+ }
70
+ firstTimeBucket = this.setTimeBucket(dayjs(), flags.start || "1d");
71
+ ux.log(ux.colorize("green", `Setting first time bucket to ${ux.colorize("yellow", flags.start || "1d")} ago (${ux.colorize("yellowBright", firstTimeBucket)})`));
72
+ let lastTimeBucket = eventRangeRequest.datacore.flowtypes.find(ft => ft.aggregator === aggregator)?.events.find(e => e.name === eventType)?.catalog.range.lastTimeBucket;
73
+ if (!lastTimeBucket) {
74
+ lastTimeBucket = createTimebucket(dayjs());
75
+ this.warn(`Last time bucket not found, setting to current time (${ux.colorize("yellowBright", lastTimeBucket)})`);
76
+ }
77
+ const observer = new Subject();
78
+ this.log(ux.colorize("blackBright", `Starting to stream events for ${dataCoreId} - ${aggregator} - ${eventType}, press ${ux.colorize("whiteBright", "ctrl+c")} to stop`));
79
+ // eslint-disable-next-line no-void
80
+ void this.streanEvents(dataCoreId, aggregator, eventType, firstTimeBucket, lastTimeBucket, graphqlClient, observer, flags.live);
81
+ await this.processEvents(observer, flags.destination, flags.output);
82
+ }
83
+ async processEvents(observer, destination, output) {
84
+ let done = false;
85
+ let events = [];
86
+ observer.subscribe({
87
+ complete() {
88
+ ux.warn("Stream completed");
89
+ done = true;
90
+ },
91
+ error(error) {
92
+ ux.warn(`Stream error occurred with error ${error}`);
93
+ done = true;
94
+ },
95
+ next(value) {
96
+ events.push(value);
97
+ }
98
+ });
99
+ // eslint-disable-next-line no-unmodified-loop-condition
100
+ while (!done) {
101
+ const processingQueue = Queue.fromArray(events);
102
+ events = [];
103
+ while (!processingQueue.isEmpty()) {
104
+ const event = processingQueue.dequeue();
105
+ if (output === "log") {
106
+ ux.log(JSON.stringify(event));
107
+ continue;
108
+ }
109
+ this.debug(`Sending event to ${destination}: ${event.eventId}`);
110
+ try {
111
+ // eslint-disable-next-line no-await-in-loop
112
+ const result = await fetch(destination, {
113
+ body: JSON.stringify(event),
114
+ headers: {
115
+ "Content-Type": "application/json",
116
+ },
117
+ method: "POST",
118
+ });
119
+ if (!result.ok) {
120
+ this.warn(`Error sending event to ${destination}: ${result.statusText}`);
121
+ }
122
+ if (result.body) {
123
+ // eslint-disable-next-line no-await-in-loop
124
+ ux.debug(`Response from ${destination}: ${await result.json()}`);
125
+ }
126
+ }
127
+ catch (error) {
128
+ this.warn(`Error sending event to ${destination}: ${error}`);
129
+ }
130
+ }
131
+ if (events.length === 0) {
132
+ // eslint-disable-next-line no-await-in-loop
133
+ await new Promise((resolve) => { setTimeout(resolve, 200); });
134
+ }
135
+ }
136
+ }
137
+ setTimeBucket(date, start) {
138
+ const parts = start.match(/(\d+)([dhmy])/i);
139
+ if (!parts) {
140
+ ux.error(`Invalid start time bucket, generated from ${start}`);
141
+ }
142
+ switch (parts[2]) {
143
+ case "y": {
144
+ return createTimebucket(date.subtract(Number.parseInt(parts[1], 10), "year"));
145
+ }
146
+ case "m": {
147
+ return createTimebucket(date.subtract(Number.parseInt(parts[1], 10), "month"));
148
+ }
149
+ case "d": {
150
+ return createTimebucket(date.subtract(Number.parseInt(parts[1], 10), "day"));
151
+ }
152
+ case "h": {
153
+ return createTimebucket(date.subtract(Number.parseInt(parts[1], 10), "hour"));
154
+ }
155
+ default: {
156
+ this.error("Invalid start time bucket");
157
+ }
158
+ }
159
+ }
160
+ async streanEvents(dataCoreId, aggregator, eventType, firstTimeBucket, lastTimeBucket, graphqlClient, observer, live) {
161
+ let startTimeBucket = dayjs(firstTimeBucket, TIME_BUCKET_HOUR_PATTERN);
162
+ const currentTime = dayjs.utc();
163
+ let liveMode = false;
164
+ let lastEventId = null;
165
+ // eslint-disable-next-line no-constant-condition
166
+ while (true) {
167
+ const timeBucket = createTimebucket(startTimeBucket);
168
+ ux.action.start(`Fetching events for ${startTimeBucket}`);
169
+ let cursor = null;
170
+ let found = false;
171
+ do {
172
+ ux.action.status = `(${timeBucket}), cursor: ${cursor}`;
173
+ // eslint-disable-next-line no-await-in-loop
174
+ await graphqlClient.validateToken();
175
+ try {
176
+ // eslint-disable-next-line no-await-in-loop
177
+ const response = await graphqlClient.client.request(FETCH_EVENTS_GQL_QUERY, {
178
+ afterEventId: lastEventId || undefined,
179
+ aggregator,
180
+ dataCoreId,
181
+ eventType,
182
+ timeBucket,
183
+ });
184
+ cursor = response.datacore.fetchEvents.cursor;
185
+ for (const event of response.datacore.fetchEvents.events) {
186
+ found = true;
187
+ observer.next(event);
188
+ lastEventId = event.eventId;
189
+ }
190
+ }
191
+ catch (error) {
192
+ // don't display error if token is expired
193
+ if (error instanceof Error && error.message.includes("Token is expired")) {
194
+ continue;
195
+ }
196
+ ux.warn(`Error fetching events: ${error}`);
197
+ }
198
+ } while (cursor);
199
+ if (dayjs(timeBucket, TIME_BUCKET_HOUR_PATTERN).isBefore(currentTime, "hour")) {
200
+ startTimeBucket = startTimeBucket.add(1, "hour");
201
+ }
202
+ if (timeBucket === lastTimeBucket) {
203
+ if (!live) {
204
+ observer.complete();
205
+ ux.action.stop("Fetched all events, stopping");
206
+ return;
207
+ }
208
+ if (!liveMode) {
209
+ ux.warn("Reached last time bucket, switching to live mode");
210
+ startTimeBucket = createTimebucketDayjs(currentTime);
211
+ liveMode = true;
212
+ }
213
+ }
214
+ if (liveMode && !found) {
215
+ // eslint-disable-next-line no-await-in-loop
216
+ await new Promise((resolve) => { setTimeout(resolve, 1000); });
217
+ }
218
+ }
219
+ }
220
+ }
@@ -0,0 +1,5 @@
1
+ import { BaseCommand } from "../base-command.js";
2
+ export default class Whoami extends BaseCommand<typeof Whoami> {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ }
@@ -0,0 +1,25 @@
1
+ import { BaseCommand } from "../base-command.js";
2
+ import { LOGIN_CODES, ValidateLogin } from "../utils/validate-login.util.js";
3
+ export default class Whoami extends BaseCommand {
4
+ static description = 'Check what user you are logged in as';
5
+ async run() {
6
+ const config = this.cliConfiguration.getConfig();
7
+ const { auth, login } = config;
8
+ if (!login.url) {
9
+ this.error("No login url configured");
10
+ }
11
+ if (!auth) {
12
+ this.error("Not logged in");
13
+ }
14
+ const validator = new ValidateLogin(login.url);
15
+ const result = await validator.validate(config, this.cliConfiguration);
16
+ if (result.status === LOGIN_CODES.LOGIN_SUCCESS) {
17
+ this.log(`Logged in as ${result.name} (${result.flowcore_user_id})`);
18
+ return;
19
+ }
20
+ if (result.status === LOGIN_CODES.LOGIN_EXPIRED) {
21
+ this.error("Login expired, please login again");
22
+ }
23
+ this.error("Not logged in");
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
@@ -0,0 +1,18 @@
1
+ export interface SourceEvent {
2
+ /** aggregator name (e.g. "website A") */
3
+ aggregator: string;
4
+ /** uuid of the data core */
5
+ dataCore: string;
6
+ /** time uuid of the event, when it was stored */
7
+ eventId: string;
8
+ /** event type (e.g. "letter clicked") */
9
+ eventType: string;
10
+ /** metadata related to the event (e.g. url, user id, etc.) */
11
+ metadata: unknown;
12
+ /** event data serialized to a string */
13
+ payload: unknown;
14
+ /** timebucket of the event, when it was stored */
15
+ timeBucket: string;
16
+ /** event timestamp, when this event is valid */
17
+ validTime: string;
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ export declare const CONFIG_FILE_NAME = "credentials.json";
2
+ export declare class CliConfiguration {
3
+ private config;
4
+ private configPath;
5
+ private selectedProfile;
6
+ private static generateConfigPath;
7
+ displayConfigTable(log: (message?: string, ...args: unknown[]) => void): void;
8
+ getConfig(): CliConfig;
9
+ getSelectedProfile(): string;
10
+ init(profile: string, path: string): void;
11
+ setConfig(newConfig: unknown): void;
12
+ private ensureConfigIsValid;
13
+ }
14
+ export type CliConfig = {
15
+ api: {
16
+ url: string;
17
+ };
18
+ auth?: {
19
+ accessToken: string;
20
+ idToken?: string;
21
+ refreshToken?: string;
22
+ };
23
+ login: {
24
+ callbackPort: number;
25
+ clientId: string;
26
+ clientSecret?: string;
27
+ url: string;
28
+ };
29
+ };
30
+ export declare const createDefaultConfig: (configPath: string) => void;
31
+ export declare const loadConfig: (configPath: string) => CliConfig;
@@ -0,0 +1,108 @@
1
+ import { ux } from "@oclif/core";
2
+ import _ from "lodash";
3
+ import { merge, recursive } from "merge";
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { getObjectPaths } from "./object-path.util.js";
7
+ export const CONFIG_FILE_NAME = "credentials.json";
8
+ export class CliConfiguration {
9
+ config = {};
10
+ configPath = "";
11
+ selectedProfile = "";
12
+ static generateConfigPath(configDir) {
13
+ return `${configDir}/${CONFIG_FILE_NAME}`;
14
+ }
15
+ displayConfigTable(log) {
16
+ const maskedValues = new Set([
17
+ "login.clientSecret",
18
+ "auth.accessToken",
19
+ "auth.idToken",
20
+ "auth.refreshToken",
21
+ ]);
22
+ const objectPaths = getObjectPaths(this.config);
23
+ ux.table(objectPaths.map(oMap => maskedValues.has(oMap) ?
24
+ ({ path: oMap, value: "*******" }) : ({ path: oMap, value: _.get(this.config, oMap) })), {
25
+ path: {},
26
+ value: {
27
+ get: (row) => row.value ?? "not configured",
28
+ header: 'Value'
29
+ }
30
+ }, {
31
+ printLine: log,
32
+ });
33
+ }
34
+ getConfig() {
35
+ return this.config;
36
+ }
37
+ getSelectedProfile() {
38
+ return this.selectedProfile;
39
+ }
40
+ init(profile, path) {
41
+ this.configPath = CliConfiguration.generateConfigPath(path);
42
+ this.selectedProfile = profile;
43
+ this.ensureConfigIsValid();
44
+ }
45
+ setConfig(newConfig) {
46
+ this.config = recursive(this.config, newConfig);
47
+ const existingConfig = JSON.parse(fs.readFileSync(this.configPath, "utf8"));
48
+ fs.writeFileSync(this.configPath, JSON.stringify(recursive(existingConfig, {
49
+ [this.selectedProfile]: this.config,
50
+ }), null, 2));
51
+ }
52
+ ensureConfigIsValid() {
53
+ const defaultConfig = {
54
+ api: {
55
+ url: "https://graph.api.flowcore.io/graphql",
56
+ },
57
+ login: {
58
+ callbackPort: 3000,
59
+ clientId: "flowcore-cli",
60
+ url: "https://auth.flowcore.io/realms/flowcore/.well-known/openid-configuration",
61
+ }
62
+ };
63
+ if (!fs.existsSync(path.dirname(this.configPath))) {
64
+ fs.mkdirSync(path.dirname(this.configPath), { recursive: true });
65
+ }
66
+ if (fs.existsSync(this.configPath)) {
67
+ const existingConfig = JSON.parse(fs.readFileSync(this.configPath, "utf8"));
68
+ this.config = existingConfig[this.selectedProfile] ? recursive(defaultConfig, existingConfig[this.selectedProfile]) : defaultConfig;
69
+ fs.writeFileSync(this.configPath, JSON.stringify(recursive(existingConfig, {
70
+ [this.selectedProfile]: this.config,
71
+ }), null, 2));
72
+ }
73
+ else {
74
+ this.config = defaultConfig;
75
+ fs.writeFileSync(this.configPath, JSON.stringify({
76
+ [this.selectedProfile]: this.config,
77
+ }, null, 2));
78
+ }
79
+ }
80
+ }
81
+ export const createDefaultConfig = (configPath) => {
82
+ const config = {
83
+ api: {
84
+ url: "https://graph.api.flowcore.io/graphql",
85
+ },
86
+ login: {
87
+ callbackPort: 3000,
88
+ clientId: "flowcoreweb",
89
+ url: "https://auth.flowcore.io/realms/flowcore/.well-known/openid-configuration",
90
+ }
91
+ };
92
+ if (!fs.existsSync(path.dirname(configPath))) {
93
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
94
+ }
95
+ if (fs.existsSync(configPath)) {
96
+ const existingConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
97
+ fs.writeFileSync(configPath, JSON.stringify(merge(config, existingConfig), null, 2));
98
+ }
99
+ else {
100
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
101
+ }
102
+ };
103
+ export const loadConfig = (configPath) => {
104
+ if (!fs.existsSync(configPath)) {
105
+ createDefaultConfig(configPath);
106
+ }
107
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
108
+ };
@@ -0,0 +1,8 @@
1
+ import { GraphQLClient } from "graphql-request";
2
+ export declare class QueryGraphQL {
3
+ client: GraphQLClient;
4
+ private readonly revalidate;
5
+ private readonly url;
6
+ constructor(url: string, token: string, revalidateClient: () => Promise<false | string>);
7
+ validateToken(): Promise<void>;
8
+ }
@@ -0,0 +1,25 @@
1
+ import { GraphQLClient } from "graphql-request";
2
+ export class QueryGraphQL {
3
+ client;
4
+ revalidate;
5
+ url;
6
+ constructor(url, token, revalidateClient) {
7
+ this.client = new GraphQLClient(url, {
8
+ headers: {
9
+ authorization: `Bearer ${token}`,
10
+ },
11
+ });
12
+ this.revalidate = revalidateClient;
13
+ this.url = url;
14
+ }
15
+ async validateToken() {
16
+ const newToken = await this.revalidate();
17
+ if (newToken !== false) {
18
+ this.client = new GraphQLClient(this.url, {
19
+ headers: {
20
+ authorization: `Bearer ${newToken}`,
21
+ },
22
+ });
23
+ }
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ export declare function getObjectPaths(obj: unknown, parentKey?: string): string[];
@@ -0,0 +1,17 @@
1
+ export function getObjectPaths(obj, parentKey) {
2
+ let paths = [];
3
+ for (const key in obj) {
4
+ if (Object.hasOwn(obj, key)) {
5
+ const path = parentKey ? `${parentKey}.${key}` : key;
6
+ const value = obj[key];
7
+ if (typeof value === 'object' && value !== null) {
8
+ const nestedPaths = getObjectPaths(value, path);
9
+ paths = [...paths, ...nestedPaths];
10
+ }
11
+ else {
12
+ paths.push(path);
13
+ }
14
+ }
15
+ }
16
+ return paths;
17
+ }
@@ -0,0 +1,12 @@
1
+ export type FetchDataCoreResponse = {
2
+ organization: {
3
+ datacores: {
4
+ id: string;
5
+ }[];
6
+ };
7
+ };
8
+ export type FetchDataCoreQueryInput = {
9
+ dataCore: string;
10
+ organization: string;
11
+ };
12
+ export declare const FETCH_DATA_CORE_GQL_QUERY: string;
@@ -0,0 +1,10 @@
1
+ import { gql } from "graphql-request";
2
+ export const FETCH_DATA_CORE_GQL_QUERY = gql `
3
+ query FLOWCORE_CLI_FETCH_DATA_CORE($organization: String!, $dataCore: String!) {
4
+ organization(search: {org: $organization}) {
5
+ datacores(search: { name: $dataCore }) {
6
+ id
7
+ }
8
+ }
9
+ }
10
+ `;