Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds support for client generics, improving type safety in comb… #401

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
5 changes: 4 additions & 1 deletion lib/client/delivery-client.factory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ClientTypes } from '../models';
import { IDeliveryClientConfig } from '../config/delivery-configs';
import { DeliveryClient } from './delivery-client';

export function createDeliveryClient(config: IDeliveryClientConfig): DeliveryClient {
export function createDeliveryClient<TClientTypes extends ClientTypes = ClientTypes>(
config: IDeliveryClientConfig
): DeliveryClient<TClientTypes> {
return new DeliveryClient(config);
}
50 changes: 30 additions & 20 deletions lib/client/delivery-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { HttpService } from '@kontent-ai/core-sdk';

import { IDeliveryClientConfig } from '../config';
import { IContentItem } from '../models';
import { ClientTypes, IContentItem } from '../models';
import {
ElementQuery,
ItemsFeedQuery,
Expand All @@ -19,9 +18,9 @@ import { sdkInfo } from '../sdk-info.generated';
import { IMappingService, MappingService, QueryService } from '../services';
import { IDeliveryClient } from './idelivery-client.interface';

export class DeliveryClient implements IDeliveryClient {
private queryService: QueryService;
public mappingService: IMappingService;
export class DeliveryClient<TClientTypes extends ClientTypes = ClientTypes> implements IDeliveryClient {
private queryService: QueryService<TClientTypes>;
public mappingService: IMappingService<TClientTypes>;

/**
* Delivery client used to fetch data from Kontent.ai
Expand Down Expand Up @@ -49,82 +48,93 @@ export class DeliveryClient implements IDeliveryClient {
/**
* Gets query for multiple languages
*/
languages(): LanguagesQuery {
languages(): LanguagesQuery<TClientTypes> {
return new LanguagesQuery(this.config, this.queryService);
}

/**
* Gets query for multiple types
*/
types(): MultipleTypeQuery {
types(): MultipleTypeQuery<TClientTypes> {
return new MultipleTypeQuery(this.config, this.queryService);
}

/**
* Gets query for single type
* @param {string} typeCodename - Codename of the type to fetch
*/
type(typeCodename: string): SingleTypeQuery {
type(typeCodename: TClientTypes['contentTypeCodenames']): SingleTypeQuery<TClientTypes> {
return new SingleTypeQuery(this.config, this.queryService, typeCodename);
}

/**
* Gets query for multiple items
*/
items<TContentItem extends IContentItem = IContentItem>(): MultipleItemsQuery<TContentItem> {
return new MultipleItemsQuery<TContentItem>(this.config, this.queryService);
items<TContentItem extends IContentItem = TClientTypes['contentItemType']>(): MultipleItemsQuery<
TClientTypes,
TContentItem
> {
return new MultipleItemsQuery<TClientTypes, TContentItem>(this.config, this.queryService);
}

/**
* Gets query for single item
* @param {string} codename - Codename of item to fetch
*/
item<TContentItem extends IContentItem = IContentItem>(codename: string): SingleItemQuery<TContentItem> {
return new SingleItemQuery<TContentItem>(this.config, this.queryService, codename);
item<TContentItem extends IContentItem = TClientTypes['contentItemType']>(
codename: string
): SingleItemQuery<TClientTypes, TContentItem> {
return new SingleItemQuery<TClientTypes, TContentItem>(this.config, this.queryService, codename);
}

/**
* Gets query for items feed. Executes single HTTP request only
*/
itemsFeed<TContentItem extends IContentItem = IContentItem>(): ItemsFeedQuery<TContentItem> {
return new ItemsFeedQuery<TContentItem>(this.config, this.queryService);
itemsFeed<TContentItem extends IContentItem = TClientTypes['contentItemType']>(): ItemsFeedQuery<
TClientTypes,
TContentItem
> {
return new ItemsFeedQuery<TClientTypes, TContentItem>(this.config, this.queryService);
}

/**
* Gets query for single taxonomy
* @param {string} codename - Codename of taxonomy to fetch
*/
taxonomy(codename: string): TaxonomyQuery {
taxonomy(codename: TClientTypes['taxonomyCodenames']): TaxonomyQuery<TClientTypes> {
return new TaxonomyQuery(this.config, this.queryService, codename);
}

/**
* Gets query for multiple taxonomies
*/
taxonomies(): TaxonomiesQuery {
return new TaxonomiesQuery(this.config, this.queryService);
taxonomies(): TaxonomiesQuery<TClientTypes> {
return new TaxonomiesQuery<TClientTypes>(this.config, this.queryService);
}

/**
* Gets query for an element within a type
* @param {string} typeCodename - Codename of the type
* @param {string} elementCodename - Codename of the element
*/
element(typeCodename: string, elementCodename: string): ElementQuery {
element(
typeCodename: TClientTypes['contentTypeCodenames'],
elementCodename: TClientTypes['elementCodenames']
): ElementQuery<TClientTypes> {
return new ElementQuery(this.config, this.queryService, typeCodename, elementCodename);
}

/**
* Gets query for initializing sync
*/
initializeSync(): InitializeSyncQuery {
initializeSync(): InitializeSyncQuery<TClientTypes> {
return new InitializeSyncQuery(this.config, this.queryService);
}

/**
* Gets query fetching delta updates of content items
*/
syncChanges(): SyncChangesQuery {
syncChanges(): SyncChangesQuery<TClientTypes> {
return new SyncChangesQuery(this.config, this.queryService);
}
}
39 changes: 25 additions & 14 deletions lib/client/idelivery-client.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IContentItem } from '../models';
import { ClientTypes, IContentItem } from '../models';
import {
ElementQuery,
ItemsFeedQuery,
Expand All @@ -14,67 +14,78 @@ import {
} from '../query';
import { IMappingService } from '../services';

export interface IDeliveryClient {
export interface IDeliveryClient<TClientTypes extends ClientTypes = ClientTypes> {
/**
* Mapping service - can be used to get strongly typed responses from json result
*/
mappingService: IMappingService;
mappingService: IMappingService<TClientTypes>;

/**
* Gets query for languages
*/
languages(): LanguagesQuery;
languages(): LanguagesQuery<TClientTypes>;

/**
* Gets query for multiple types
*/
types(): MultipleTypeQuery;
types(): MultipleTypeQuery<TClientTypes>;

/**
* Gets query for single type
* @param {string} typeCodename - Codename of the type to retrieve
*/
type(typeCodename: string): SingleTypeQuery;
type(typeCodename: TClientTypes['contentTypeCodenames']): SingleTypeQuery<TClientTypes>;

/**
* Gets query for multiple items
*/
items<TContentItem extends IContentItem = IContentItem>(): MultipleItemsQuery<TContentItem>;
items<TContentItem extends IContentItem = TClientTypes['contentItemType']>(): MultipleItemsQuery<
TClientTypes,
TContentItem
>;

/**
* Gets query for items feed. Executes single HTTP request only
*/
itemsFeed<TContentItem extends IContentItem = IContentItem>(): ItemsFeedQuery<TContentItem>;
itemsFeed<TContentItem extends IContentItem = TClientTypes['contentItemType']>(): ItemsFeedQuery<
TClientTypes,
TContentItem
>;

/**
* Gets query for single item
* @param {string} codename - Codename of item to retrieve
*/
item<TContentItem extends IContentItem = IContentItem>(codename: string): SingleItemQuery<TContentItem>;
item<TContentItem extends IContentItem = TClientTypes['contentItemType']>(
codename: string
): SingleItemQuery<TClientTypes, TContentItem>;

/**
* Gets query for multiple taxonomies
*/
taxonomies(): TaxonomiesQuery;
taxonomies(): TaxonomiesQuery<TClientTypes>;

/**
* Gets query for single item
* @param {string} codename - Codename of taxonomy to retrieve
*/
taxonomy(codename: string): TaxonomyQuery;
taxonomy(codename: TClientTypes['taxonomyCodenames']): TaxonomyQuery<TClientTypes>;

/**
* Gets query for an element within a type
*/
element(typeCodename: string, elementCodename: string): ElementQuery;
element(
typeCodename: TClientTypes['contentTypeCodenames'],
elementCodename: TClientTypes['elementCodenames']
): ElementQuery<TClientTypes>;

/**
* Gets query for initializing sync
*/
initializeSync(): InitializeSyncQuery;
initializeSync(): InitializeSyncQuery<TClientTypes>;

/**
* Gets query fetching delta updates of content items
*/
syncChanges(): SyncChangesQuery;
syncChanges(): SyncChangesQuery<TClientTypes>;
}
17 changes: 9 additions & 8 deletions lib/mappers/element.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IDeliveryClientConfig } from '../config';
import { Contracts } from '../contracts';
import { ElementModels, Elements, ElementType } from '../elements';
import {
ClientTypes,
IContentItem,
IContentItemsContainer,
IContentItemWithRawDataContainer,
Expand All @@ -18,15 +19,15 @@ interface IRichTextImageUrlRecord {
newUrl: string;
}

export class ElementMapper {
export class ElementMapper<TClientTypes extends ClientTypes> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this class doesn't need to send the whole type of object TClientTypes it just needs need type TClientTypes['contentItemType'].

It would be easier to test this class if you need to pass only the necessary stuff

Check also the other classes for this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't, the only reason I passed all types is for consistency and ease of use. Do you think it's worth splitting?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand the point of ease of use, I am not sure about consistency. These generic types are new after all, so they are only consistent with the new code you are writing :).
For me there are two options:

  1. Pass only the required type, which is more correct of system modelling. This improves the type dependency coupling.
    • If I wanted to unit test only ElementMapper I would need to define the whole type to the generic. That would be bothering as the class really only needs type of contentItemType. So for me as a tester of ElementMapper would be nice to pass just ElementMapper<TContentItemType>
  2. If you just want to pass the object everywhere, I guess there is not as elegant workaround to satisfy both ends (ease of use and creating only part of the object)
    ElementMapper<TClientTypes extends Pick<ClientTypes, 'contentItemType'>>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me it seemed more straightforward to just pass the TClientTypes as a whole, but I changed it so the mappers now need only required generic arguments :)

constructor(private readonly config: IDeliveryClientConfig) {}

mapElements<TContentItem extends IContentItem = IContentItem>(data: {
mapElements<TContentItem extends IContentItem = TClientTypes['contentItemType']>(data: {
dataToMap: IContentItemWithRawElements;
processedItems: IContentItemsContainer;
processedItems: IContentItemsContainer<TClientTypes['contentItemType']>;
processingStartedForCodenames: string[];
preparedItems: IContentItemWithRawDataContainer;
}): IMapElementsResult<TContentItem> | undefined {
}): IMapElementsResult<TContentItem, TClientTypes['contentItemType']> | undefined {
// return processed item to avoid infinite recursion
const processedItem = data.processedItems[
codenameHelper.escapeCodenameInCodenameIndexer(data.dataToMap.item.system.codename)
Expand Down Expand Up @@ -83,7 +84,7 @@ export class ElementMapper {
private mapElement(data: {
elementWrapper: ElementModels.IElementWrapper;
item: IContentItem;
processedItems: IContentItemsContainer;
processedItems: IContentItemsContainer<TClientTypes['contentItemType']>;
processingStartedForCodenames: string[];
preparedItems: IContentItemWithRawDataContainer;
}): ElementModels.IElement<any> {
Expand Down Expand Up @@ -146,7 +147,7 @@ export class ElementMapper {

private mapRichTextElement(
elementWrapper: ElementModels.IElementWrapper,
processedItems: IContentItemsContainer,
processedItems: IContentItemsContainer<TClientTypes['contentItemType']>,
processingStartedForCodenames: string[],
preparedItems: IContentItemWithRawDataContainer
): Elements.RichTextElement {
Expand Down Expand Up @@ -336,7 +337,7 @@ export class ElementMapper {

private mapLinkedItemsElement(data: {
elementWrapper: ElementModels.IElementWrapper;
processedItems: IContentItemsContainer;
processedItems: IContentItemsContainer<TClientTypes['contentItemType']>;
processingStartedForCodenames: string[];
preparedItems: IContentItemWithRawDataContainer;
}): Elements.LinkedItemsElement<any> {
Expand Down Expand Up @@ -371,7 +372,7 @@ export class ElementMapper {
private getOrSaveLinkedItemForElement(
codename: string,
element: Contracts.IElementContract,
processedItems: IContentItemsContainer,
processedItems: IContentItemsContainer<TClientTypes['contentItemType']>,
mappingStartedForCodenames: string[],
preparedItems: IContentItemWithRawDataContainer
): IContentItem | undefined {
Expand Down
Loading