Skip to content

Commit

Permalink
feat(angular-data-source-graphql): added graphql data source
Browse files Browse the repository at this point in the history
  • Loading branch information
JBBianchi committed Jan 7, 2024
1 parent d6ecce1 commit 60cbd63
Show file tree
Hide file tree
Showing 16 changed files with 1,293 additions and 81 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Observable } from 'rxjs';
import buildQuery from 'odata-query';
import { HttpRequestInfo, ODataQueryResultDto, logHttpRequest } from '@neuroglia/angular-rest-core';
import { CombinedParams, QueryableDataSource } from '@neuroglia/angular-data-source-queryable';
import { Injectable, inject } from '@angular/core';
import {
GRAPHQL_DATA_SOURCE_ARGS,
GRAPHQL_DATA_SOURCE_ENDPOINT,
GRAPHQL_DATA_SOURCE_FIELDS,
GRAPHQL_DATA_SOURCE_QUERY_BUILDER,
GRAPHQL_DATA_SOURCE_TARGET,
GRAPHQL_DATA_SOURCE_VARIABLES_MAPPER,
} from './injection-tokens';
import { GraphQLQueryArguments, GraphQLQueryBuilder, GraphQLVariablesMapper } from './models';

/**
* A data source used to handle GraphQL interactions
*/
@Injectable()
export class GraphQLDataSource<T = any> extends QueryableDataSource<T> {
protected endpoint: string = inject(GRAPHQL_DATA_SOURCE_ENDPOINT);
protected target: string = inject(GRAPHQL_DATA_SOURCE_TARGET);
protected fields: string[] = inject(GRAPHQL_DATA_SOURCE_FIELDS);
protected args: GraphQLQueryArguments | null = inject(GRAPHQL_DATA_SOURCE_ARGS, { optional: true });
protected variablesMapper: GraphQLVariablesMapper | null = inject(GRAPHQL_DATA_SOURCE_VARIABLES_MAPPER, {
optional: true,
});
protected queryBuilder: GraphQLQueryBuilder | null = inject(GRAPHQL_DATA_SOURCE_QUERY_BUILDER, { optional: true });

constructor() {
super();
this.loggerName = `GraphQLDataSource|${this.endpoint}`;
this.logger = this.namedLoggingServiceFactory.create(this.loggerName);
}

/**
* Builds the query
* @param combinedParams
*/
protected buildQuery(combinedParams: CombinedParams<T>): string {
if (this.queryBuilder) {
return this.queryBuilder(this.target, this.args, this.fields, combinedParams);
}
const operationName = 'QueryDataSource';
const select = combinedParams[0]?.select;
const expand = combinedParams[1]?.expand;
let selectAndExpand: string[] = [];
if (select) {
if (!Array.isArray(select)) {
selectAndExpand.push(select as string);
} else if (select.length) {
selectAndExpand = [...selectAndExpand, ...(select as string[])];
}
}
if (expand) {
if (!Array.isArray(expand)) {
selectAndExpand.push(expand as string);
} else if (expand.length) {
selectAndExpand = [...selectAndExpand, ...(expand as string[])];
}
}
const fields = (!selectAndExpand.length ? this.fields : selectAndExpand).join('\n');
let operationArgs = '($options: QueryOptionsInput)';
let targetArgs = '(options: $options)';
if (this.args && Object.keys(this.args).length) {
operationArgs = `(${Object.entries(this.args).reduce(
(acc, [name, type], idx) => acc + `${idx !== 0 ? ',' : ''}$${name}: ${type}`,
'',
)})`;
targetArgs = `(${Object.keys(this.args).reduce(
(acc, name, idx) => acc + `${idx !== 0 ? ',' : ''}${name}: $${name}`,
'',
)})`;
}
const query = `query ${operationName} ${operationArgs} {
${this.target} ${targetArgs} {
${fields}
}
}`;
const options = Object.fromEntries(
combinedParams
.flatMap((param) => (param ? Object.entries(param) : []))
.filter(([key, value]) => key != 'select' && (!Array.isArray(value) ? value != null : !!value?.length)),
);
const variables = this.variablesMapper ? this.variablesMapper(this.args, combinedParams) : { options };
return JSON.stringify({ operationName, query, variables });
}

/**
* Queries the GraphQL endpoint
* @param query
*/
protected gatherData(query: string): Observable<ODataQueryResultDto<T>> {
const url: string = `${this.endpoint}`;
const httpRequestInfo: HttpRequestInfo = new HttpRequestInfo({
clientServiceName: this.loggerName,
methodName: 'gatherData',
verb: 'post',
url,
});
return logHttpRequest(
this.logger,
this.errorObserver,
this.http.post<ODataQueryResultDto<T>>(url, JSON.parse(query)),
httpRequestInfo,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { InjectionToken } from '@angular/core';
import { GraphQLQueryBuilder, GraphQLVariablesMapper } from './models';

export const GRAPHQL_DATA_SOURCE_ENDPOINT = new InjectionToken<string>('graphql-data-source-endpoint');
export const GRAPHQL_DATA_SOURCE_TARGET = new InjectionToken<string>('graphql-data-source-target');
export const GRAPHQL_DATA_SOURCE_FIELDS = new InjectionToken<string[]>('graphql-data-source-fields');
export const GRAPHQL_DATA_SOURCE_ARGS = new InjectionToken<{ [arg: string]: string }>('graphql-data-source-args');
export const GRAPHQL_DATA_SOURCE_QUERY_BUILDER = new InjectionToken<GraphQLQueryBuilder>(
'graphql-data-source-query-builder',
);
export const GRAPHQL_DATA_SOURCE_VARIABLES_MAPPER = new InjectionToken<GraphQLVariablesMapper>(
'graphql-data-source-variables-mapper',
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* An { argumentName: argumentType } map
*/
export interface GraphQLQueryArguments {
[key: string]: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CombinedParams } from '@neuroglia/angular-data-source-queryable';
import { GraphQLQueryArguments } from './graphql-query-arguments';

/**
* Builds a GraphQL query based on the provided context
*/
export type GraphQLQueryBuilder = <T = any>(
target: string,
args: GraphQLQueryArguments | null,
fields: string[],
combinedParams: CombinedParams<T>,
) => string;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CombinedParams } from '@neuroglia/angular-data-source-queryable';
import { GraphQLQueryArguments } from './graphql-query-arguments';

/**
* Builds a GraphQL query based on the provided context
*/
export type GraphQLVariablesMapper = <T = any>(
args: GraphQLQueryArguments | null,
combinedParams: CombinedParams<T>,
) => any;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './graphql-query-builder';
export * from './graphql-query-arguments';
export * from './graphql-variables-mapper';
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
* Public API Surface of angular-data-source-graphql
*/

export * from './lib/angular-data-source-graphql.service';
export * from './lib/angular-data-source-graphql.component';
export * from './lib/graphql-data-source';
export * from './lib/injection-tokens';
export * from './lib/models';
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedProductsResponse.value);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
});
Expand All @@ -364,6 +368,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedProductsResponse.value);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
timer(1000)
.pipe(
Expand Down Expand Up @@ -392,6 +400,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
});
Expand All @@ -407,6 +419,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedProductsWithSuppliersResponse.value);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
});
Expand All @@ -423,6 +439,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});

Expand All @@ -437,6 +457,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});

Expand All @@ -451,6 +475,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedProductsResponse.value);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
});
Expand All @@ -467,6 +495,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
it('should emit all the items ordered by descending name', (done) => {
Expand All @@ -480,6 +512,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
});
Expand All @@ -496,6 +532,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
it('should emit the item with name equals to "Bread" with an object filter', (done) => {
Expand All @@ -509,6 +549,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
it('should emit the items with name containing "ui" with a string filter', (done) => {
Expand All @@ -522,6 +566,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
it('should emit the items with name containing "ui" with an object filter', (done) => {
Expand All @@ -535,6 +583,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
it('should emit the items with name not containing "ui" with a string filter', (done) => {
Expand All @@ -548,6 +600,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
it('should emit the items with name not containing "ui" with an object filter', (done) => {
Expand All @@ -561,6 +617,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
it('should emit the items from supplier "Exotic Liquids"', (done) => {
Expand All @@ -576,6 +636,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
});
Expand All @@ -598,6 +662,10 @@ describe('OData Data Source', () => {
expect(data).toEqual(expectedValues);
done();
},
error: (err) => {
expect(err).withContext('error').toBeNull();
done();
},
});
});
});
Expand Down
Loading

0 comments on commit 60cbd63

Please sign in to comment.