Skip to content

Commit

Permalink
feat: Create the Medusa API SDK as js-sdk package
Browse files Browse the repository at this point in the history
  • Loading branch information
sradevski committed May 8, 2024
1 parent c71a06c commit 69f3446
Show file tree
Hide file tree
Showing 12 changed files with 560 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/weak-cherries-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/js-sdk": patch
---

Introduce a js-sdk package for the Medusa API
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ module.exports = {
"./packages/core/orchestration/tsconfig.json",
"./packages/core/workflows-sdk/tsconfig.spec.json",
"./packages/core/modules-sdk/tsconfig.spec.json",
"./packages/core/js-sdk/tsconfig.spec.json",
"./packages/core/types/tsconfig.spec.json",
"./packages/core/utils/tsconfig.spec.json",
"./packages/core/medusa-test-utils/tsconfig.spec.json",
Expand Down
Empty file.
13 changes: 13 additions & 0 deletions packages/core/js-sdk/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
transform: {
"^.+\\.[jt]s?$": [
"ts-jest",
{
tsconfig: "tsconfig.json",
isolatedModules: true,
},
],
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
}
40 changes: 40 additions & 0 deletions packages/core/js-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@medusajs/js-sdk",
"version": "0.0.1",
"description": "SDK for the Medusa API",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/core/js-sdk"
},
"engines": {
"node": ">=16"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"author": "Medusa",
"license": "MIT",
"devDependencies": {
"cross-env": "^5.2.1",
"jest": "^29.6.3",
"rimraf": "^5.0.1",
"ts-jest": "^29.1.1",
"typescript": "^5.1.6"
},
"dependencies": {
"@medusajs/types": "^1.11.16",
"@medusajs/utils": "^1.11.9",
"qs": "^6.12.1"
},
"scripts": {
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
"build": "rimraf dist && tsc --build",
"test": "jest --passWithNoTests --runInBand --bail --forceExit",
"watch": "tsc --build --watch"
}
}
8 changes: 8 additions & 0 deletions packages/core/js-sdk/src/admin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Client } from "../client"

export class Admin {
private client: Client
constructor(client: Client) {
this.client = client
}
}
137 changes: 137 additions & 0 deletions packages/core/js-sdk/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import qs from "qs"
import { ClientFetch, Config, FetchParams, Logger } from "./types"

const isBrowser = () => typeof window !== "undefined"

const toBase64 = (str: string) => {
if (typeof window !== "undefined") {
return window.btoa(str)
}

return Buffer.from(str).toString("base64")
}

const sanitizeHeaders = (headers: any) => {
return { ...headers, Authorization: "<REDACTED>" }
}

// TODO: Add support for retries and timeouts
export class Client {
public fetch: ClientFetch
private logger: Logger

private DEFAULT_JWT_STORAGE_KEY = "medusa_auth_token"
private token = ""

constructor(config: Config) {
const logger = config.logger || {
error: console.error,
warn: console.warn,
info: console.info,
debug: console.debug,
}

this.logger = {
...logger,
debug: config.debug ? logger.debug : () => {},
}

this.fetch = this.initClient(config)
}

protected initClient(config: Config): ClientFetch {
const defaultHeaders = {
"Content-Type": "application/json",
Accept: "application/json",
...this.getApiKeyHeader(config),
...this.getPublishableKeyHeader(config),
...config.globalHeaders,
}

this.logger.debug(
"Initiating Medusa client with default headers:\n",
`${JSON.stringify(sanitizeHeaders(defaultHeaders), null, 2)}\n`
)

return (
input: FetchParams[0],
init?: FetchParams[1] & { query?: Record<string, any> }
) => {
// We always want to fetch the up-to-date JWT token before firing off a request.
const jwtToken = this.getJwtTokenHeader(config)

const headers = init?.headers
? { ...defaultHeaders, ...jwtToken, ...init.headers }
: { ...defaultHeaders, ...jwtToken }

let normalizedInput: RequestInfo | URL = input
if (input instanceof URL || typeof input === "string") {
normalizedInput = new URL(input, config.baseUrl)
if (init?.query) {
const existing = qs.parse(normalizedInput.search)
const stringifiedQuery = qs.stringify({ existing, ...init.query })
normalizedInput.search = stringifiedQuery
}
}

this.logger.debug(
"Performing request to:\n",
`URL: ${normalizedInput.toString()}\n`,
`Headers: ${JSON.stringify(sanitizeHeaders(headers), null, 2)}\n`
)

// TODO: Make response a bit more user friendly (throw errors, return JSON if resp content type is json, etc.)
return fetch(normalizedInput, { ...init, headers }).then((resp) => {
this.logger.debug(`Received response with status ${resp.status}\n`)
return resp
})
}
}

protected getApiKeyHeader = (
config: Config
): { Authorization: string } | {} => {
return config.apiKey
? { Authorization: "Basic " + toBase64(config.apiKey + ":") }
: {}
}

protected getPublishableKeyHeader = (
config: Config
): { "x-medusa-pub-key": string } | {} => {
return config.publishableKey
? { "x-medusa-pub-key": config.publishableKey }
: {}
}

protected getJwtTokenHeader = (
config: Config
): { Authorization: string } | {} => {
const storageMethod =
config.jwtToken?.storageMethod || (isBrowser() ? "local" : "memory")
const storageKey =
config.jwtToken?.storageKey || this.DEFAULT_JWT_STORAGE_KEY

switch (storageMethod) {
case "local": {
if (!isBrowser()) {
throw new Error("Local JWT storage is only available in the browser")
}
const token = window.localStorage.getItem(storageKey)
return token ? { Authorization: `Bearer ${token}` } : {}
}
case "session": {
if (!isBrowser()) {
throw new Error(
"Session JWT storage is only available in the browser"
)
}
const token = window.sessionStorage.getItem(storageKey)
return token ? { Authorization: `Bearer ${token}` } : {}
}
case "memory": {
return this.token ? { Authorization: `Bearer ${this.token}` } : {}
}
}
}
}
18 changes: 18 additions & 0 deletions packages/core/js-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Admin } from "./admin"
import { Client } from "./client"
import { Store } from "./store"
import { Config } from "./types"

class Medusa {
public client: Client
public admin: Admin
public store: Store

constructor(config: Config) {
this.client = new Client(config)
this.admin = new Admin(this.client)
this.store = new Store(this.client)
}
}

export default Medusa

0 comments on commit 69f3446

Please sign in to comment.