Skip to content

Commit

Permalink
Merge pull request #2 from CartoDB/feat/add-core-module
Browse files Browse the repository at this point in the history
Add core module in Web SDK
  • Loading branch information
Jesús Botella authored Jun 9, 2020
2 parents 1b5ad4b + fbd9135 commit 58c0ea0
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 1 deletion.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ module.exports = {

// Maybe we can enable this later
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"import/prefer-default-export": "off",
},
};
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"arrowParens": "avoid",
"requirePragma": false,
"insertPragma": false,
"proseWrap": "preserve"
"proseWrap": "preserve",
"printWidth": 100
}
90 changes: 90 additions & 0 deletions src/lib/core/Credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
const DEFAULT_USER_NAME = 'username';
const DEFAULT_PUBLIC_API_KEY = 'default_public';
const DEFAULT_SERVER_URL_TEMPLATE = 'https://{user}.carto.com';
const DEFAULT_USER_COMPONENT_IN_URL = '{user}';

/**
* Build a generic instance of credentials, eg to interact with APIs such as Windshaft or SQL
* @param username
* @param apiKey
* @param serverURL A url pattern with {user}, like default 'https://{user}.carto.com'
*
*/
export class Credentials {
private _username: string;
private _apiKey: string;
private _serverUrlTemplate: string;

constructor(
username: string,
apiKey: string,
serverUrlTemplate: string = DEFAULT_SERVER_URL_TEMPLATE
) {
if (!username) {
throw new Error('Username is required');
}

if (!apiKey) {
throw new Error('Api key is required');
}

this._username = username;
this._apiKey = apiKey;
this._serverUrlTemplate = serverUrlTemplate;
}

public static get DEFAULT_SERVER_URL_TEMPLATE() {
return DEFAULT_SERVER_URL_TEMPLATE;
}

public static get DEFAULT_PUBLIC_API_KEY() {
return DEFAULT_PUBLIC_API_KEY;
}

public get username(): string {
return this._username;
}

public set username(value: string) {
this._username = value;
}

public get apiKey(): string {
return this._apiKey;
}

public set apiKey(value: string) {
this._apiKey = value;
}

public get serverUrlTemplate(): string {
return this._serverUrlTemplate;
}

public set serverUrlTemplate(value: string) {
this._serverUrlTemplate = value;
}

public get serverURL(): string {
let url = this._serverUrlTemplate.replace(DEFAULT_USER_COMPONENT_IN_URL, this._username);

if (!url.endsWith('/')) {
url += '/';
}

return url;
}
}

export const defaultCredentials = new Credentials(DEFAULT_USER_NAME, DEFAULT_PUBLIC_API_KEY);

export function setDefaultCredentials(credentials: {
username: string;
apiKey: string;
serverUrlTemplate: string;
}) {
defaultCredentials.username = credentials.username;
defaultCredentials.apiKey = credentials.apiKey || DEFAULT_PUBLIC_API_KEY;
defaultCredentials.serverUrlTemplate =
credentials.serverUrlTemplate || DEFAULT_SERVER_URL_TEMPLATE;
}
33 changes: 33 additions & 0 deletions src/lib/core/MetricsEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { uuidv4 } from './utils';

// Custom CARTO headers, for metrics at API level
// Double check they are valid for the API (eg. allowed for CORS requests)
const CUSTOM_HEADER_EVENT_SOURCE = 'Carto-Event-Source';
const CUSTOM_HEADER_EVENT = 'Carto-Event';
const CUSTOM_HEADER_EVENT_GROUP_ID = 'Carto-Event-Group-Id';

/**
* Class to represent a relevant event, identifying several relevant properties
* of it: source, name and group-id
*/
class MetricsEvent {
public source: string;
public name: string;
public groupId: string;

constructor(source: string, name: string, groupId: string = uuidv4()) {
this.source = source;
this.name = name;
this.groupId = groupId;
}

public getHeaders() {
return [
[CUSTOM_HEADER_EVENT_SOURCE, this.source],
[CUSTOM_HEADER_EVENT, this.name],
[CUSTOM_HEADER_EVENT_GROUP_ID, this.groupId]
];
}
}

export default MetricsEvent;
35 changes: 35 additions & 0 deletions src/lib/core/__tests__/Credentials.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Credentials } from '../Credentials';

describe('auth/Credentials', () => {
it('should require a username and an API key', () => {
const creds = new Credentials('aUserName', 'anApiKey');

expect(creds.username).toBe('aUserName');
expect(creds.apiKey).toBe('anApiKey');
});

it('should fail if api key or username are not present', () => {
expect(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const creds = new Credentials('aUserName', undefined as any);
expect(creds.username).toBe('aUserName');
}).toThrow();

expect(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const creds = new Credentials(undefined as any, 'anApiKey');
expect(creds.username).toBe(undefined);
}).toThrow();
});

it('has a default server', () => {
const creds = new Credentials('aUser', 'anApiKey');
expect(creds.serverURL).toBe('https://aUser.carto.com/');
});

it('can manage different servers', () => {
const customServer = 'http://127.0.0.1:8181/user/{user}';
const creds = new Credentials('aUser', 'anApiKey', customServer);
expect(creds.serverURL).toBe('http://127.0.0.1:8181/user/aUser/');
});
});
29 changes: 29 additions & 0 deletions src/lib/core/__tests__/MetricsEvent.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import MetricsEvent from '../MetricsEvent';

describe('core/MetricsEvent', () => {
it('can be created easily', () => {
const event = new MetricsEvent('mytestlib', 'name of this test');

expect(event.source).toBe('mytestlib');
expect(event.name).toBe('name of this test');
expect(event.groupId).toBeTruthy();
});

it('can be created with a custom id', () => {
const event = new MetricsEvent('mytestlib', 'name of this test', 'id');

expect(event.source).toBe('mytestlib');
expect(event.name).toBe('name of this test');
expect(event.groupId).toBe('id');
});

it('can generate proper headers', () => {
const event = new MetricsEvent('mytestlib', 'name of this test', 'id');

const headers = event.getHeaders();
expect(headers).toBeTruthy();
expect(headers[0]).toEqual(['Carto-Event-Source', 'mytestlib']);
expect(headers[1]).toEqual(['Carto-Event', 'name of this test']);
expect(headers[2]).toEqual(['Carto-Event-Group-Id', 'id']);
});
});
37 changes: 37 additions & 0 deletions src/lib/core/errors/CartoError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Represents an error in the carto library.
*
* @typedef {Object} CartoError
* @property {String} message - A short error description
* @property {String} name - The name of the error "CartoError"
* @property {String} type - The type of the error "CartoError"
* @property {Object} originalError - An object containing the internal/original error
*
* @event CartoError
* @api
*/
export class CartoError extends Error {
/**
* Build a cartoError from a generic error.
* @constructor
*
* @return {CartoError} A well formed object representing the error.
*/

constructor(error: { message: string; type: string }) {
if (!error) {
throw Error('Invalid CartoError, a message is mandatory');
}

if (!error.message) {
throw Error('Invalid CartoError, a message is mandatory');
}

if (!error.type) {
throw Error('Invalid CartoError, a type is mandatory');
}

super(`${error.type} ${error.message}`);
this.name = 'CartoError';
}
}
49 changes: 49 additions & 0 deletions src/lib/core/mixins/WithEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import mitt from 'mitt';
import { CartoError } from '../errors/CartoError';

export abstract class WithEvents {
protected emitter = mitt();
protected availableEvents = ['*'];

protected registerAvailableEvents(eventArray: string[]) {
this.availableEvents = ['*', ...eventArray];
}

public emit(type: string, event?: unknown) {
if (!this.availableEvents.includes(type)) {
throw new CartoError({
type: '[Events]',
message: `Trying to emit an unknown event type: ${type}. Available events: ${this.availableEvents.join(
', '
)}.`
});
}

this.emitter.emit(type, event);
}

on(type: string, handler: mitt.Handler) {
if (!this.availableEvents.includes(type)) {
this.throwEventNotFoundError(type);
}

this.emitter.on(type, handler);
}

off(type: string, handler: mitt.Handler) {
if (!this.availableEvents.includes(type)) {
this.throwEventNotFoundError(type);
}

this.emitter.off(type, handler);
}

private throwEventNotFoundError(eventType: string) {
throw new CartoError({
type: '[Events]',
message: `Trying to listen an unknown event type: ${eventType}. Available events: ${this.availableEvents.join(
', '
)}.`
});
}
}
14 changes: 14 additions & 0 deletions src/lib/core/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Unique Identifier v4
* Adapted from https://stackoverflow.com/a/2117523/251834
*/
/* eslint-disable no-bitwise */
export function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
// tslint:disable-next-line: no-bitwise
const r = (Math.random() * 16) | 0;
// tslint:disable-next-line: no-bitwise
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

1 comment on commit 58c0ea0

@vercel
Copy link

@vercel vercel bot commented on 58c0ea0 Jun 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.