Makaira Shop Adapters
Hello and welcome to the Makaira shop adapters. These well tested and in production used packages are bringing your new storefront even faster to production.
Table of Contents
- Intention of the Shop Adapters
- Basic usage of a shop adapter
- How to add a shop adapter to your project
- How to extend the shop adapter functionality in the storefront
-
Contributing
- Understanding the MakairaShopProviderInteractor
- How to add new shop adapter specific features methods
- How to add new feature methods for all shop adapters
- How to add new feature for all shop adapters
- How to create a new shop adapter
- How to add additional arguments to the constructor of an shop adapter
- Enforced standards
- Testing
- Good to know
- Known issues
Intention of the Shop Adapters
Makaira is offering a great e-commerce platform for the online shop of the future. We focus on bringing speed to your online shop and with our marketing and personalization focus we increase your revenue. For our team speed has two perspective:
- The speed of the online shop. That is what we're caring about for you.
- The speed of developing the storefront. That's what we even want to solve for you as a developer and as a agency by supporting you with pre-developed shop adapters.
Our goal while the development where three points:
-
Consistency
All of our costumers have different shop systems and our partner agencies have to support all of them. To keep adopting different shop systems as simple as possible we focused on developing a standardized framework. To achieve this, each adapter has the same base signature. If one of them requires additional information these can be added. In addition we also unified the response data to the basics that a storefront requires to run. But we know if a project is getting more complex it requires more data. Therefore we expose the raw api request responses.
-
Frontend Stack Independency
We love developing shop systems. But why should we force you as a developer to adopt our software stack. Therefore all of our adapters are written in javascript without any dependency to frameworks like React or Vue.js. In addition we know that fast websites requires SSR. Thats why we also made all of our adapter SSR compatible.
But to reduce your work connecting the adapter with your choice of love we added a react layer to get your storefront reactive. If you missing a connector feel free to contribute.
-
Customizability
At some point the dependency that was added to a project doesn't fit anymore the need and you have to develop it on your own. That is what we don't want for you. Our goal was to get everything that you have in mind could also be implemented. You are able to extend or overwrite each of the functionalities by defining the parts that you need without having to implement everything from scratch.
Basic usage of a shop adapter
Each of our shop adapter is divided into five sub features. For simplicity we are now using the Local Shop Adapter that is simulating a shop in the localstorage. You are free to choose another one. For detailed information of additional parameters and raw responses see the documentation for the specific shop adapter.
import { StorefrontShopAdapterLocal } from '@makaira/storefront-shop-adapter-local'
const client = new StorefrontShopAdapterLocal()
To accomplish a reactivity of the shop adapters, each of them implements the EventTarget
. By this you can register event listener to let your app react to changes in the storefront. You can attach an event listener like in the following example. The events are associated with an action that a shop adapter is possible to perform like login. The events are only triggered in a case of success.
import { UserLoginEvent } from '@makaira/storefront-types'
function setUser({ data: { data, raw } }) {}
client.addEventListener(UserLoginEvent.eventName, setUser)
The signature for each event follows the same standard. All events implement the MessageEvent
class and they have the following data
properties:
Property | Required/Optional | Description |
---|---|---|
data | required | The unified data response. This is equivalent to the data property returned by an shop adapter action. |
raw | required | The shop adapter specific api response. This is equivalent to the raw property returned by an shop adapter action. |
Basic signature of each method
Every callable method shares the same signature. Let us look at the how the user login works.
import { StorefrontShopAdapterLocal } from '@makaira/storefront-shop-adapter-local'
const client = new StorefrontShopAdapterLocal()
const { data, error, raw } = await client.user.login({
input: { username: 'foo', password: 'bar' },
})
Each method accepts one single argument that is an object. In this you pass an input property that is also an object containing the method specific parameters. In this case it is the username and the password.
Every of our methods return a standardized promise. For us it was important that error handling should not be your part. Therefore every promise resolves and never rejects. The resolved value is an object containing the following properties:
Property | Required/Optional | Description |
---|---|---|
data | optional | The data property returns in each shop adapter the same unified result for each method. By this we achieve the consistency. If an error occur data is undefined . |
error | optional | The error property indicates if an error occured. Depending on the shop adapter implementation the result differs. |
raw | optional | The raw property returns the api results as they got returned. By them you are able to extend the functionality. |
Cart
With the cart feature you can get the current cart of your customer and you can add, update and remove items from it.
Get Cart
const cart = await client.cart.getCart({ input: {} })
const { data, error, raw } = cart
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
items[] | required | An array containing the items currently in the cart. | object |
- product | required | The product in the cart. | MakairaProduct |
- quantity | required | The quantity of how much of the product is in the cart. | number |
Event Listener
Getters don't have an event listener
Add item to cart
const result = await client.cart.addItem({
input: {
id: 'foo',
quantity: 1,
attributes: [
{
key: 'foo',
value: 'bar',
},
],
},
})
const { data, error, raw } = result
Unified Input Parameters
Parameters | Required/Optional | Description | |
---|---|---|---|
product | required | The product to add to the cart. | object |
- id | required | The id of the product. For most shop adapters this is the productId . But for Shopify for example might be the variantId . |
string |
- attributes[] | optional | An optional list of attributes that should be stored with the product. Not every shop adapter supports them. | object[] |
-- key | required | The reference key to associate the value with. | string |
-- value | required | The value to be set to the key. | string |
quantity | required | The quantity of how much of the product should be added. | number |
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
items[] | required | An array containing the items currently in the cart. | object[] |
- product | required | The product in the cart. Is of type MakairaProduct . |
MakairaProduct |
- quantity | required | The quantity of how much of the product is in the cart. | number |
Event Listener
import { CartAddItemEvent } from '@makaira/storefront-types'
function setNewCart({ data: { data, raw } }) {}
client.addEventListener(CartAddItemEvent.eventName, setNewCart)
The data property is equivalent to the unified data response
Update Item from cart
const result = await client.cart.updateItem({
input: {
id: 'foo',
quantity: 2,
attributes: [
{
key: 'foo',
value: 'bar',
},
],
},
})
const { data, error, raw } = result
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|---|---|---|
product | required | The product to update from the cart. | object |
- id | required | The id of the product. For most shop adapters this is the productId . But for Shopify for example might be the variantId . |
string |
- attributes[] | optional | An optional list of attributes that should be stored with the product. Not every shop adapter supports them. | object[] |
-- key | required | The reference key to associate the value with. | string |
-- value | required | The value to be set to the key. | string |
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
items[] | required | An array containing the items currently in the cart. | object[] |
- product | required | The product in the cart. Is of type MakairaProduct . |
MakairaProduct |
- quantity | required | The quantity of how much of the product is in the cart. | number |
Event Listener
import { CartUpdateItemEvent } from '@makaira/storefront-types'
function setNewCart({ data: { data, raw } }) {}
client.addEventListener(CartUpdateItemEvent.eventName, setNewCart)
The data property is equivalent to the unified data response
Remove item from cart
const result = await client.cart.removeItem({ input: { id: 'foo' } })
const { data, error, raw } = result
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|---|---|---|
product | required | The product to remove from the cart. | object |
- id | required | The id of the product. For most shop adapters this is the productId . But for Shopify for example might be the variantId . |
string |
- attributes[] | optional | An optional list of attributes that should be stored with the product. Not every shop adapter supports them. | object[] |
-- key | required | The reference key to associate the value with. | string |
-- value | required | The value to be set to the key. | string |
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
items[] | required | An array containing the items currently in the cart. | object[] |
- product | required | The product in the cart. Is of type MakairaProduct . |
MakairaProduct |
- quantity | required | The quantity of how much of the product is in the cart. | number |
Event Listener
import { CartRemoveItemEvent } from '@makaira/storefront-types'
function setNewCart({ data: { data, raw } }) {}
client.addEventListener(CartRemoveItemEvent.eventName, setNewCart)
The data property is equivalent to the unified data response
Checkout
Currently we are working on a checkout feature for our shop adapters. Feel free to support us developing the next extension.
Review
With the review feature you can get reviews of a product and you can create new reviews.
Get Reviews
const reviews = await client.review.getReviews({
input: { product: { id: 'foo' }, pagination: { limit: 10, offset: 0 } },
})
const { data, error, raw } = reviews
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|---|---|---|
product | required | The product for what you want to receive the reviews. | object |
- id | required | The id of the product. For most shop adapters this is the productId . But for Shopify for example might be the variantId . |
string |
pagination | optional | If you only want to receive a limited number of reviews you can use the pagination property. This property will only be respected if the shop supports pagination. | object |
- limit | optional | The number of reviews to receive. | number |
- offset | optional | The starting offset from which on to receive reviews. | number |
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
items[] | required | An array containing the review items for the requested product. | object[] |
- review | required | The review for the product. Is of type MakairaReview . |
MakairaReview |
Event Listener
Getters don't have an event listener
Create Review
const result = await client.review.createReview({
input: {
review: {
rating: 2,
text: 'foo bar',
product: {
id: 'foo',
},
},
},
})
const { data, error, raw } = result
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|---|---|---|
review | required | The review to add to an product. | object |
- rating | required | The product rating for this review. Range of the rating depends on the shop adapter. | number |
- text | required | The review text. | string |
- product | required | The product with that the review is associated. | object |
-- id | required | The id of the product. For most shop adapters this is the productId . But for Shopify for example might be the variantId . |
string |
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
review | required | The newly created review for the product. Is of type MakairaReview . |
MakairaReview |
Event Listener
import { ReviewCreateEvent } from '@makaira/storefront-types'
function setNewReview({ data: { data, raw } }) {}
client.addEventListener(ReviewCreateEvent.eventName, setNewReview)
The data property is equivalent to the unified data response
User
With the user feature you can get the current signed in customer you can register, update and remove items from it.
Get Cart
const user = await client.user.getUser({ input: {} })
const { data, error, raw } = user
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
user | required | The currently signed in user. Is of type MakairaUser . |
MakairaUser |
Event Listener
Getters don't have an event listener
Login
const result = await client.user.login({
input: {
username: 'foo',
password: 'bar',
},
})
const { data, error, raw } = result
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|---|---|---|
username | required | The username of the user. Could also be an email based on the shop. | string |
password | required | The password of the user. | string |
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
user | required | The newly signed in user. Is of type MakairaUser . |
MakairaUser |
Event Listener
import { UserLoginEvent } from '@makaira/storefront-types'
function setUser({ data: { data, raw } }) {}
client.addEventListener(UserLoginEvent.eventName, setUser)
The data property is equivalent to the unified data response
Sign up
const result = await client.user.signup({
input: {
username: 'foo',
password: 'bar',
},
})
const { data, error, raw } = result
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|---|---|---|
username | required | The username of the user. Could also be an email based on the shop. | string |
password | required | The password of the user. | string |
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
user | required | The new signed up user. Is of type MakairaUser . |
MakairaUser |
Event Listener
import { UserSignupEvent } from '@makaira/storefront-types'
function setUser({ data: { data, raw } }) {}
client.addEventListener(UserSignupEvent.eventName, setUser)
The data property is equivalent to the unified data response
Forgot Password
const result = await client.user.forgotPassword({
input: {
username: 'foo',
},
})
const { data, error, raw } = result
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|---|---|---|
username | required | The username of the user. Could also be an email based on the shop. | string |
Data
Response
Unified Property | Required/Optional | Description | Type |
---|
Event Listener
Currently their is no event listener for forgot password.
Logout
const result = await client.user.logout({
input: {},
})
const { data, error, raw } = result
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|
Data
Response
Unified Property | Required/Optional | Description | Type |
---|
Event Listener
import { UserLogoutEvent } from '@makaira/storefront-types'
function setUser({ data: { data, raw } }) {}
client.addEventListener(UserLogoutEvent.eventName, setUser)
The data property is equivalent to the unified data response
Wishlist
With the wishlist feature you can get the current wishlist of your customer and you can add and remove items from it.
Get Wishlist
const wishlist = await client.wishlist.getWishlist({ input: {} })
const { data, error, raw } = wishlist
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
items[] | required | An array containing the items currently in the wishlist. | object[] |
- product | required | The product in the wishlist. Is of type MakairaProduct . |
MakairaProduct |
Event Listener
Getters don't have an event listener
Add item to wishlist
const result = await client.wishlist.addItem({
input: {
id: 'foo',
attributes: [
{
key: 'foo',
value: 'bar',
},
],
},
})
const { data, error, raw } = result
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|---|---|---|
product | required | The product to add to the wishlist. | object[] |
- id | required | The id of the product. For most shop adapters this is the productId . But for Shopify for example might be the variantId . |
string |
- attributes[] | optional | An optional list of attributes that should be stored with the product. Not every shop adapter supports them. | object[] |
-- key | required | The reference key to associate the value with. | string |
-- value | required | The value to be set to the key. | string |
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
items[] | required | An array containing the items currently in the wishlist. | object[] |
- product | required | The product in the wishlist. Is of type MakairaProduct . |
MakairaProduct |
Event Listener
import { WishlistAddItemEvent } from '@makaira/storefront-types'
function setNewWishlist({ data: { data, raw } }) {}
client.addEventListener(WishlistAddItemEvent.eventName, setNewWishlist)
The data property is equivalent to the unified data response
Remove item from wishlist
const result = await client.wishlist.removeItem({ input: { id: 'foo' } })
const { data, error, raw } = result
Unified Input Parameters
Parameters | Required/Optional | Description | Type |
---|---|---|---|
product | required | The product to remove from the wishlist. | object[] |
- id | required | The id of the product. For most shop adapters this is the productId . But for Shopify for example might be the variantId . |
string |
- attributes[] | optional | An optional list of attributes that are associated the product. Not every shop adapter supports them. | object[] |
-- key | required | The reference key to associate the value with. | string |
-- value | required | The value to be set to the key. | string |
Data
Response
Unified Property | Required/Optional | Description | Type |
---|---|---|---|
items[] | required | An array containing the items currently in the wishlist. | object[] |
- product | required | The product in the wishlist. Is of type MakairaProduct . |
MakairaProduct |
Event Listener
import { WishlistRemoveItemEvent } from '@makaira/storefront-types'
function setNewWishlist({ data: { data, raw } }) {}
client.addEventListener(WishlistRemoveItemEvent.eventName, setNewWishlist)
The data property is equivalent to the unified data response
Unified data types
MakairaUser
Parameters | Required/Optional | Description | Type |
---|---|---|---|
id | required | The id of the user. | string |
firstname | required | The first name of the user. | string |
lastname | required | The last name of the user. | string |
required | The email of the user. | string |
MakairaProduct
Parameters | Required/Optional | Description | Type |
---|---|---|---|
id | required | The id of the product. For most shop adapters this is the productId . But for Shopify for example might be the variantId . |
string |
title | required | The title of the product. | string |
url | required | The url to the product. | string |
price | required | The price of the product. | number |
images[] | required | A list of image urls associated with this product. | string[] |
attributes[] | optional | An optional list of attributes that are associated the product. Not every shop adapter supports them. | object[] |
- key | required | The reference key to associate the value with. | string |
- value | required | The value to be set to the key. | string |
MakairaReview
Parameters | Required/Optional | Description | Type |
---|---|---|---|
id | required | The id of the review. | string |
rating | required | The product rating for this review. Range of the rating depends on the shop adapter. | number |
text | required | The review text. | string |
product | required | The product with that the review is associated. | object |
- id | required | The id of the product. For most shop adapters this is the productId . But for Shopify for example might be the variantId . |
string |
How to add a shop adapter to your project
Each of our adapters share the base signature. But some of the requires more parameters like shopify requires an access token. Therefore select your adapter and follow the specific documentation how to add them.
Shop Adapter | Documentation |
---|---|
Shopify | Documentation |
Oxid | Documentation |
Plentymarkets | Documentation |
Local | Documentation |
How to extend the shop adapter functionality in the storefront
One of the main goals of the shop adapters is the customizability. This can be archived by extending our base feature classes (cart, user, e.g) where you can then overwrite or attach custom functionality to your needs.
To understand how it is working we go through an example using the local shop adapter.
The default way of creating a shop adapter looks like this.
import { StorefrontShopAdapterLocal } from '@makaira/storefront-shop-adapter-local'
const client = new StorefrontShopAdapterLocal()
To overwrite the feature (cart, user, e.g) you just need to pass the property providers
which is an optional object where you can set for each feature a custom provider. To create one you just need to extend the original provider.
import {
StorefrontShopAdapterLocalUser,
StorefrontShopAdapterLocal,
StorefrontShopAdapterLocalCart,
StorefrontShopAdapterLocalCheckout,
StorefrontShopAdapterLocalWishlist,
StorefrontShopAdapterLocalReview,
} from '@makaira/storefront-shop-adapter-local'
import {
MakairaShopProviderUser,
MakairaShopProviderInteractor,
MakairaLogout,
} from '@makaira/storefront-types'
class CustomUserProvider
extends StorefrontShopAdapterLocalUser
implements MakairaShopProviderUser
{
// This is required because super only works on prototype members.
// Look for more details here: https://basarat.gitbook.io/typescript/future-javascript/arrow-functions#tip-arrow-functions-and-inheritance
private superLogout = this.logout
constructor(mainAdapter: StorefrontShopAdapterLocal) {
super(mainAdapter)
}
// We recommend you to use our base signature for each method.
// When you use typescript you can safely archive this:
setUserDisplayName: MakairaShopProviderInteractor<
{ inputVariable1: string },
{ returnedVariable1: string },
{ returnedRawVariable1: string },
Error
> = async ({ input: { inputVariable1 } }) => {
return {
raw: { returnedRawVariable1: inputVariable1 },
data: { returnedVariable1: inputVariable1 },
error: undefined,
}
// or with error
return {
raw: { returnedRawVariable1: inputVariable1 },
data: undefined,
error: new Error('some error'),
}
}
// if you don't prefer using our base method signature define it yourself
setUserDisplayName(inputVariable1: string) {
return inputVariable1
}
// you can even overwrite existing methods
logout: MakairaLogout<
{ additionalInput1: string },
{ customRawData1: string }
> = async ({ input: { additionalInput1 } }) => {
// The super only works on prototype members. Therefore we need to create a copy.
// Look for more details here: https://basarat.gitbook.io/typescript/future-javascript/arrow-functions#tip-arrow-functions-and-inheritance
const response = await this.superLogout({ input: {} })
return {
data: response.data,
raw: { customRawData1: additionalInput1 },
error: response.error,
}
}
}
const client = new StorefrontShopAdapterLocal<
StorefrontShopAdapterLocalCart,
StorefrontShopAdapterLocalCheckout,
CustomUserProvider,
StorefrontShopAdapterLocalWishlist,
StorefrontShopAdapterLocalReview
>({
providers: {
user: CustomUserProvider,
},
})
Contributing
In this section we focus on how to contribute to the shop adapter packages. You can have four choices what you can contribute to:
- Add new shop adapter specific feature methods. By this you add to a feature (like the user feature) a new method that does not exists currently and is only specific for this single shop adapter (like a customerReset).
- Add new feature methods for all shop adapters. By this you add a feature method that every shop adapter has to support like forgotPassword.
- Add new feature for all shop adapters. By this you add a feature that every shop adapter has to support like the user feature.
MakairaShopProviderInteractor
Understanding the The most important part is that each feature method has to implement the MakairaShopProviderInteractor
interface. The interface is defined as:
export type MakairaResponse<ResData, ResRawData, ResError extends Error> = {
data?: ResData // The response data property which the feature method returns. Can be undefined in a case of error or when no data has to be returned.
raw: ResRawData // the response raw property which the feature method returns. Is by required but you can set it to undefined if you wish.
error?: ResError // The type of error that could be returned. Currently each feature method uses an instance of Error.
}
export type MakairaShopProviderInteractorContext<Input> = {
input: Input // The input data which the feature method receives.
}
export type MakairaShopProviderInteractor<
Input = unknown, // The input data which the feature method receives.
ResData = unknown, // The response data property which the feature method returns. Can be undefined in a case of error or when no data has to be returned.
ResRawData = unknown, // the response raw property which the feature method returns. Is by required but you can set it to undefined if you wish.
ResError extends Error = Error // The type of error that could be returned. Currently each feature method uses an instance of Error.
> = (
context: MakairaShopProviderInteractorContext<Input>
) => Promise<MakairaResponse<ResData, ResRawData, ResError>>
Sometimes the generic type definition of the MakairaShopProviderInteractor
can be a little bit complicated to understand. Therefore here are some common use cases how to use them:
// Defining a feature method without input arguments.
export class StorefrontShopAdapterLocalUser implements MakairaShopProviderUser {
foo: MakairaShopProviderInteractor<
unknown, // To disable any input arguments set here to unknown
{ dataResponseVariable1: string },
{ rawResponseVariable1: string },
Error
> = async () => {}
}
// Defining a feature method without return data or raw response.
export class StorefrontShopAdapterLocalUser implements MakairaShopProviderUser {
foo: MakairaShopProviderInteractor<
{inputVariable1:string}
undefined, // Set this to undefined to have a response data property without data. The data and the raw property can be set independently to undefined.
undefined, // Set this to undefined to have a response raw property without data. The data and the raw property can be set independently to undefined.
Error
> = async () => {}
}
// Adjust the returned type of the error.
export class StorefrontShopAdapterLocalUser implements MakairaShopProviderUser {
foo: MakairaShopProviderInteractor<
unknown,
{ dataResponseVariable1: string },
{ rawResponseVariable1: string },
NotImplementedError // Adjust this to set a custom error type.
> = async () => {}
}
// You can create a new type of it that is again generic. This is used when you add new feature methods for all shop adapters. Here is the example of the login
export type MakairaLoginInput<AdditionalInput = unknown> = {
username: string
password: string
} & AdditionalInput
export type MakairaLoginResData = {
user: MakairaUser
}
export type MakairaLogin<
AdditionalInput = any,
ResRawData = any,
ResError extends Error = Error
> = MakairaShopProviderInteractor<
MakairaLoginInput<AdditionalInput>,
MakairaLoginResData,
ResRawData,
ResError
>
export type LocalLoginRaw = { rawResponseVariable1: string }
export class StorefrontShopAdapterLocalUser implements MakairaShopProviderUser {
login: MakairaLogin<unknown, LocalLoginRaw, Error> = async ({
input: { inputVariable1 },
}) => {
if (inputVariable1 !== 'foo') {
return {
data: undefined,
raw: { rawResponseVariable1: inputVariable1 },
error: new Error('inputVariable1 does not match foot'),
}
}
return {
data: { dataResponseVariable1: inputVariable1 },
raw: { rawResponseVariable1: inputVariable1 },
error: undefined,
}
}
}
How to add new shop adapter specific features methods
To add new shop adapter specific feature method you just need to add the method to the class. In the following we add a feature method foo to the user feature of the local shop adapter.
Like described in this section the feature method must implement the MakairaShopProviderInteractor
. It has also to follow the standard.
export class StorefrontShopAdapterLocalUser implements MakairaShopProviderUser {
foo: MakairaShopProviderInteractor<
{ inputVariable1: string },
{ dataResponseVariable1: string },
{ rawResponseVariable1: string },
Error
> = async ({ input: { inputVariable1 } }) => {
if (inputVariable1 !== 'foo') {
return {
data: undefined,
raw: { rawResponseVariable1: inputVariable1 },
error: new Error('inputVariable1 does not match foot'),
}
}
return {
data: { dataResponseVariable1: inputVariable1 },
raw: { rawResponseVariable1: inputVariable1 },
error: undefined,
}
}
}
How to add new feature methods for all shop adapters
To add a new feature method to all shop adapters we have to touch all shop adapter and the main type definition.
At first we modify the @makaira/storefront-types
package located in packages/storefront-types
.
Choose the feature where you want to add a new feature method. Then go into the file packages/storefront-types/src/providers/<FEATURE>.ts
.
As an example we create an deleteUser feature method in the user feature
First we have to create the unified signature definition. By this we enforce each shop provider to implement the same base usage to archive our goal of consistency.
//#region type definition: deleteUser
// To have a better separation and a better reading we first create the input type. These are the input arguments that each shop adapter has to support. By the generic AdditionalInput it is possible to add more shop provider specific input arguments.
export type MakairaDeleteUserInput<AdditionalInput = unknown> = {
username: string
} & AdditionalInput
// As an alternative where you don't need any basic input arguments you can also write the following:
export type MakairaDeleteUserInput<AdditionalInput = unknown> = AdditionalInput
// Next we define the unified response data. In the case of deleteUser we don't have any response data.
export type MakairaDeleteUserResData = undefined
// If you have any response data you can write the following:
export type MakairaDeleteUserResData = { resDataVariable1: boolean }
// And finally we have to combine everything using the MakairaShopProviderInteractor into a new generic type. This new type is again generic to allow each shop adapter to define it's own additional input arguments and their custom response raw data.
export type MakairaDeleteUser<
AdditionalInput = any,
ResRawData = any,
ResError extends Error = Error
> = MakairaShopProviderInteractor<
MakairaDeleteUserInput<AdditionalInput>,
MakairaDeleteUserResData,
ResRawData,
ResError
>
//#endregion
After we have added our unified signature for deleteUser we now have to add it to the definition of the feature methods. At the end of each type definition feature file (packages/storefront-types/src/providers/<FEATURE>.ts
) is the definition. Add to this the following:
export type MakairaShopProviderUser = {
login: MakairaLogin
// ...
// ...
deleteUser: MakairaDeleteUser
}
After you added this automatically the typescript definition checkup will fail on each commit until you don't added the shop adapter specific implementation. This is what we will add now. Therefore go into each shop adapter feature and add the implementation. The file is located at packages/<SHOP-ADAPTER>/src/providers/<FEATURE>.ts
export class StorefrontShopAdapterLocalUser implements MakairaShopProviderUser {
constructor(private mainAdapter: StorefrontShopAdapterLocal) {}
// Example 1: base usage without additional input variables and without raw data
deleteUser: MakairaDeleteUser<unknown, undefined, Error> = async ({
input: {},
}) => {
// your specific implementation ...
}
// Example 2: base usage with additional input variables and without raw data
deleteUser: MakairaDeleteUser<{ confirm: boolean }, undefined, Error> =
async ({ input: { confirm } }) => {
// your specific implementation ...
}
// Example 3: base usage with additional input variables and with raw data
deleteUser: MakairaDeleteUser<
{ confirm: boolean },
{ successful: boolean },
Error
> = async ({ input: { confirm } }) => {
// your specific implementation ...
}
}
We recommend to not inline the types for AdditionalInput and RawResData. Outsource them into the types file packages/<SHOP-ADAPTER>/src/types.ts
How to add new feature for all shop adapters
Adding a complete new feature is a little bit more work to be done.
In the following we add a new feature called order to all shop adapters.
At first we have to create the unified type definition for our new order feature. To do so create the file src/providers/order.ts
in @makaira/storefront-types
. Like in the section how to add new feature for all shop adapters we create the unified type definition.
import { MakairaShopProviderInteractor } from '../general/shop-provider-interactor'
//#region type definition: getOrders
export type MakairaGetOrdersInput<AdditionalInput = unknown> = {
user: { id: string }
} & AdditionalInput
export type MakairaGetOrdersResData = {
orders: { id: string; products: MakairaProduct[] }
}
export type MakairaGetOrders<
AdditionalInput = any,
ResRawData = any,
ResError extends Error = Error
> = MakairaShopProviderInteractor<
MakairaGetOrdersInput<AdditionalInput>,
MakairaGetOrdersResData,
ResRawData,
ResError
>
//#endregion
//#region type definition: provider order
export type MakairaShopProviderOrder = {
getOrders: MakairaGetOrders
}
Next you have to add the feature to the main interface that is located in the src/providers/main.ts
in @makaira/storefront-types
. We extend our generic types MakairaShopProviderOptions
and MakairaShopProvider
to accept the new feature. This leads to a BREAKING CHANGE, since AdditionalOptions should stay at the last position of the generic types.
export type MakairaShopProviderOptions<
CartProviderType = MakairaShopProviderCart,
// ... other features/providers defined
// ...
OrderProviderType = MakairaShopProviderOrder, // <- add this here
AdditionalOptions = unknown
> = {
providers?: {
cart?: Constructor<CartProviderType>
// ... other features/providers defined
// ...
order?: Constructor<OrderProviderType> // <- add this here
}
} & AdditionalOptions
export interface MakairaShopProvider<
CartProviderType extends MakairaShopProviderCart = MakairaShopProviderCart,
// ... other features/providers defined
// ...
OrderProviderType extends MakairaShopProviderOrder = MakairaShopProviderOrder // <- add this here
> extends EventTarget {
cart: CartProviderType
// ... other features/providers defined
// ...
order: OrderProviderType // <- add this here
}
In the following we get for each shop adapter a typescript error that we now fix by adding the new order feature specific implementation to each shop adapter.
Therefore create in each shop adapter the file src/providers/order.ts
. For simplicity we don't show how to add the enforced feature methods here. You can read about it in the section how to add new feature for all shop adapters.
import { StorefrontShopAdapterLocal } from './main'
export class StorefrontShopAdapterLocalOrder
implements MakairaShopProviderOrder
{
constructor(private mainAdapter: StorefrontShopAdapterLocal) {}
// ... shop adapter specific feature method implementations
}
At the last step we have to adjust our main.ts
file in each shop adapter that is located at src/providers/main.ts
. Their we have to adjust the generics and have to initialize the new feature.
export class StorefrontShopAdapterLocal<
CartProviderType extends MakairaShopProviderCart = StorefrontShopAdapterLocalCart,
// ... other features/providers defined
// ...
OrderProviderType extends MakairaShopProviderOrder = StorefrontShopAdapterLocalOrder // <- add this here
>
extends EventTarget
implements
MakairaShopProvider<
CartProviderType,
// ... other features/providers defined
// ...
OrderProviderType // <- add this here
>
{
cart: CartProviderType
// ... other features/providers defined
// ...
order: OrderProviderType // <- add this here
constructor(
options: MakairaShopProviderOptions<
CartProviderType,
// ... other features/providers defined
// ...
OrderProviderType // <- add this here
> = {}
) {
super()
// Here are all other providers to destructed.
// Because of our goal to make everything customizable as much as possible we assign here a default value instead of directly initializing our new created feature. So in the future feature overwriting by passing a custom provider is possible.
const {
cart: CartProvider = StorefrontShopAdapterLocalCart,
// ... other features/providers defined
// ...
order: OrderProvider = StorefrontShopAdapterLocalOrder, // <- add this here
} = options.providers ?? {}
// @ts-expect-error https://stackoverflow.com/questions/56505560/how-to-fix-ts2322-could-be-instantiated-with-a-different-subtype-of-constraint
this.cart = new CartProvider(this)
// ... other features/providers defined
// ...
// @ts-expect-error https://stackoverflow.com/questions/56505560/how-to-fix-ts2322-could-be-instantiated-with-a-different-subtype-of-constraint
this.order = new OrderProvider(this) // <- add this here
}
}
Finally we added our new feature order.
How to create a new shop adapter
Creating a new shop adapter is beside its specific implementation really simple. To do so we created a script that can be run from the root of this repository to create a new shop adapter. Just run the following command:
npm run create-new-shop-provider <NEW-SHOP-ADAPTER-NAME>
or
yarn create-new-shop-provider <NEW-SHOP-ADAPTER-NAME>
This will create a new folder in the packages folder with the naming convention storefront-shop-adapter-<NEW-SHOP-ADAPTER-NAME>
.
The script automatically set up for you:
- creating an npm package that will be published under:
@makaira/storefront-shop-adapter-<NEW-SHOP-ADAPTER-NAME>
- versioning the package
- validating the typescript configuration on each commit
- validating the code quality using eslint on each commit
- creating empty main adapter and feature classes
- build command
After you run the create command you have to implement the feature methods. If you finished it you can commit it and create a PR.
How to add additional arguments to the constructor of an shop adapter
Sometimes a shop adapter needs some arguments to work. For example most adapters require a api url. We know that and this is why we added an option for this.
To add additional arguments to have to adjust the type definition of the main shop adapter class. The last argument of the generic type MakairaShopProviderOptions
allows you to add a type definition for your additional arguments.
class StorefrontShopAdapterDemo {
constructor(
options: MakairaShopProviderOptions<
CartProviderType,
CheckoutProviderType,
UserProviderType,
WishlistProviderType,
ReviewProviderType,
{ apiUrl: string } // <-- add this here
>
) {
super()
// custom code
// ...
}
}
Enforced standards
To archive a high standard and consistency for the monorepo we added some checks to ensure them. Currently on each commit theses standards are enforced. If their is any problem the commit will fail and reports you what has to be adjusted. The checks are:
- each commit message must follow the conventional commits
- on each commit the type safety will be validated. If their is a problem the commit will fail.
- on each commit the code quality will be checked using eslint. If their is any problem the commit will fail.
- on each commit the formatting is adjusted to the standards using prettier.
Testing
This monorepo allows you to run testing manually or automated.
Manual testing
To run testing manually you can link your local npm package with your storefront or any other package that depends on one of the packages. To test it you have to do two steps:
- Install the package using a relative path. Just edit the
package.json
and find the package you are editing and testing and replace the version with the path to the file. For example it looks like this:
{
"dependencies": {
"@makaira/storefront-shop-adapter-oxid": "file:../../storefront-shop-adapter/packages/storefront-shop-adapter-oxid"
}
}
- On each change you made to the shop adapter you have to rebuild it. Just go into the shop adapter package folder and run
npm run build
. Feel free to add an implementation to run it in a watch mode to automatically rebuild on file changes.
Automatic testing
The hole monorepo supports automated testing using jest tests. Feel free to add new jest test. They will be automatically detected. To run the test execute the command npm run test
on the root of the monorepo or in each package.
Good to know
-
When you are locally developing each package accesses the current implementation of code instead of the builded code. So if you for example add a new feature method for all shop adapters each of them will directly create a typescript error because they access the newly added code. It means that the version that stands in the dependencies does not matter.
-
The release of the packages is done automatically in the ci. To ensure that each package has the correct referenced version between them at first the
@makaira/storefront-types
will be published and then each package that requires@makaira/storefront-types
will get the newly released version set. -
The versions for each package will automatically calculated using
semantic-release
in the ci. So you don't have to worry about setting the correct versions. -
The monorepo orchestration is done by
turborepo
Known issues
- In some cases after a commit files seems to be changed. This comes by prettier we think. Prettier sets another file permissions that will be recorded by git. Just discard these changes.