Skip to content

Commit

Permalink
feat: initial sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
bruuuuuuuce committed Oct 12, 2021
1 parent 0af81f8 commit d33fa41
Show file tree
Hide file tree
Showing 17 changed files with 453 additions and 21 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
dist
52 changes: 34 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@
"name": "@momento/client-sdk-typescript",
"version": "1.0.0",
"description": "Client SDK for Momento services",
"main": "index.js",
"main": "dist/index.js",
"files": [
"/dist"
],
"types": "dist/index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",

"devDependencies": {
"typescript": "^4.4.3"
},
"dependencies": {
"@momento/wire-types-typescript": "0.1.0"
"@grpc/grpc-js": "1.3.7",
"jwt-decode": "^3.1.2",
"@momento/wire-types-typescript": "0.1.1"
}
}
108 changes: 108 additions & 0 deletions src/Cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {cache, grpc} from '@momento/wire-types-typescript';
import {authInterceptor} from "./grpc/AuthInterceptor";
import {cacheNameInterceptor} from "./grpc/CacheNameInterceptor";
import {MomentoCacheResult, momentoResultConverter} from "./messages/Result";
import {IllegalArgumentError} from "./errors/IllegalArgumentError";
import {ClientSdkError} from "./errors/ClientSdkError";
import {errorMapper} from "./errors/GrpcErrorMapper";

type GetResponse = {
body: string;
message: string;
result: MomentoCacheResult;
}

type SetResponse = {
message: string;
result: MomentoCacheResult;
}

export class Cache {
private readonly client: cache.cache_client.ScsClient;
private readonly textEncoder: TextEncoder;
private readonly textDecoder: TextDecoder;
private readonly interceptors: grpc.Interceptor[];
private readonly cacheName: string;
constructor(authToken: string, cacheName: string, endpoint: string) {
this.client = new cache.cache_client.ScsClient(endpoint, grpc.ChannelCredentials.createSsl())
this.textEncoder = new TextEncoder();
this.textDecoder = new TextDecoder();
this.cacheName = cacheName;
this.interceptors = [authInterceptor(authToken), cacheNameInterceptor(cacheName)]
}

public async set(key: string, value: string, ttl: number = 1000): Promise<SetResponse> {
this.ensureValidSetRequest(key, value, ttl)
const encodedKey = this.textEncoder.encode(key)
const encodedValue = this.textEncoder.encode(value)

const request = new cache.cache_client.SetRequest({
cache_body: encodedValue,
cache_key: encodedKey,
ttl_milliseconds: ttl
})
return new Promise((resolve, reject) => {
this.client.Set(request, { interceptors: this.interceptors }, (err, resp) => {
if (err) {
reject(errorMapper(err))
}
if (resp) {
resolve(this.parseSetResponse(resp))
}
reject(new ClientSdkError("unable to perform set"))
})
})
}

public async get(key: string): Promise<GetResponse> {
this.ensureValidKey(key);
const request = new cache.cache_client.GetRequest({
cache_key: this.textEncoder.encode(key)
})

return new Promise((resolve, reject) => {
this.client.Get(request, { interceptors: this.interceptors }, (err, resp) => {
if (err) {
reject(errorMapper(err))
}
if (resp) {
resolve(this.parseGetResponse(resp))
}
reject(new ClientSdkError("unable to get from cache"))
})
})
}

private parseGetResponse(resp: cache.cache_client.GetResponse): GetResponse {
return {
result: momentoResultConverter(resp.result),
message: resp.message,
body: this.textDecoder.decode(resp.cache_body)
}
}

private parseSetResponse(resp: cache.cache_client.SetResponse): SetResponse {
return {
result: momentoResultConverter(resp.result),
message: resp.message
}
}

private ensureValidKey(key: any) {
if (!key) {
throw new IllegalArgumentError("key must not be empty")
}
}

private ensureValidSetRequest(key: any, value: any, ttl: number) {
this.ensureValidKey(key)

if (!value) {
throw new IllegalArgumentError("value must not be empty")
}

if (ttl && ttl < 0) {
throw new IllegalArgumentError("ttl must be a positive integer")
}
}
}
72 changes: 72 additions & 0 deletions src/Momento.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {control, grpc} from '@momento/wire-types-typescript';
import jwtDecode from "jwt-decode";
import {Cache} from "./Cache";
import {authInterceptor} from "./grpc/AuthInterceptor";
import {IllegalArgumentError} from "./errors/IllegalArgumentError";
import {ClientSdkError} from "./errors/ClientSdkError";
import {Status} from "@grpc/grpc-js/build/src/constants";
import {CacheAlreadyExistsError} from "./errors/CacheAlreadyExistsError";
import {errorMapper} from "./errors/GrpcErrorMapper";

type Claims = {
cp: string,
c: string
}

export class Momento {
private readonly client: control.control_client.ScsControlClient;
private readonly interceptors: grpc.Interceptor[];
private readonly controlEndpoint: string;
private readonly cacheEndpoint: string;
private readonly authToken: string;
constructor(authToken: string, endpointOverride?: string) {
const claims = Momento.decodeJwt(authToken);
this.authToken = authToken;
this.interceptors = [authInterceptor(authToken)]
this.controlEndpoint = endpointOverride ? `control.${endpointOverride}` : claims.cp;
this.cacheEndpoint = endpointOverride ? `cache.${endpointOverride}` : claims.c;
this.client = new control.control_client.ScsControlClient(this.controlEndpoint, grpc.ChannelCredentials.createSsl())
}

private static decodeJwt(jwt?: string): Claims {
if(!jwt) {
throw new IllegalArgumentError("malformed auth token")
}
try {
return jwtDecode<Claims>(jwt)
} catch (e) {
throw new ClientSdkError("failed to parse jwt")
}

}

public getCache(name: string): Cache {
this.validateCachename(name);
return new Cache(this.authToken, name, this.cacheEndpoint);
}

public createCache(name: string): Promise<control.control_client.CreateCacheResponse> {
this.validateCachename(name);
const request = new control.control_client.CreateCacheRequest({ cache_name: name })
return new Promise((resolve, reject) => {
this.client.CreateCache(request, { interceptors: this.interceptors}, (err, resp) => {
if (err) {
if (err.code === Status.ALREADY_EXISTS) {
reject(new CacheAlreadyExistsError(`cache with name: ${name} already exists`))
}
reject(errorMapper(err))
}
if(resp) {
resolve(resp)
}
reject(new ClientSdkError("unable to create cache"))
})
})
}

private validateCachename(name: string) {
if (!name) {
throw new IllegalArgumentError("cache name must not be null")
}
}
}
8 changes: 8 additions & 0 deletions src/errors/CacheAlreadyExistsError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {ClientSdkError} from "./ClientSdkError";

export class CacheAlreadyExistsError extends ClientSdkError {
constructor(message: string) {
super(message);
this.name = "CacheAlreadyExistsError"
}
}
8 changes: 8 additions & 0 deletions src/errors/CacheNotFoundError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {ClientSdkError} from "./ClientSdkError";

export class CacheNotFoundError extends ClientSdkError {
constructor(message: string) {
super(message);
this.name = "CacheNotFoundError";
}
}
5 changes: 5 additions & 0 deletions src/errors/ClientSdkError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class ClientSdkError extends Error {
constructor(message: string) {
super(message);
}
}
20 changes: 20 additions & 0 deletions src/errors/GrpcErrorMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {grpc} from '@momento/wire-types-typescript';
import {ClientSdkError} from "./ClientSdkError";
import {Status} from "@grpc/grpc-js/build/src/constants";
import {PermissionDeniedError} from "./PermissionDeniedError";
import {CacheNotFoundError} from "./CacheNotFoundError";
import {IllegalArgumentError} from "./IllegalArgumentError";
import {InternalServerError} from "./InternalServerError";

export function errorMapper(err: grpc.ServiceError): ClientSdkError {
if (err.code === Status.PERMISSION_DENIED) {
return new PermissionDeniedError(err.message)
}
if (err.code === Status.NOT_FOUND) {
return new CacheNotFoundError(err.message)
}
if (err.code === Status.INVALID_ARGUMENT) {
return new IllegalArgumentError(err.message)
}
return new InternalServerError("unable to process request")
}
8 changes: 8 additions & 0 deletions src/errors/IllegalArgumentError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {ClientSdkError} from "./ClientSdkError";

export class IllegalArgumentError extends ClientSdkError {
constructor(message: string) {
super(message);
this.name = "IllegalArgumentError"
}
}
8 changes: 8 additions & 0 deletions src/errors/InternalServerError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {ClientSdkError} from "./ClientSdkError";

export class InternalServerError extends ClientSdkError {
constructor(message: string) {
super(message);
this.name = "InternalServerError"
}
}
8 changes: 8 additions & 0 deletions src/errors/PermissionDeniedError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {ClientSdkError} from "./ClientSdkError";

export class PermissionDeniedError extends ClientSdkError {
constructor(message: string) {
super(message);
this.name = "PermissionDeniedError"
}
}
14 changes: 14 additions & 0 deletions src/grpc/AuthInterceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {grpc} from "@momento/wire-types-typescript";

grpc.InterceptingCall

export const authInterceptor = (token: string): grpc.Interceptor => {
return (options, nextCall) => {
return new grpc.InterceptingCall(nextCall(options), {
start: function (metadata, listener, next) {
metadata.add("Authorization", token)
next(metadata, {})
},
})
}
}
Loading

0 comments on commit d33fa41

Please sign in to comment.