How To: Work with Data¶
This section describes the basic mechanisms for extending the application components responsible for parsing data from the backend and generating entity objects within the application.
How To: Extend Entities¶
The main entities of the application, in addition to their main properties and methods, have an important extra
property - this is an object into which you can write any custom property or new method.
For a better understanding of the entities, you can refer to the definition of classes from OOP. They can also be considered as data storage. For example, if during the process of parsing data from the backend you add new properties to the product, then they will be available wherever the product is used. Also, your new property will be written and read from the database (indexedDb) or localStorage, then the new data may be used as well in all components where an instance of the Product class is available.
How to add new information about an entity¶
To perform it, subscribe to the events of the parserSubscriber
object.
For example, we want to add two new properties to a product: isMasterProduct
and masterProductId
.
parserSubscriber.on<{
item: IProductDataWithMasterProduct,
extra: IProductExtraWithMasterId
}>('api.parse.product', async ({item: product, extra}) => {
extra['isMasterProduct'] = !parseInt(product.company_id)
extra['masterProductId'] = parseInt(product.master_product_id)
})
parserSubscriber
emits events whose names are generated based on entity names. For example, an event triggered by the parser of a product object will look like this: api.parse.product
.
parserSubscriber.on
- has a general type and, for convenience, we can specify what data we expect from the event object.
item
is a parsed json object that contains all the data as it came from the backend. Thus, if you want to get new data on the front, add it to the backend API response.
import IProductResponse from 'api/parser/cscart/Product'
export interface IProductDataWithMasterProduct extends IProductResponse {
master_product_id: string; // "0"
master_product_status: string; // "A"
master_product_offers_count: string; // "0"
}
Note
pwajet.d.ts
contains many other add-on descriptions that can be useful when writing extensions. For example, IProductResponse
is an interface that describes the standard dataset of the product object that comes in the API response.
Next, we will simply extend the base interface, indicating that the IProductDataWithMasterProduct
interface contains our new dataset: master_product_id
, master_product_status
, master_product_offers_count
. Sample data are marked with comments in the code for convenience.
IProductExtraWithMasterId
extends the interface describing the basic set of properties for the extra object of the product entity. Since we are writing an extension for the base PWAjet, the extra object is an empty object {}
. But if we were to write an extension for another extension, we would need to inherit from the extra type of the extension. In general, in our case, we can specify an interface without inheritance:
export interface IProductExtraWithMasterId {
isMasterProduct?: boolean;
masterProductId?: number;
}
or
export type IProductExtraWithMasterId = Partial<{
isMasterProduct: boolean;
masterProductId: number;
}>
Take into account that if the corresponding add-on in CS-Cart is disabled during the app’s work, we will not be able to get the necessary data. Therefore, additional data must be merged with the undefined type, then the application will be stable even when the add-ons are turned on/off.
Let’s say the data types and properties received from the API do not suit our needs, so we do the conversion:
extra['isMasterProduct'] = !parseInt(product.company_id)
extra['masterProductId'] = parseInt(product.master_product_id)
Warning
It is recommended to use your own prefixes to avoid collisions of property names in objects.
How To: Add Factories¶
Each entity, whether it is a cart or a product, is assembled by a factory. This is a function that prepares data that will be passed to the entity’s constructor, for example:
const createProduct = async (data: IFactoryProductData): Promise<Product> => {
...
return new Product(newData)
}
As a rule, complex objects have a composition from other objects, so we need calling factories of nested classes within that factory:
const createProduct = async (product: IFactoryProductData): Promise<Product> => {
const newData = ...
const options: Array<ProductOption> = await createListItems(product.options, createProductOption)
return new Product({ ...newData, options })
}
Let’s say we would like to specify a vendor for a product. How could we configure the required instance of the Vendor class in this factory?
To do this, use factoryService
- the service for registering factories:
factoryService.registerFactory ('vendor', createVendor)
, where vendor is the name of the entity for which the createVendor
function will be tried.
So, if product.extra
contains a property named vendor
, then the createVendor
function will be applied to it.
By standard, the factory file is expected to contain a named export of a function with a name createEntityName
, for example:
export const createProduct = ...
export const createVendor = ...
All factories must be asynchronous because this allows them to be loaded dynamically without overloading the application initialization with unnecessary code.
How To: Make a Class Extensible¶
Let’s say, we created our own logic for receiving the entity Vendor from the API, a factory for it, etc. How to expand this logic with other extensions?
Step 1: Add Extra to the Entity¶
So, we have a data interface for a class constructor:
export interface IVendor {
id: number;
name: string;
description: string;
}
Let’s make it common and add extra
property:
export interface IVendor<T = any> {
id: number;
name: string;
description: string;
extra: T;
}
Now we will move on to the Vendor class:
import IVendor from './IVendor'
class Vendor<T = any> {
id: number;
name: string;
description: string;
extra: T;
constructor(vendor: IVendor) {
this.id = vendor.id
this.name = vendor.name
this.description = vendor.description
this.extra = vendor.extra
}
}
export default Vendor
Step 2: Add an Event to the Parser¶
export const castVendor = async (vendor: IVendor): Promise<IVendorFactory> => {
return {
id: parseInt(vendor.company_id),
name: vendor.company,
description: vendor.description,
}
}
Add support for extending this function via parserSubscriber
:
export const castVendor = async (vendor: IVendor): Promise<IVendorFactory> => {
const extra: any = {}
await parserSubscriber.emit('api.parse.vendor', {item: vendor, extra})
return {
extra,
id: parseInt(vendor.company_id),
name: vendor.company,
description: vendor.description,
}
}
await parserSubscriber.emit
allows waiting for the application to load the necessary assets containing the data transformation logic and apply them. Therefore, all functions for parsing data from the backend must be asynchronous.
Step 3: Add Processing Extra by Registered Factories¶
export interface IVendorFactory {
id: number;
name: string;
description: string;
}
const createVendor = async (vendorData: IVendorFactory): Promise<Vendor> => {
const defaults = {
id: 0,
name: '',
description: '',
}
const vendor = {
...defaults,
...vendorData,
}
return new Vendor(vendor)
Let’s add a factoryService
to apply the necessary factories automatically.
And also in IVendorFactory
we will specify extra
:
export interface IVendorFactory {
id: number;
name: string;
description: string;
extra: Record<string, unknown>;
}
const createVendor = async (vendorData: IVendorFactory): Promise<Vendor> => {
const defaults = {
id: 0,
name: '',
description: '',
}
const vendor = {
...defaults,
...vendorData,
}
const { extra: newExtra } = await factoryService.createAll(product)
return new Vendor({
...vendor,
extra: newExtra,
})
}
That’s it! The vendor is now extensible.
Note
The example is simplified for clarity purposes. For example, the vendor lacks validation, link generation and nested entity factories.