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

Merged
merged 13 commits into from
Mar 10, 2025
Merged
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## [16.0.0-0](https://github.com/kontent-ai/delivery-sdk-js/compare/v15.2.0...v16.0.0-0) (2025-02-13)


### Features

* adds support for client generics, improving type safety in combination with model generator ([d45a0e3](https://github.com/kontent-ai/delivery-sdk-js/commit/d45a0e385e3ddc2b88805ef7c69725c5fdf84f18))
* removes url-parse package in favor of native URL class ([34bfe8f](https://github.com/kontent-ai/delivery-sdk-js/commit/34bfe8f93df1f801948f4f948f6de0671d4cc524))
* updates deps ([6335bba](https://github.com/kontent-ai/delivery-sdk-js/commit/6335bbaa49e92a75ae34bfda22844c217b36d31a))


### Bug Fixes

* package.json & package-lock.json to reduce vulnerabilities ([da3b536](https://github.com/kontent-ai/delivery-sdk-js/commit/da3b536e4b1c99344f3ecb7135257136f07c587c))

## [15.2.0](https://github.com/kontent-ai/delivery-sdk-js/compare/v15.1.1...v15.2.0) (2024-09-27)


Expand Down
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>;
}
28 changes: 14 additions & 14 deletions lib/mappers/element.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,18 @@ interface IRichTextImageUrlRecord {
newUrl: string;
}

export class ElementMapper {
export class ElementMapper<TContentItemType extends IContentItem> {
constructor(private readonly config: IDeliveryClientConfig) {}

mapElements<TContentItem extends IContentItem = IContentItem>(data: {
mapElements<TContentItem extends TContentItemType = TContentItemType>(data: {
dataToMap: IContentItemWithRawElements;
processedItems: IContentItemsContainer;
processedItems: IContentItemsContainer<TContentItem>;
processingStartedForCodenames: string[];
preparedItems: IContentItemWithRawDataContainer;
}): IMapElementsResult<TContentItem> | undefined {
}): IMapElementsResult<TContentItem, TContentItemType> | undefined {
// return processed item to avoid infinite recursion
const processedItem = data.processedItems[
codenameHelper.escapeCodenameInCodenameIndexer(data.dataToMap.item.system.codename)
] as TContentItem | undefined;
const processedItem =
data.processedItems[codenameHelper.escapeCodenameInCodenameIndexer(data.dataToMap.item.system.codename)];
if (processedItem) {
// item was already resolved
return {
Expand All @@ -43,7 +42,8 @@ export class ElementMapper {

const preparedItem =
data.preparedItems[codenameHelper.escapeCodenameInCodenameIndexer(data.dataToMap.item.system.codename)];
const itemInstance = preparedItem?.item as TContentItem;

const itemInstance = preparedItem?.item;

if (!itemInstance) {
// item is not present in response
Expand Down Expand Up @@ -73,7 +73,7 @@ export class ElementMapper {
}

return {
item: itemInstance,
item: itemInstance as TContentItem,
processedItems: data.processedItems,
preparedItems: data.preparedItems,
processingStartedForCodenames: data.processingStartedForCodenames
Expand All @@ -83,7 +83,7 @@ export class ElementMapper {
private mapElement(data: {
elementWrapper: ElementModels.IElementWrapper;
item: IContentItem;
processedItems: IContentItemsContainer;
processedItems: IContentItemsContainer<TContentItemType>;
processingStartedForCodenames: string[];
preparedItems: IContentItemWithRawDataContainer;
}): ElementModels.IElement<any> {
Expand Down Expand Up @@ -146,7 +146,7 @@ export class ElementMapper {

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

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

let mappedLinkedItem: IContentItem | undefined;
let mappedLinkedItem: TContentItemType | undefined;

// original resolving if item is still undefined
const mappedLinkedItemResult = this.mapElements({
Expand Down
Loading