Skip to content

Commit

Permalink
feat: Add session token
Browse files Browse the repository at this point in the history
  • Loading branch information
simenandre committed Apr 10, 2022
1 parent e5abee3 commit 1ef2a40
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 37 deletions.
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "3.1.0-next.4",
"description": "Typescript-wrapper for Tripletex 2.0 API",
"scripts": {
"build": "tsc"
"build": "tsc",
"test": "jest --coverage src/**/*"
},
"keywords": [
"tripletex",
Expand All @@ -22,11 +23,17 @@
"@bjerk/eslint-config": "^1.0.0",
"@cobraz/prettier": "^2.0.0",
"@tsconfig/node16": "^1.0.2",
"@types/jest": "^27.4.1",
"eslint": "^8.7.0",
"jest": "^27.4.7",
"nock": "^13.2.4",
"ts-jest": "^27.1.4",
"typescript": "^4.5.5"
},
"dependencies": {
"date-fns": "^2.28.0",
"dotenv": "^16.0.0",
"invariant": "^2.2.4",
"node-fetch": "^3.2.0",
"runtypes": "^6.5.0",
"typical-fetch": "^1.4.1"
Expand Down
71 changes: 59 additions & 12 deletions src/calls/base.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,45 @@
import { buildCall, CallReturn, TypicalHttpError } from 'typical-fetch';
import { invariant } from 'ts-invariant';
import { TripletexClientConfig, TripletexRuntimeConfig } from '../types';

export class TripletexBase {
protected buildCall() {
return buildCall();
export abstract class TripletexBase {
constructor(
readonly config: TripletexClientConfig,
readonly runtimeConfig?: TripletexRuntimeConfig,
) {}

protected authenticatedCall() {
invariant(this.runtimeConfig, 'missing runtime config');
invariant(this.config.baseUrl, 'missing baseUrl in config');
const basicAuth = Buffer.from(
[
this.runtimeConfig?.organizationId ?? '0', //
this.runtimeConfig.sessionToken,
].join(':'),
).toString('base64');

return buildCall() //
.baseUrl(this.config.baseUrl)
.headers(() => ({
'User-Agent': this.config.userAgent ?? 'bjerkio-tripletex/3',
Authorization: `Basic ${basicAuth}`,
}));
// .mapError(errorParser);
}

protected async performRequest<R, E = Error>(
protected unauthenticatedCall() {
invariant(this.config.baseUrl, 'missing baseUrl in config');
return buildCall() //
.baseUrl(this.config.baseUrl)
.headers(() => ({
'User-Agent': this.config.userAgent ?? 'bjerkio-tripletex/3',
}));
// .mapError(errorParser);
}

protected async performRequest<R, E>(
call: () => Promise<CallReturn<R, E>>,
): Promise<CallReturn<R, E>> {
let err: E = new Error('Unknown error');

for (let n = 0; n <= 3; n++) {
const res = await call();

Expand All @@ -18,7 +48,6 @@ export class TripletexBase {
}

const { error } = res;
err = error;
if (error instanceof TypicalHttpError && error.status === 429) {
const resetHeader = error.res.headers?.get('X-Rate-Limit-Reset');
const secondsToRetry = resetHeader ? Number(resetHeader) : 2;
Expand All @@ -29,10 +58,28 @@ export class TripletexBase {
}
}

return {
success: false,
error: err,
body: undefined,
};
throw new Error('Not able to perform request');
}

// private refreshToken():
// | ReturnType<typeof authCalls.getToken>
// | ReturnType<typeof authCalls.refreshTokens>
// | undefined {
// if (this.tokenState === undefined) {
// invariant(this.clientKey);
// return authCalls.getToken({
// applicationKey: this.applicationKey,
// baseUrl: this.baseUrl,
// clientKey: this.clientKey,
// });
// } else if (this.tokenHasExpired()) {
// return authCalls.refreshTokens({
// baseUrl: this.baseUrl,
// refreshToken: this.tokenState.refreshToken,
// });
// } else {
// // we have a token and it's valid!
// return undefined;
// }
// }
}
21 changes: 1 addition & 20 deletions src/calls/ledger-account/ledger-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,9 @@ export class TripletexLedgerAccount extends TripletexBase {
.args<rt.Static<typeof ledgerAccountListRequest>>()
.path('/ledger/account')
.method('get')
// .query(
// args =>
// new URLSearchParams(
// pickQueryValues(
// args,
// 'id',
// 'number',
// 'isBankAccount',
// 'isInactive',
// 'isApplicableForSupplierInvoice',
// 'ledgerType',
// 'isBalanceAccount',
// 'from',
// 'count',
// 'sorting',
// 'fields',
// ),
// ),
// )
.parseJson(withRuntype(listResponseAccountRt))
.build();

return this.fetchWithRetry(call);
return this.performRequest(call);
}
}
60 changes: 60 additions & 0 deletions src/calls/token/__tests__/token.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { addDays } from 'date-fns';
import * as dotenv from 'dotenv';
import nock from 'nock';
import { invariant } from 'ts-invariant';
import { formatDate } from '../../../utils';
import { TripletexToken } from '../token';

dotenv.config();

const baseUrl = process.env.BASE_URL || 'https://api.tripletex.io/';

describe('token class', () => {
let tokenClass: TripletexToken;

beforeEach(() => {
tokenClass = new TripletexToken({ baseUrl });
});

it('should create session token', async () => {
const expirationDate = addDays(new Date(), 2);
nock(baseUrl, { encodedQueryParams: true })
.put('/v2/token/session/:create')
.query({
employeeToken: String(process.env.EMPLOYEE_TOKEN),
consumerToken: String(process.env.CONSUMER_TOKEN),
expirationDate: formatDate(expirationDate),
})
.reply(200, {
value: {
id: 19015016,
version: 1,
url: 'api.tripletex.io/v2/token/session/19015016',
consumerToken: {
id: 1754,
url: 'api.tripletex.io/v2/token/consumer/1754',
},
employeeToken: {
id: 2913,
url: 'api.tripletex.io/v2/token/employee/2913',
},
expirationDate: '2022-04-12',
token:
'eyJ0b2tlbklkIjoxOTAxNTAxNiwidG9rZW4iOiJ0ZXN0LWIzMjU0NTg5LTU1OTctNDY0Mi04MmU5LTUyNDkyOWZlZGViNSJ9',
encryptionKey: null,
},
});

const token = await tokenClass.createSessionToken({
employeeToken: String(process.env.EMPLOYEE_TOKEN),
consumerToken: String(process.env.CONSUMER_TOKEN),
expirationDate,
});

invariant(token.success);

expect(token.body.value.token).toEqual(
'eyJ0b2tlbklkIjoxOTAxNTAxNiwidG9rZW4iOiJ0ZXN0LWIzMjU0NTg5LTU1OTctNDY0Mi04MmU5LTUyNDkyOWZlZGViNSJ9',
);
});
});
11 changes: 11 additions & 0 deletions src/calls/token/models/session-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as rt from 'runtypes';
import { singleValueEnvelope } from '../../../utils';

const sessionTokenRt = rt.Record({
expirationDate: rt.String,
token: rt.String,
});

export type SessionToken = rt.Static<typeof sessionTokenRt>;

export const getTokenResponseRt = singleValueEnvelope(sessionTokenRt);
28 changes: 28 additions & 0 deletions src/calls/token/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { formatDate, withRuntype } from '../../utils';
import { TripletexBase } from '../base';
import { getTokenResponseRt } from './models/session-token';

export interface CreateSessionTokenInput {
employeeToken: string;
consumerToken: string;
expirationDate: Date;
}

export class TripletexToken extends TripletexBase {
createSessionToken(args: CreateSessionTokenInput) {
const call = this.unauthenticatedCall() //
.args<{
input: CreateSessionTokenInput;
}>()
.path('/v2/token/session/:create')
.query(args => ({
...args.input,
expirationDate: formatDate(args.input.expirationDate),
}))
.method('put')
.parseJson(withRuntype(getTokenResponseRt))
.build();

return this.performRequest(() => call({ input: args }));
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TripletexClientConfig } from "./types";

/**
* Tripletex Client
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ export interface TripletexClientConfig {

expirationDate?: string;
}

export interface TripletexRuntimeConfig {
sessionToken: string;
organizationId?: string;
}
5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { format } from 'date-fns';
import * as rt from 'runtypes';

export function pickFromObject<
Expand Down Expand Up @@ -35,3 +36,7 @@ export function singleValueEnvelope<T>(type: rt.Runtype<T>) {
})
.asReadonly();
}

export function formatDate(d: Date | number): string {
return format(d, 'yyyy-MM-dd');
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
"outDir": "dist",
"esModuleInterop": true
},
"include": ["src/**/*.ts"]
}
Loading

0 comments on commit 1ef2a40

Please sign in to comment.