Skip to content

Commit

Permalink
fix(global): random improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
JBBianchi committed Nov 29, 2023
1 parent 27b80ae commit 0730e59
Show file tree
Hide file tree
Showing 14 changed files with 227 additions and 20 deletions.
22 changes: 18 additions & 4 deletions projects/neuroglia/angular-common/src/lib/pipes/human-case.pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,28 @@ import { Pipe, PipeTransform } from '@angular/core';
import { humanCase } from '@neuroglia/common';

/**
* A pipe to transform text into "Human case"
* A pipe to transform `PascalCase`/`camelCase` into `Human Case`
*/
@Pipe({
name: 'humanCase',
})
export class HumanCasePipe implements PipeTransform {
transform(source: string, keepCapitalLetters: boolean = false): string {
if (!source) return '';
return humanCase(source, keepCapitalLetters);
transform(value: string, keepCapitalLetters: boolean = false, removeUnions: boolean = false): string {
if (!value) return '';
let transformable = value.trim();
if (removeUnions) {
transformable = transformable.replace(/[\-_](.?)/g, (match, capture) => capture.toUpperCase());
}
transformable =
transformable[0].toUpperCase() +
transformable
.slice(1)
.replace(/([A-Z])/g, ' $1')
.replace(/\s+/g, ' ');
if (keepCapitalLetters) {
return transformable;
} else {
return transformable.toLowerCase();
}
}
}
2 changes: 2 additions & 0 deletions projects/neuroglia/angular-rest-core/src/lib/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './http-error-info';
export * from './http-request-info';
export * from './odata-query-options';
export * from './odata-query-result-dto';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ModelConstructor } from '@neuroglia/common';

/**
* Represents the options used to configure an OData query
*/
export class ODataQueryOptions extends ModelConstructor {
constructor(model?: any) {
super(model);
}

$filter?: string | undefined;
$orderBy?: string | undefined;
$select?: string | undefined;
$skip?: number | undefined;
$top?: number | undefined;
$count?: boolean | undefined;
$expand?: string | undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ModelConstructor } from '@neuroglia/common';

/**
* The results of an OData query
*/
export class ODataQueryResultDto<T> extends ModelConstructor {
constructor(model?: any) {
super(model);
this.value = model.value ? model.value.map((m: any) => m as T) : [];
this['@odata.context'] = model['@odata.context'] as string;
this['@odata.count'] = model['@odata.count'] as number;
}

value: T[];
'@odata.context': string;
'@odata.count': number;
}
121 changes: 114 additions & 7 deletions projects/neuroglia/angular-rest-core/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { HttpErrorResponse } from '@angular/common/http';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { ILogger } from '@neuroglia/logging';
import { Observable, of } from 'rxjs';
import { mergeMap, tap } from 'rxjs/operators';
import { HttpErrorInfo, HttpRequestInfo } from './models';
import { EMPTY, Observable, of } from 'rxjs';
import { catchError, expand, map, tap } from 'rxjs/operators';
import { HttpErrorInfo, HttpRequestInfo, ODataQueryOptions } from './models';
import { HttpErrorObserverService } from './http-error-observer.service';

/**
Expand All @@ -26,9 +26,8 @@ export function logHttpRequest<T>(
httpRequest: Observable<T>,
httpRequestInfo: HttpRequestInfo,
): Observable<T> {
return of(null).pipe(
tap(() => logger.log(`${httpRequestInfo.info} | call.`, httpRequestInfo)),
mergeMap(() => httpRequest),
logger.log(`${httpRequestInfo.info} | call.`, httpRequestInfo);
return httpRequest.pipe(
tap({
next: () => logger.log(`${httpRequestInfo.info} | succeeded.`, httpRequestInfo),
error: (err: HttpErrorResponse) => {
Expand All @@ -38,3 +37,111 @@ export function logHttpRequest<T>(
}),
);
}

export interface outcome<T> {
response: any;
results: T[];
}

/**
* Recursively fetches data for `sourceUrl`
* @param logger The `ILogger` to use
* @param errorObserver A `HttpErrorObserverService` instance
* @param http A `HttpClient`
* @param sourceUrl The URL to fetch the data from
* @param pageLength The number of results expected per page
* @param pageLengthParam The name of the query string parameter for the pager length
* @param skipParam The name of the query string parameter for the pager index/skip
* @param resultsSelector A function to select the results from the response
* @param until A function that indicates when to terminate processing by returning true
* @param computeSkip A function to compute the next index/skip parameter
* @returns
*/
export function recursiveFetch<TResult>(
logger: ILogger,
errorObserver: HttpErrorObserverService,
http: HttpClient,
sourceUrl: string,
pageLength: number = 50,
pageLengthParam: string = '$top',
skipParam: string = '$skip',
resultsSelector: (response: any) => TResult[] = (response: any) => response as TResult[],
until: (response: any, results: TResult[]) => boolean = (response: any, _: TResult[]): boolean =>
(response || []).length < pageLength,
computeSkip: (length: number, index: number) => number = (length: number, index: number) => length * index,
): Observable<TResult[]> {
let skip = 0;
let index = 0;
return of(undefined).pipe(
expand((outcome: outcome<TResult> | null | undefined) => {
if (outcome !== undefined && (outcome === null || until(outcome.response, outcome.results as TResult[]))) {
return EMPTY;
}
skip = computeSkip(pageLength, index);
index++;
const url = new URL(sourceUrl);
const params = url.searchParams || new URLSearchParams();
params.set(pageLengthParam, pageLength.toString());
params.set(skipParam, skip.toString());
url.search = params.toString();
const httpRequestInfo: HttpRequestInfo = new HttpRequestInfo({
clientServiceName: 'none',
methodName: 'recursiveFetch',
verb: 'get',
url: url.toString(),
});
return logHttpRequest(logger, errorObserver, http.get<any>(url.toString()), httpRequestInfo).pipe(
map(
(response: any): outcome<TResult> =>
({
response,
results: [...(outcome?.results || []), ...resultsSelector(response)],
}) as outcome<TResult>,
),
catchError((err) => of(null as any)),
);
}),
map((outcome: outcome<TResult>) => outcome?.results || []),
);
}

export function recursiveODataServiceFetch<TResult>(
service: any,
getter: (queryOptions: ODataQueryOptions) => Observable<TResult[]>,
queryOptions: ODataQueryOptions = {},
isAPI: boolean = true,
): Observable<TResult[]> {
queryOptions = queryOptions || {};
const $top = queryOptions.$top || 50;
let $skip = 0 - $top; // will be set to 0 at the first call of expand()
const until = (response: any, results: TResult[]): boolean => {
if (isAPI) {
return (response || []).length < $top;
}
return results.length >= response['@odata.count'];
};
const resultsSelector = (response: any): TResult[] =>
isAPI ? (response as TResult[]) : (response.value as TResult[]);
return of(undefined).pipe(
expand((outcome: outcome<TResult> | null | undefined) => {
if (outcome !== undefined && (outcome === null || until(outcome.response, outcome.results as TResult[]))) {
return EMPTY;
}
$skip += $top;
const query = { ...queryOptions, $top, $skip, $count: true };
return getter
.bind(service)(query)
.pipe(
map(
(response: any): outcome<TResult> =>
({
response,
results: [...(outcome?.results || []), ...resultsSelector(response)],
}) as outcome<TResult>,
),
catchError((err) => of(null as any)),
);
}),
map((outcome: outcome<TResult>) => outcome?.results || []),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { InjectionToken } from '@angular/core';

/** A token to provide an `accessTokenFactory` (see SignalR IHttpConnectionOptions) */
export const ACCESS_TOKEN_FACTORY_TOKEN = new InjectionToken<string>(
'internal-access-token-factory-a1e016d878d74104a94391ce6d908eee',
);
1 change: 1 addition & 0 deletions projects/neuroglia/angular-signalr/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './angular-signalr.module';
export * from './signalr.service';
export * from './access-token-factory-token';
8 changes: 5 additions & 3 deletions projects/neuroglia/angular-signalr/src/lib/signalr.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as signalR from '@microsoft/signalr';
import { Inject, Injectable, OnDestroy } from '@angular/core';
import { Inject, Injectable, OnDestroy, Optional } from '@angular/core';
import { Observable, BehaviorSubject, from, Subject, Observer, of, throwError, timer } from 'rxjs';
import { mergeMap, retryWhen, tap } from 'rxjs/operators';

Expand All @@ -8,6 +8,7 @@ import { NamedLoggingServiceFactory } from '@neuroglia/angular-logging';
import { HttpErrorObserverService, HttpRequestInfo, logHttpRequest } from '@neuroglia/angular-rest-core';
import { WS_URL_TOKEN } from './ws-url-token';
import { LABEL_TOKEN } from './label-token';
import { ACCESS_TOKEN_FACTORY_TOKEN } from './access-token-factory-token';

const defaultReconnectPolicyFactory: (delays?: (number | null)[]) => signalR.IRetryPolicy = (
delays = [0, 2000, 10000, 30000],
Expand Down Expand Up @@ -35,7 +36,7 @@ export abstract class SignalRService implements OnDestroy {

/**
* Creates a new SignalRService instance
* @param apiUrl the base url of the websocket API, e.g.: https://server.com/websockets
* @param wsUrl the base url of the websocket API, e.g.: https://server.com/websockets
* @param errorObserver an instance of @see HttpErrorObserverService
* @param namedLoggingServiceFactory an instance of @see NamedLoggingServiceFactory
* @param loggingLabel the label used for logging for the current instance
Expand All @@ -45,6 +46,7 @@ export abstract class SignalRService implements OnDestroy {
protected errorObserver: HttpErrorObserverService,
protected namedLoggingServiceFactory: NamedLoggingServiceFactory,
@Inject(LABEL_TOKEN) protected label: string,
@Optional() @Inject(ACCESS_TOKEN_FACTORY_TOKEN) protected accessTokenFactory: () => string | Promise<string>,
) {
//super(wsUrl, errorObserver, namedLoggingServiceFactory, label);
this.logger = this.namedLoggingServiceFactory.create(label);
Expand Down Expand Up @@ -78,7 +80,7 @@ export abstract class SignalRService implements OnDestroy {
return of();
}
let builder: signalR.HubConnectionBuilder = new signalR.HubConnectionBuilder()
.withUrl(this.url)
.withUrl(this.url, { accessTokenFactory: this.accessTokenFactory })
.configureLogging(logLevel);
if (withAutomaticReconnect) {
if (typeof withAutomaticReconnect === typeof true) {
Expand Down
10 changes: 8 additions & 2 deletions projects/neuroglia/common/src/lib/deep-copy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
/**
* Returns a deep copy of the provided object
* @param object the object to copy
* @param replacer A function that transforms the results.
* @param reviver A function that transforms the results. This function is called for each member of the object.
* @returns A deep copy of the given object
*/
export const deepCopy = (object: any): any => {
return JSON.parse(JSON.stringify(object));
export const deepCopy = (
object: any,
replacer?: (this: any, key: string, value: any) => any,
reviver?: (this: any, key: string, value: any) => any,
): any => {
return JSON.parse(JSON.stringify(object, replacer), reviver);
};
1 change: 1 addition & 0 deletions projects/neuroglia/common/src/lib/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './case-convertion-options';
export * from './key-value-pair';
export * from './model-constructor';
export * from './operation-error';
export * from './operation-result';
export * from './storage-entry.interface';
export * from './storage-handler.interface';
export * from './identifiable-record';
3 changes: 3 additions & 0 deletions projects/neuroglia/common/src/lib/models/operation-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import { ModelConstructor } from './model-constructor';
export class OperationError extends ModelConstructor {
constructor(model?: any) {
super(model);
this.key = this.key || this.code;
this.code = this.code || this.key;
}

key: string;
code: string;
message: string;
}
17 changes: 17 additions & 0 deletions projects/neuroglia/common/src/lib/models/operation-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ModelConstructor } from './model-constructor';
import { OperationError } from './operation-error';

export class OperationResult<T = any> extends ModelConstructor {
constructor(model?: any) {
super(model);
if (model) {
this.errors = model.errors ? model.errors.map((m: any) => new OperationError(m)) : [];
}
}

code: string;
errors: OperationError[];
returned: boolean;
suceeded: boolean;
data: T;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { get, isObject } from '@neuroglia/common';

Check failure on line 1 in projects/neuroglia/string-formatter/src/lib/string-formatter.ts

View workflow job for this annotation

GitHub Actions / pipeline (string-formatter) / build / build

Cannot find module '@neuroglia/common' or its corresponding type declarations.

/**
* Formats the provided message with the provided array of parameters
* @param message the message with numeric arguments like {0}, {1}...
Expand All @@ -9,23 +11,34 @@ export function strFormat(message: string, ...params: any[]): string {
return message.replace(/{(\d+)}/g, (pattern, index) => params[index] || pattern);
}

function replacer(payload: any, pattern: string, ...matches: any[]): string {
const value = get(payload, matches[0], pattern);
if (!value) {
return '';
}
if (typeof value !== 'object') {
return value;
}
return JSON.stringify(value);
}
/**
* Formats the provided message with the provided object
* @param message the message with string interpolation like placeholders (eg: ${name})
* @param params the object to format the message with
*/
export function strformatNamed(message: string, params: any): string {
export function strFormatNamed(message: string, params: any): string {
if (!message) return '';
if (!params) return message;
return message.replace(/\${(\w+)}/g, (pattern, match) => params[match] || pattern);
const needle = /\$\{([^}]*)\}/g;
return message.replace(needle, replacer.bind(null, params));
}

/**
* Formats the provided message with the provided object (works with nested properties but is unsafe)
* @param message the message with string interpolation like placeholders (eg: ${name})
* @param params the object to format the message with
*/
export function strformatNamedUnsafe(message: string, params: any): string {
export function strFormatNamedUnsafe(message: string, params: any): string {
if (!message || !params) return message || '';
const renderer = new Function('p', 'return `' + message.replace(/\$\{/g, '${p.') + '`;');
return renderer(params);
Expand Down
2 changes: 1 addition & 1 deletion projects/neuroglia/string-formatter/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
* Public API Surface of string-formatter
*/

export * from './lib/string-formatter.service';
export * from './lib/string-formatter';

0 comments on commit 0730e59

Please sign in to comment.