Skip to content

Commit

Permalink
WIP.
Browse files Browse the repository at this point in the history
  • Loading branch information
Syndesi committed Mar 18, 2024
1 parent dc25a5f commit e60b74e
Show file tree
Hide file tree
Showing 22 changed files with 670 additions and 39 deletions.
40 changes: 35 additions & 5 deletions src/EmberNexus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,33 @@ import 'reflect-metadata';
import { LRUCache } from 'lru-cache';
import { Container } from 'typedi';

import GetElementChildrenEndpoint from '~/Endpoint/Element/GetElementChildrenEndpoint';
import GetElementEndpoint from '~/Endpoint/Element/GetElementEndpoint';
import { Logger } from '~/Service/Logger';
import { WebSdkConfiguration } from '~/Service/WebSdkConfiguration';
import { createChildrenCollectionIdentifier } from '~/Type/Definition/ChildrenCollectionIdentifier';
import { Collection } from '~/Type/Definition/Collection';
import { Node } from '~/Type/Definition/Node';
import { Relation } from '~/Type/Definition/Relation';
import { Uuid } from '~/Type/Definition/Uuid';

class EmberNexus {
private cache: LRUCache<Uuid, Node | Relation>;
private elementCache: LRUCache<Uuid, Node | Relation>;
private collectionCache: LRUCache<string, Collection>;

constructor() {
this.cache = new LRUCache<Uuid, Node | Relation>({
this.elementCache = new LRUCache<Uuid, Node | Relation>({
max: Container.get(WebSdkConfiguration).getElementCacheMaxEntries(),
});
this.collectionCache = new LRUCache<string, Collection>({
max: Container.get(WebSdkConfiguration).getCollectionCacheMaxEntries(),
});
}

getElement(uuid: Uuid): Promise<Node | Relation> {
return new Promise<Node | Relation>((resolve) => {
if (this.cache.has(uuid)) {
const element = this.cache.get(uuid);
if (this.elementCache.has(uuid)) {
const element = this.elementCache.get(uuid);
if (element !== undefined) {
return resolve(element);
}
Expand All @@ -31,12 +38,35 @@ class EmberNexus {
Container.get(GetElementEndpoint)
.getElement(uuid)
.then((element) => {
this.cache.set(uuid, element);
this.elementCache.set(uuid, element);
return element;
}),
);
});
}

getElementChildren(parentUuid: Uuid, page: number = 1, pageSize: number | null = null): Promise<Collection> {
if (pageSize === null) {
pageSize = Container.get(WebSdkConfiguration).getCollectionPageSize();
}
const collectionCacheKey = createChildrenCollectionIdentifier(parentUuid, page, pageSize);
return new Promise<Collection>((resolve) => {
if (this.collectionCache.has(collectionCacheKey)) {
const collection = this.collectionCache.get(collectionCacheKey);
if (collection !== undefined) {
return resolve(collection);
}
}
return resolve(
Container.get(GetElementChildrenEndpoint)
.getElementChildren(parentUuid, page, pageSize as number)
.then((collection) => {
this.collectionCache.set(collectionCacheKey, collection);
return collection;
}),
);
});
}
}

export { EmberNexus, Container, WebSdkConfiguration, Logger };
77 changes: 47 additions & 30 deletions src/Endpoint/Element/GetElementChildrenEndpoint.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,67 @@
import { Service } from 'typedi';

import { EmberNexusError } from '~/Error/EmberNexusError';
import { NetworkError } from '~/Error/NetworkError';
import { ParseError } from '~/Error/ParseError';
import { ValidationError } from '~/Error/ValidationError';
import { CollectionParser } from '~/Service/CollectionParser';
import { FetchHelper } from '~/Service/FetchHelper';
import { Logger } from '~/Service/Logger';
import { RequestProblemParser } from '~/Service/RequestProblemParser';
import { Collection } from '~/Type/Definition/Collection';
import { RequestProblem } from '~/Type/Definition/RequestProblem';
import { Uuid } from '~/Type/Definition/Uuid';
import { RequestProblemCategory } from '~/Type/Enum/RequestProblemCategory';

@Service()
class GetElementChildrenEndpoint {
constructor(
private logger: Logger,
private fetchHelper: FetchHelper,
private collectionParser: CollectionParser,
private requestProblemParser: RequestProblemParser,
) {}

async getElementChildren(uuid: Uuid, page: number = 1, pageSize: number = 25): Promise<Collection> {
return new Promise<Collection>((resolve, reject) => {
this.fetchHelper
.runWrappedFetch(`/${uuid}/children?page=${page}&pageSize=${pageSize}`, this.fetchHelper.getDefaultGetOptions())
.then(async (response) => {
const data = await response.json();
if (response.ok) {
const collection = this.collectionParser.rawCollectionToCollection(data);
resolve(collection);
return;
}
const requestProblem = this.requestProblemParser.rawRequestProblemToRequestProblem(data);
this.logger.error(requestProblem);
reject(requestProblem);
return;
})
.catch((error) => {
const requestProblem = {
category: RequestProblemCategory.ClientSide,
title: `Encountered network problem`,
detail: `See exception for details.`,
exception: error,
} as RequestProblem;
this.logger.error(requestProblem);
reject(requestProblem);
});
});
return Promise.resolve()
.then(() => {
if (page < 1) {
return Promise.reject(new ValidationError('Page number must be at least 1.'));
}
if (pageSize < 1) {
return Promise.reject(new ValidationError('Page size must be at least 1.'));
}
const url = this.fetchHelper.buildUrl(`/${uuid}/children?page=${page}&pageSize=${pageSize}`);
this.logger.debug(`Executing HTTP GET request against url ${url} .`);
return fetch(url, this.fetchHelper.getDefaultGetOptions());
})
.catch((error) => {
if (error instanceof EmberNexusError) {
return Promise.reject(error);
}
return Promise.reject(new NetworkError(`Experienced generic network error during fetching resource.`, error));
})
.then(async (response: Response) => {
const contentType = response.headers.get('Content-Type');
if (contentType == null) {
return Promise.reject(new ParseError('Response does not contain content type header.'));
}
if (!(contentType.includes('application/json') || contentType.includes('application/problem+json'))) {
return Promise.reject(
new ParseError(
"Unable to parse response as content type is neither 'application/json' nor 'application/problem+json'.",
),
);
}
const data = await response.json();
if (!response.ok) {
return Promise.reject(this.fetchHelper.createResponseErrorFromBadResponse(response, data));
}
return data;
})
.then<Collection>((jsonResponse) => {
return this.collectionParser.rawCollectionToCollection(jsonResponse);
})
.catch((error) => {
this.logger.error(error.message, error);
return Promise.reject(error);
});
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/Error/EmberNexusError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class EmberNexusError extends Error {
constructor(message) {
super(message);
}
}

export { EmberNexusError };
4 changes: 3 additions & 1 deletion src/Error/NetworkError.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class NetworkError extends Error {
import { EmberNexusError } from '~/Error/EmberNexusError';

class NetworkError extends EmberNexusError {
constructor(message, originalError) {
super(message);
this.name = 'NetworkError';
Expand Down
4 changes: 3 additions & 1 deletion src/Error/ParseError.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class ParseError extends Error {
import { EmberNexusError } from '~/Error/EmberNexusError';

class ParseError extends EmberNexusError {
constructor(message) {
super(message);
this.name = 'ParseError';
Expand Down
4 changes: 3 additions & 1 deletion src/Error/ResponseError.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class ResponseError extends Error {
import { EmberNexusError } from '~/Error/EmberNexusError';

class ResponseError extends EmberNexusError {
private _type: string | null = null;
private _title: string | null = null;
private _detail: string | null = null;
Expand Down
10 changes: 10 additions & 0 deletions src/Error/ValidationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { EmberNexusError } from '~/Error/EmberNexusError';

class ValidationError extends EmberNexusError {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}

export { ValidationError };
22 changes: 22 additions & 0 deletions src/Service/WebSdkConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ class WebSdkConfiguration {
private token: Token | null;
private apiHost: string;
private elementCacheMaxEntries: number;
private collectionCacheMaxEntries: number;
private collectionPageSize: number;

constructor() {
this.token = null;
this.apiHost = '';
this.elementCacheMaxEntries = 100;
this.collectionCacheMaxEntries = 50;
this.collectionPageSize = 25;
}

hasToken(): boolean {
Expand Down Expand Up @@ -41,6 +45,24 @@ class WebSdkConfiguration {
this.elementCacheMaxEntries = value;
return this;
}

getCollectionCacheMaxEntries(): number {
return this.collectionCacheMaxEntries;
}

setCollectionCacheMaxEntries(value: number): WebSdkConfiguration {
this.collectionCacheMaxEntries = value;
return this;
}

getCollectionPageSize(): number {
return this.collectionPageSize;
}

setCollectionPageSize(value: number): WebSdkConfiguration {
this.collectionPageSize = value;
return this;
}
}

export { WebSdkConfiguration };
14 changes: 14 additions & 0 deletions src/Type/Definition/ChildrenCollectionIdentifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Branded } from '~/Type/Definition/Branded';
import { Uuid } from '~/Type/Definition/Uuid';

type ChildrenCollectionIdentifier = Branded<string, 'COLLECTION_IDENTIFIER'>;

function createChildrenCollectionIdentifier(
parentUuid: Uuid,
page: number,
pageSize: number,
): ChildrenCollectionIdentifier {
return `children-collection-of-parent-${parentUuid}-page-size-${pageSize}-page-${page}` as ChildrenCollectionIdentifier;
}

export { ChildrenCollectionIdentifier, createChildrenCollectionIdentifier };
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { expect } from 'chai';
import { HttpResponse, http } from 'msw';
// eslint-disable-next-line import/no-unresolved
import { setupServer } from 'msw/node';
import { Container } from 'typedi';

import GetElementChildrenEndpoint from '~/Endpoint/Element/GetElementChildrenEndpoint';
import { Logger } from '~/Service/Logger';
import { WebSdkConfiguration } from '~/Service/WebSdkConfiguration';
import { validateUuidFromString } from '~/Type/Definition/Uuid';

import { TestLogger } from '../../../TestLogger';

const mockServer = setupServer(
http.get('http://mock-api/07212e8a-14cc-4f45-a3e9-1179080bbd61/children', () => {
return HttpResponse.json(
{
type: '_PartialCollection',
id: '/07212e8a-14cc-4f45-a3e9-1179080bbd61/children',
totalNodes: 2,
links: {
first: '/07212e8a-14cc-4f45-a3e9-1179080bbd61/children',
previous: null,
next: null,
last: '/07212e8a-14cc-4f45-a3e9-1179080bbd61/children',
},
nodes: [
{
type: 'Tag',
id: '45482998-274a-43d0-a466-f31d0b24cc0a',
data: {
created: '2023-10-25T10:44:39+00:00',
updated: '2023-10-25T10:44:39+00:00',
name: 'Yellow',
color: '#FFC835',
},
},
{
type: 'Tag',
id: '6b8341ca-851a-4e98-8194-e57b87d30519',
data: {
created: '2023-10-25T10:44:39+00:00',
updated: '2023-10-25T10:44:39+00:00',
name: 'Red',
color: '#BD002A',
},
},
],
relations: [
{
type: 'OWNS',
id: 'bc1ba1ad-5866-4c23-a58e-15282994c72c',
start: '07212e8a-14cc-4f45-a3e9-1179080bbd61',
end: '45482998-274a-43d0-a466-f31d0b24cc0a',
data: {
created: '2023-10-25T10:44:42+00:00',
updated: '2023-10-25T10:44:42+00:00',
},
},
{
type: 'OWNS',
id: '94ab04d8-c7a0-408c-aea7-59c66018b242',
start: '07212e8a-14cc-4f45-a3e9-1179080bbd61',
end: '6b8341ca-851a-4e98-8194-e57b87d30519',
data: {
created: '2023-10-25T10:44:42+00:00',
updated: '2023-10-25T10:44:42+00:00',
},
},
],
},
{
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
},
);
}),
);

const testLogger: TestLogger = new TestLogger();
Container.set(Logger, testLogger);
Container.get(WebSdkConfiguration).setApiHost('http://mock-api');

test('GetElementChildrenEndpoint should handle collection response', async () => {
mockServer.listen();
const uuid = validateUuidFromString('07212e8a-14cc-4f45-a3e9-1179080bbd61');

const collection = await Container.get(GetElementChildrenEndpoint).getElementChildren(uuid);

expect(
testLogger.assertDebugHappened(
'Executing HTTP GET request against url http://mock-api/07212e8a-14cc-4f45-a3e9-1179080bbd61/children?page=1&pageSize=25 .',
),
).to.be.true;

expect(collection).to.have.keys('id', 'links', 'totalNodes', 'nodes', 'relations');
expect(Object.keys(collection.nodes)).to.have.lengthOf(2);
expect(Object.keys(collection.relations)).to.have.lengthOf(2);

mockServer.close();
});
Loading

0 comments on commit e60b74e

Please sign in to comment.