Skip to content

Commit a601cf7

Browse files
committed
feat(js-api-client): cart api and TSUP updates
1 parent cf92180 commit a601cf7

File tree

6 files changed

+167
-21
lines changed

6 files changed

+167
-21
lines changed

README.md

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ You get access to different helpers for each API:
4848
- orderApi
4949
- subscriptionApi
5050
- pimApi
51+
- pimNextApi
52+
- shopCartApi
5153

5254
First, you need to create the _Client_:
5355

@@ -63,14 +65,64 @@ Then you can use it:
6365

6466
```typescript
6567
export async function fetchSomething(): Promise<Something[]> {
66-
const caller = CrystallizeClient.catalogueApi;
67-
const response = await caller(graphQLQuery, variables);
68+
const response = await CrystallizeClient.catalogueApi(graphQLQuery, variables);
6869
return response.catalogue;
6970
}
7071
```
7172

7273
There is a live demo: https://crystallizeapi.github.io/libraries/js-api-client/call-api
7374

75+
When it comes to API that requires Authentication you can provide more to _createClient_.
76+
77+
```javascript
78+
const pimApiClient = createClient({
79+
tenantIdentifier: 'furniture',
80+
accessTokenId: 'xxx',
81+
accessTokenSecret: 'xxx',
82+
};
83+
await pimApiClient.pimApi(query)
84+
85+
const catalogueApiClient = createClient({
86+
tenantIdentifier: 'furniture',
87+
staticAuthToken: 'xxx'
88+
};
89+
await catalogueApiClient.catalogueApi(query)
90+
```
91+
92+
There is even more about the Shop Cart API that requires a specific token.
93+
If you fetched the Token yourself you can pass it directly and enjoy the Shop Cart API
94+
95+
```javascript
96+
const cartApiClient = createClient({
97+
tenantIdentifier: 'furniture',
98+
shopApiToken: 'xxx',
99+
});
100+
await cartApiClient.shopCartApi(query);
101+
```
102+
103+
But you can let the JS API Client do the heavy-lifting for you. Shop Cart API requires proof of access to PIM in order to get such Token.
104+
Based on your situation, most likely you are using Shop Cart API server-side:
105+
106+
```javascript
107+
const cartApiClient = createClient(
108+
{
109+
tenantIdentifier: 'furniture',
110+
accessTokenId: 'xxx',
111+
accessTokenSecret: 'xxx',
112+
},
113+
{
114+
// optional
115+
shopApiToken: {
116+
expiresIn: 900000, // optional, default 12 hours
117+
scopes: ['cart', 'cart:admin', 'usage'], // optional, default ['cart']
118+
},
119+
},
120+
);
121+
await cartApiClient.shopCartApi(query);
122+
```
123+
124+
JS API Client will grab the Token for you (once) and use it within the following calls to Shop Cart API.
125+
74126
## Catalogue Fetcher
75127
76128
You can pass objects that respect the logic of https://www.npmjs.com/package/json-to-graphql-query to the Client.

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
22
"name": "@crystallize/js-api-client",
33
"license": "MIT",
4-
"version": "1.13.1",
4+
"version": "2.0.0",
55
"author": "Crystallize <[email protected]> (https://crystallize.com)",
66
"contributors": [
77
"Sébastien Morel <[email protected]>",
88
"Dhairya Dwivedi <[email protected]>"
99
],
1010
"scripts": {
1111
"watch": "yarn tsc -W",
12-
"build": "yarn tsc",
12+
"build": "tsup src/index.ts --format esm,cjs --dts",
1313
"test": "jest",
1414
"test:watch": "jest --watch",
1515
"bump": "yarn tsc && yarn version --no-git-tag-version --new-version"
@@ -18,16 +18,18 @@
1818
"type": "git",
1919
"url": "https://github.com/CrystallizeAPI/js-api-client.git"
2020
},
21-
"main": "dist/index.js",
22-
"types": "dist/index.d.ts",
21+
"types": "./dist/index.d.ts",
22+
"main": "./dist/index.js",
23+
"module": "./dist/index.mjs",
2324
"devDependencies": {
2425
"@tsconfig/node16": "^16.1.1",
2526
"@types/node": "^20.10.4",
27+
"tsup": "^8.0.1",
2628
"@types/node-fetch": "^2",
2729
"jest": "^29.7.0"
2830
},
2931
"dependencies": {
30-
"dotenv": "^16.3.1",
32+
"dotenv": "^16.4.1",
3133
"json-to-graphql-query": "^2.2.5",
3234
"mime-lite": "^1.0.3",
3335
"node-fetch": "^2",

src/core/client.ts

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type ClientConfiguration = {
77
accessTokenSecret?: string;
88
staticAuthToken?: string;
99
sessionId?: string;
10+
shopApiToken?: string;
1011
origin?: string;
1112
};
1213

@@ -27,6 +28,11 @@ type ProfilingOptions = {
2728

2829
export type CreateClientOptions = {
2930
profiling?: ProfilingOptions;
31+
shopApiToken?: {
32+
doNotFetch?: boolean;
33+
scopes?: string[];
34+
expiresIn?: number;
35+
};
3036
};
3137

3238
export type VariablesType = Record<string, any>;
@@ -38,10 +44,12 @@ export type ClientInterface = {
3844
orderApi: ApiCaller<any>;
3945
subscriptionApi: ApiCaller<any>;
4046
pimApi: ApiCaller<any>;
47+
nextPimApi: ApiCaller<any>;
48+
shopCartApi: ApiCaller<any>;
4149
config: Pick<ClientConfiguration, 'tenantIdentifier' | 'tenantId' | 'origin'>;
4250
};
4351

44-
function authenticationHeaders(config: ClientConfiguration) {
52+
function authenticationHeaders(config: ClientConfiguration): Record<string, string> {
4553
if (config.sessionId) {
4654
return {
4755
Cookie: 'connect.sid=' + config.sessionId,
@@ -67,14 +75,15 @@ async function post<T>(
6775
profiling?: ProfilingOptions,
6876
): Promise<T> {
6977
try {
70-
const commonHeaders = {
78+
const { headers: initHeaders, ...initRest } = init || {};
79+
80+
const headers = {
7181
'Content-type': 'application/json; charset=UTF-8',
7282
Accept: 'application/json',
73-
};
74-
const headers = {
75-
...commonHeaders,
7683
...authenticationHeaders(config),
84+
...initHeaders,
7785
};
86+
7887
const body = JSON.stringify({ query, variables });
7988
let start: number = 0;
8089
if (profiling) {
@@ -85,7 +94,7 @@ async function post<T>(
8594
}
8695

8796
const response = await fetch(path, {
88-
...init,
97+
...initRest,
8998
method: 'POST',
9099
headers,
91100
body,
@@ -133,6 +142,12 @@ async function post<T>(
133142
}
134143
}
135144

145+
function apiHost(configuration: ClientConfiguration) {
146+
const origin = configuration.origin || '.crystallize.com';
147+
return (path: string[], prefix: 'api' | 'pim' | 'shop-api' = 'api') =>
148+
`https://${prefix}${origin}/${path.join('/')}`;
149+
}
150+
136151
function createApiCaller(
137152
uri: string,
138153
configuration: ClientConfiguration,
@@ -143,16 +158,58 @@ function createApiCaller(
143158
};
144159
}
145160

161+
function shopApiCaller(configuration: ClientConfiguration, options?: CreateClientOptions) {
162+
const identifier = configuration.tenantIdentifier;
163+
let shopApiToken = configuration.shopApiToken;
164+
return async function callApi<T>(query: string, variables?: VariablesType): Promise<T> {
165+
if (!shopApiToken && options?.shopApiToken?.doNotFetch !== true) {
166+
const headers = {
167+
'Content-type': 'application/json; charset=UTF-8',
168+
Accept: 'application/json',
169+
...authenticationHeaders(configuration),
170+
};
171+
const response = await fetch(apiHost(configuration)([`@${identifier}`, 'auth', 'token'], 'shop-api'), {
172+
method: 'POST',
173+
headers,
174+
body: JSON.stringify({
175+
scopes: options?.shopApiToken?.scopes || ['cart'],
176+
expiresIn: options?.shopApiToken?.expiresIn || 3600 * 12,
177+
}),
178+
});
179+
const results = await response.json();
180+
if (results.success !== true) {
181+
throw new Error('Could not fetch shop api token: ' + results.error);
182+
}
183+
shopApiToken = results.token;
184+
}
185+
return post<T>(
186+
apiHost(configuration)([`@${identifier}`, 'cart'], 'shop-api'),
187+
{
188+
...configuration,
189+
shopApiToken: shopApiToken,
190+
},
191+
query,
192+
variables,
193+
{
194+
headers: {
195+
Authorization: `Bearer ${shopApiToken}`,
196+
},
197+
},
198+
options?.profiling,
199+
);
200+
};
201+
}
202+
146203
export function createClient(configuration: ClientConfiguration, options?: CreateClientOptions): ClientInterface {
147204
const identifier = configuration.tenantIdentifier;
148-
const origin = configuration.origin || '.crystallize.com';
149-
const apiHost = (path: string[], prefix: 'api' | 'pim' = 'api') => `https://${prefix}${origin}/${path.join('/')}`;
150205
return {
151-
catalogueApi: createApiCaller(apiHost([identifier, 'catalogue']), configuration, options),
152-
searchApi: createApiCaller(apiHost([identifier, 'search']), configuration, options),
153-
orderApi: createApiCaller(apiHost([identifier, 'orders']), configuration, options),
154-
subscriptionApi: createApiCaller(apiHost([identifier, 'subscriptions']), configuration, options),
155-
pimApi: createApiCaller(apiHost(['graphql'], 'pim'), configuration, options),
206+
catalogueApi: createApiCaller(apiHost(configuration)([identifier, 'catalogue']), configuration, options),
207+
searchApi: createApiCaller(apiHost(configuration)([identifier, 'search']), configuration, options),
208+
orderApi: createApiCaller(apiHost(configuration)([identifier, 'orders']), configuration, options),
209+
subscriptionApi: createApiCaller(apiHost(configuration)([identifier, 'subscriptions']), configuration, options),
210+
pimApi: createApiCaller(apiHost(configuration)(['graphql'], 'pim'), configuration, options),
211+
nextPimApi: createApiCaller(apiHost(configuration)([`@${identifier}`]), configuration, options),
212+
shopCartApi: shopApiCaller(configuration, options),
156213
config: {
157214
tenantId: configuration.tenantId,
158215
tenantIdentifier: configuration.tenantIdentifier,

src/core/massCallClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ export function createMassCallClient(
189189
orderApi: client.orderApi,
190190
subscriptionApi: client.subscriptionApi,
191191
pimApi: client.pimApi,
192+
shopCartApi: client.shopCartApi,
193+
nextPimApi: client.nextPimApi,
192194
config: client.config,
193195
enqueue: {
194196
catalogueApi: (query: string, variables?: VariablesType): string => {

tests/call.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,36 @@ test('callSearchApi: Raw fetch Skus', async () => {
100100

101101
expect(response.search.edges[0].node.path).toBe('/shop/bathroom-fitting/large-mounted-cabinet-in-treated-wood');
102102
});
103+
104+
test.skip('Shop API Cart: Test we can call it', async () => {
105+
const CrystallizeClient = createClient(
106+
{
107+
tenantIdentifier: 'frntr',
108+
accessTokenId: 'xxx',
109+
accessTokenSecret: 'xxx',
110+
// shopApiToken: 'xxx'
111+
},
112+
{
113+
shopApiToken: {
114+
// doNotFetch: false,
115+
// expiresIn: 900000,
116+
// scopes: ['cart', 'cart:admin', 'usage']
117+
},
118+
},
119+
);
120+
const caller = CrystallizeClient.shopCartApi;
121+
const query = `mutation HYDRATE ($sku: String!) {
122+
hydrate(input: { items: [ { sku: $sku } ] }) {
123+
id
124+
total {
125+
gross
126+
}
127+
items {
128+
name
129+
}
130+
}}`;
131+
const response = await caller(query, {
132+
sku: 'smeg-robot-pink-standard',
133+
});
134+
expect(response.hydrate.id).toBeDefined();
135+
});

tests/catalogue.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { catalogueFetcherGraphqlBuilder } = require('../dist/core/catalogue.js');
1+
const { catalogueFetcherGraphqlBuilder } = require('../dist/index.js');
22
const { jsonToGraphQLQuery } = require('json-to-graphql-query');
33

44
test('Catalogue Query Builder Test', () => {

0 commit comments

Comments
 (0)