Skip to content

Commit

Permalink
refactor: errors are now displayed in the profile section
Browse files Browse the repository at this point in the history
  • Loading branch information
joacoc committed Oct 5, 2023
1 parent 3b4f016 commit a6dab62
Show file tree
Hide file tree
Showing 13 changed files with 317 additions and 107 deletions.
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ The extension runs in a parallel instance of Visual Studio Code. To start it, pr

### Running the tests

To run the tests run:
1. Run Materialize in docker:
```bash
docker run -v mzdata:/mzdata -p 6875:6875 -p 6876:6876 materialize/materialized
```

2. Run the VSCode tests:
```bash
npm run test
```
Expand Down
4 changes: 4 additions & 0 deletions resources/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
color: rgb(220 38 38);
}

.profileErrorMessage {
color: rgb(220 38 38);
}

.action_button {
display: flex;
align-items: center;
Expand Down
54 changes: 41 additions & 13 deletions src/clients/admin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import fetch from "node-fetch";
import AppPassword from "../context/appPassword";
import { JwksError } from "jwks-rsa";
import { Errors } from "../utilities/error";
const jwksClient = require("jwks-rsa");
const jwt = require("node-jsonwebtoken");

Expand Down Expand Up @@ -50,8 +52,16 @@ export default class AdminClient {
headers: {'Content-Type': 'application/json'}
});

this.auth = (await response.json()) as AuthenticationResponse;
return this.auth.accessToken;
if (response.status === 200) {
this.auth = (await response.json()) as AuthenticationResponse;
return this.auth.accessToken;
} else {
const { errors } = await response.json() as any;
const [error] = errors;
console.error("[AdminClient]", "Error during getToken: ", error);

throw new Error(error);
}
} else {
return this.auth.accessToken;
}
Expand All @@ -70,24 +80,42 @@ export default class AdminClient {
/// Verifies the JWT signature using a JWK from the well-known endpoint and
/// returns the user claims.
async getClaims() {
const [jwk] = await this.getJwks();
const key = jwk.getPublicKey();
console.log("[AdminClient]", "Getting Token.");
const token = await this.getToken();

// Ignore expiration during tests
// The extension is not in charge of manipulating any type of information in Materialize servers.
const authData = jwt.verify(token, key, { complete: true });
try {
console.log("[AdminClient]", "Getting JWKS.");
const [jwk] = await this.getJwks();
const key = jwk.getPublicKey();

// Ignore expiration during tests
const authData = jwt.verify(token, key, { complete: true });

return authData.payload;
return authData.payload;
} catch (err) {
console.error("[AdminClient]", "Error retrieving claims: ", err);
throw new Error(Errors.verifyCredential);
}
}

/// Returns the current user's email.
async getEmail() {
const claims = await this.getClaims();
if (typeof claims === "string") {
return JSON.parse(claims).email as string;
} else {
return claims.email as string;
let claims = await this.getClaims();

try {
if (typeof claims === "string") {
claims = JSON.parse(claims);
}

console.log(claims);
if (!claims.email) {
throw new Error(Errors.emailNotPresentInClaims);
} else {
return claims.email as string;
}
} catch (err) {
console.error("[AdminClient]", "Error retrieving email: ", err);
throw new Error(Errors.retrievingEmail);
}
}
}
Expand Down
57 changes: 39 additions & 18 deletions src/clients/cloud.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fetch from "node-fetch";
import AdminClient from "./admin";
import * as vscode from 'vscode';
import { Errors } from "../utilities/error";

const DEFAULT_API_CLOUD_ENDPOINT = 'https://api.cloud.materialize.com';

Expand Down Expand Up @@ -64,7 +65,7 @@ export default class CloudClient {
// eslint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': 'application/json',
// eslint-disable-next-line @typescript-eslint/naming-convention
"Authorization": `Bearer ${(await this.adminClient.getToken())}`
"Authorization": `Bearer ${await this.adminClient.getToken()}`
}
});
}
Expand All @@ -73,31 +74,49 @@ export default class CloudClient {
const cloudProviders = [];
let cursor = '';

while (true) {
let response = await this.fetch(`${this.providersEndpoint}?limit=50&cursor=${cursor}`);
try {
while (true) {
let response = await this.fetch(`${this.providersEndpoint}?limit=50&cursor=${cursor}`);

console.log("[CloudClient]", `Status: ${response.status}`);
const cloudProviderResponse = (await response.json()) as CloudProviderResponse;
cloudProviders.push(...cloudProviderResponse.data);
console.log("[CloudClient]", `Status: ${response.status}`);
const cloudProviderResponse = (await response.json()) as CloudProviderResponse;
cloudProviders.push(...cloudProviderResponse.data);

if (cloudProviderResponse.nextCursor) {
cursor = cloudProviderResponse.nextCursor;
} else {
break;
if (cloudProviderResponse.nextCursor) {
cursor = cloudProviderResponse.nextCursor;
} else {
break;
}
}
} catch (err) {
console.error("[CloudClient]", "Error listing cloud providers: ", err);
throw new Error(Errors.listingCloudProviders);
}

return cloudProviders;
}

async getRegion(cloudProvider: CloudProvider): Promise<Region> {
const regionEdnpoint = `${cloudProvider.url}/api/region`;
try {
const regionEdnpoint = `${cloudProvider.url}/api/region`;

let response = await this.fetch(regionEdnpoint);
let response = await this.fetch(regionEdnpoint);

console.log("[CloudClient]", `Status: ${response.status}`);
const region: Region = (await response.json()) as Region;
return region;
console.log("[CloudClient]", `Status: ${response.status}`);
if (response.status === 200) {
const region: Region = (await response.json()) as Region;
return region;
} else {
try {
throw new Error((await response.json() as any).error);
} catch (err) {
throw err;
}
}
} catch (err) {
console.error("[CloudClient]", "Error retrieving region: ", err);
throw new Error(Errors.retrievingRegion);
}
}

/**
Expand All @@ -107,11 +126,11 @@ export default class CloudClient {
*/
async getHost(region: string) {
console.log("[CloudClient]", "Listing cloud providers.");

const cloudProviders = await this.listCloudProviders();
console.log("[CloudClient]", "Providers: ", cloudProviders);

console.log("[CloudClient]", "Providers: ", cloudProviders);
const provider = cloudProviders.find(x => x.id === region);

console.log("[CloudClient]", "Selected provider: ", provider);
if (provider) {
console.log("[CloudClient]", "Retrieving region.");
Expand All @@ -120,10 +139,12 @@ export default class CloudClient {

if (!regionInfo) {
console.error("[CloudClient]", "Region is not enabled.");
vscode.window.showErrorMessage("Region is not enabled.");
throw new Error(Errors.disabledRegion);
} else {
return regionInfo.sqlAddress;
}
} else {
throw new Error(Errors.invalidProvider.replace("${region}", region));
}
}
}
12 changes: 10 additions & 2 deletions src/clients/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@ import { MaterializeObject } from "../providers/schema";
import AdminClient from "./admin";
import CloudClient from "./cloud";
import * as vscode from 'vscode';
import { Context, EventType } from "../context";

export default class SqlClient {
private pool: Promise<Pool>;
private adminClient: AdminClient;
private cloudClient: CloudClient;
private context: Context;
private profile: NonStorableConfigProfile;

constructor(
adminClient: AdminClient,
cloudClient: CloudClient,
profile: NonStorableConfigProfile,
context: Context,
) {
this.adminClient = adminClient;
this.cloudClient = cloudClient;
this.profile = profile;
this.context = context;

this.pool = new Promise((res, rej) => {
const asyncOp = async () => {
Expand All @@ -37,14 +41,19 @@ export default class SqlClient {
rej(err);
});
} catch (err) {
vscode.window.showErrorMessage(`Error connecting to the region: ${err}`);
console.error("[SqlClient]", "Error creating pool: ", err);
this.context.emit("event", { type: EventType.error, message: err });
}
};

asyncOp();
});
}

async connectErr() {
await this.pool;
}

/**
* Rreturns the connection options for a PSQL connection.
* @returns string connection options
Expand Down Expand Up @@ -97,7 +106,6 @@ export default class SqlClient {
async* cursorQuery(statement: string): AsyncGenerator<QueryResult> {
const pool = await this.pool;
const client = await pool.connect();
const id = randomUUID();

try {
const batchSize = 100; // Number of rows to fetch in each batch
Expand Down
14 changes: 8 additions & 6 deletions src/context/appPassword.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as uuid from "uuid";
import { Errors } from "../utilities/error";

const PREFIX = 'mzp_';

Expand Down Expand Up @@ -50,23 +51,24 @@ export default class AppPassword {
);

if (filteredChars.length !== 64) {
throw new Error();
throw new Error(Errors.invalidLengthAppPassword);
}

// Lazy way to rebuild uuid.
try {
const clientId = AppPassword.formatDashlessUuid(filteredChars.slice(0, 32).join(''));
const secretKey = AppPassword.formatDashlessUuid(filteredChars.slice(32).join(''));

return {
clientId,
secretKey,
};
return {
clientId,
secretKey,
};
} catch (err) {
console.log("Error parsing UUID.");
throw new Error(Errors.invalidAppPassword);
}
}

throw new Error("Invalid app-password");
throw new Error(Errors.invalidAppPassword);
}
}
15 changes: 6 additions & 9 deletions src/context/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class Config {
const profile = this.config.profiles[profileName];

if (!profile) {
// TODO: Display in the profile section.
vscode.window.showErrorMessage(`Error. The selected default profile '${profileName}' does not exist.`);
return;
}
Expand All @@ -74,6 +75,8 @@ export class Config {
this.createFileOrDir(this.configDir);
}
} catch (err) {
console.error("[Config]", "Error loading config: ", err);
// TODO: Display this in the profile config section.
vscode.window.showErrorMessage('Error creating the configuration directory.');
}

Expand All @@ -82,17 +85,11 @@ export class Config {
}

try {
console.log("[Config]", "Config file path: ", this.configFilePath);
let configInToml = readFileSync(this.configFilePath, 'utf-8');
try {
return TOML.parse(configInToml) as ConfigFile;
} catch (err) {
vscode.window.showErrorMessage('Error parsing the configuration file.');
console.error("Error parsing configuration file.");
throw err;
}
return TOML.parse(configInToml) as ConfigFile;
} catch (err) {
vscode.window.showErrorMessage('Error reading the configuration file.');
console.error("Error reading the configuration file.", err);
console.error("[Config]", "Error reading the configuration file.", err);
throw err;
}
}
Expand Down
Loading

0 comments on commit a6dab62

Please sign in to comment.