Skip to content
This repository has been archived by the owner on Jan 12, 2021. It is now read-only.

Commit

Permalink
Merge pull request #2 from Foxy/release/1.0.0-beta.6
Browse files Browse the repository at this point in the history
Release/1.0.0 beta.6
  • Loading branch information
brettflorio authored May 14, 2020
2 parents 607fb43 + 9f0319e commit 04369bc
Show file tree
Hide file tree
Showing 15 changed files with 690 additions and 28 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ src/playground.ts
.env
.eslintcache
.coverage
.vscode
.vscode
.DS_Store
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@foxy.io/node-api",
"version": "1.0.0-beta.5",
"version": "1.0.0-beta.6",
"description": "FoxyCart hAPI client for Node",
"main": "dist/index.js",
"types": "dist/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as winston from "winston";
import * as logform from "logform";
import fetch from "node-fetch";
import { Cache, MemoryCache } from "./utils/cache";
import { Props } from "./types/props";
import { Props } from "./types/api/props";

import {
FOXY_API_CLIENT_ID,
Expand Down
2 changes: 1 addition & 1 deletion src/follower.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Sender } from "./sender";
*
* **IMPORTANT:** this class is internal; using it in consumers code is not recommended.
*/
export class Follower<Graph extends ApiGraph, Host extends PathMember> extends Sender<Host> {
export class Follower<Graph extends ApiGraph, Host extends PathMember> extends Sender<Graph, Host> {
/**
* Navigates to the nested resource, building a request query.
* Calling this method will not fetch your data immediately. For the list of relations please refer to the
Expand Down
10 changes: 5 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { FOXY_API_URL } from "./env";
import { Follower } from "./follower";
import { Sender, SendRawInit } from "./sender";
import { ApiGraph, Followable } from "./types/utils";
import { Graph } from "./types/graph";
import { Props } from "./types/props";
import { Graph } from "./types/api/graph";
import { Props } from "./types/api/props";
import * as cache from "./utils/cache";
import * as sanitize from "./utils/sanitize";
import * as sso from "./utils/sso";
Expand Down Expand Up @@ -91,10 +91,10 @@ class Api extends Auth {
*
* @param init fetch-like request initializer supporting url, method and body params
*/
fetchRaw<Host extends keyof Props = any>(init: SendRawInit<Host>) {
return new Sender<Host>(this, [], this.endpoint).fetchRaw(init);
fetchRaw<Host extends keyof Props = any, Graph extends ApiGraph = any>(init: SendRawInit<Host>) {
return new Sender<Graph, Host>(this, [], this.endpoint).fetchRaw(init);
}
}

export { Api as FoxyApi };
export { Graph as FoxyApiGraph } from "./types/graph";
export { Graph as FoxyApiGraph } from "./types/api/graph";
166 changes: 150 additions & 16 deletions src/sender.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import fetch from "node-fetch";
import traverse from "traverse";
import { Methods } from "./types/methods";
import { HTTPMethod, HTTPMethodWithBody } from "./types/utils";
import { Methods } from "./types/api/methods";

import {
HTTPMethod,
HTTPMethodWithBody,
ApiGraph,
PathMember,
ZoomUnion,
NeverIfUndefined,
Resource,
Fields,
Order,
} from "./types/utils";

import { Resolver } from "./resolver";
import { Props } from "./types/props";
import { Props } from "./types/api/props";

type SendBody<Host, Method> = Method extends HTTPMethodWithBody
? Host extends keyof Props
Expand All @@ -13,11 +25,6 @@ type SendBody<Host, Method> = Method extends HTTPMethodWithBody

type SendMethod<Host> = Host extends keyof Methods ? Methods[Host] : HTTPMethod;

type SendResponse<Host> = (Host extends keyof Props ? Props[Host] : any) & {
_embedded: any;
_links: any;
};

export interface SendRawInit<Host, Method = SendMethod<Host>> {
/**
* The absolute URL (either a `URL` instance or a string)
Expand Down Expand Up @@ -45,11 +52,78 @@ export type SendInit<Host, Method = SendMethod<Host>> = Omit<SendRawInit<Host, M
*/
skipCache?: boolean;

/**
* An array of fields to include in the response object (aka partial resource).
* Same as setting the `fields` query parameter. If you provide values in both `fields` and `query`,
* they will be parsed, deduped and merged.
*/
fields?: Fields<Host>;

/**
* A key-value map containing the query parameters that you'd like to add to the URL when it's resolved.
* You can also use `URLSearchParams` if convenient. Empty set by default.
*/
query?: URLSearchParams | Record<string, string>;

/**
* Zoomable resources to embed in the response. Pass a string literal
* for a single resource, an array of string literals for multiple,
* and an object for multi-level zooming. Just like in the raw query
* parameter value, only bare relation names are supported (without the `fx` prefix and
* the `https://api.foxycart.com/rels` path before the relation name).
*
* @see https://api.foxycart.com/docs/cheat-sheet ("Zooming" section).
* @example
*
* // &zoom=transactions
* { zoom: "transactions" }
*
* // &zoom=transactions,customer
* { zoom: [ "transactions, customer" ] }
*
* // &zoom=customer:default_billing_address
* { zoom: { customer: ["default_billing_address"] } }
*
* // &zoom=transactions,customer:default_billing_address
* { zoom: [ "transactions", { customer: ["default_billing_address"] } ] }
*/
zoom?: ZoomUnion<Host>;

/**
* You can adjust the sorting order of the collection response using this parameter.
* Default direction is `asc` (ascending).
*
* @see https://api.foxycart.com/docs/cheat-sheet ("Sorting" section).
* @example
*
* // &order=date_created
* { order: "date_created" }
*
* // &order=date_created desc
* { order: { date_created: "desc" } }
*
* // &order=date_created,transaction_date
* { order: ["date_created", "transaction_date"] }
*
* // &order=date_created desc,transaction_date
* { order: [ { date_created: "desc" }, "transaction_date"] }
*/
order?: Order<Host>;

/**
* Out of the box, the API includes pagination links to move between pages of results via
* the rels `first`, `prev`, `next` and `last`, but you can also control the number of results per page
* with this parameter. The API returns 20 items per page by default,
* and currently the maximum results per page is 300.
*/
limit?: number;

/**
* Out of the box, the API includes pagination links to move between pages of results via
* the rels `first`, `prev`, `next` and `last`, but you can also specify a starting offset for the results
* with this parameter. The default value is 0.
*/
offset?: number;
};

/**
Expand All @@ -58,7 +132,7 @@ export type SendInit<Host, Method = SendMethod<Host>> = Omit<SendRawInit<Host, M
*
* **IMPORTANT:** this class is internal; using it in consumers code is not recommended.
*/
export class Sender<Host extends string | number | symbol> extends Resolver {
export class Sender<Graph extends ApiGraph, Host extends PathMember> extends Resolver {
/**
* Makes an API request to the specified URL, skipping the path construction
* and resolution. This is what `.fetch()` uses under the hood. Before calling
Expand All @@ -74,7 +148,7 @@ export class Sender<Host extends string | number | symbol> extends Resolver {
* });
* @param init fetch-like request initializer supporting url, method and body params
*/
async fetchRaw(params: SendRawInit<Host>): Promise<SendResponse<Host>> {
async fetchRaw(params: SendRawInit<Host>): Promise<Resource<Graph, Host>> {
const method = params.method ?? "GET";

const response = await fetch(params.url, {
Expand Down Expand Up @@ -122,20 +196,57 @@ export class Sender<Host extends string | number | symbol> extends Resolver {
*
* @param params API request options such as method, query or body
*/
async fetch(params?: SendInit<Host>): Promise<SendResponse<Host>> {
async fetch<T extends SendInit<Host>>(
params?: T
): Promise<
Resource<
Graph,
Host,
NeverIfUndefined<T["fields"]>,
T["fields"] extends any[] ? never : T["zoom"]
>
> {
let url = new URL(await this.resolve(params?.skipCache));

if (params?.query) {
const entries = [...new URLSearchParams(params.query).entries()];
entries.forEach((v) => url.searchParams.append(...v));
}

const rawParams: SendRawInit<Host> = traverse(params).map(function () {
if (this.key && ["query", "skipCache"].includes(this.key)) this.remove();
});
const queryFields = url.searchParams
.getAll("fields")
.map((v) => v.split(","))
.reduce<string[]>((p, c) => p.concat(c), []);

const paramsFields = (params?.fields ?? []) as string[];
const mergedFields = [...new Set(queryFields.concat(paramsFields))];

if (mergedFields.length > 0) {
url.searchParams.set("fields", mergedFields.join(","));
}

if (params?.zoom) {
url.searchParams.set("zoom", this._getZoomQueryValue("", params.zoom));
}

if (params?.limit) {
url.searchParams.set("limit", params.limit.toFixed(0));
}

if (params?.offset) {
url.searchParams.set("offset", params.offset.toFixed(0));
}

if (params?.order) {
url.searchParams.set("order", this._getOrderQueryValue(params.order));
}

const rawParams: SendRawInit<Host> = { url };
if (params?.body) rawParams.body = params.body;
if (params?.method) rawParams.method = params.method;

try {
return await this.fetchRaw({ url, ...rawParams });
return (await this.fetchRaw({ url, ...rawParams })) as any;
} catch (e) {
if (!params?.skipCache && e.message.includes("No route found")) {
this._auth.log({
Expand All @@ -144,11 +255,34 @@ export class Sender<Host extends string | number | symbol> extends Resolver {
});

url = new URL(await this.resolve(true));
return this.fetchRaw({ url, ...rawParams });
return this.fetchRaw({ url, ...rawParams }) as any;
} else {
this._auth.log({ level: "error", message: e.message });
throw e;
}
}
}

private _getZoomQueryValue(prefix: string, zoom: ZoomUnion<Host>): string {
const scope = prefix === "" ? "" : prefix + ":";

if (typeof zoom === "string") return scope + zoom;
if (Array.isArray(zoom)) return zoom.map((v) => this._getZoomQueryValue(prefix, v)).join();

return Object.entries(zoom)
.map(([key, value]) => this._getZoomQueryValue(scope + key, value))
.join();
}

private _getOrderQueryValue(order: Order<Host>): string {
if (typeof order === "string") return order;

if (Array.isArray(order)) {
return order.map((item) => this._getOrderQueryValue(item)).join();
}

return Object.entries(order)
.map(([key, value]) => `${key} ${value}`)
.join();
}
}
45 changes: 45 additions & 0 deletions src/types/api/collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export interface Collections {
"fx:taxes": "fx:tax";
"fx:applied_coupon_codes": "fx:coupon_code";
"fx:applied_taxes": "fx:applied_tax";
"fx:attributes": "fx:attribute";
"fx:billing_addresses": "fx:billing_address";
"fx:cart_include_templates": "fx:cart_include_template";
"fx:cart_templates": "fx:cart_template";
"fx:checkout_templates": "fx:checkout_template";
"fx:coupon_codes": "fx:coupon_code";
"fx:coupon_item_categories": "fx:coupon_item_category";
"fx:coupon_code_transactions": "fx:coupon_code_transaction";
"fx:custom_fields": "fx:custom_field";
"fx:downloadable_item_categories": "fx:item_category";
"fx:discounts": "fx:discount";
"fx:email_templates": "fx:email_template";
"fx:error_entries": "fx:error_entry";
"fx:hosted_payment_gateways": "fx:hosted_payment_gateway";
"fx:integrations": "fx:integration";
"fx:language_overrides": "fx:language_override";
"fx:native_integrations": "fx:native_integration";
"fx:payments": "fx:payment";
"fx:payment_methods_expiring": "fx:payment_method_expiring";
"fx:payment_method_sets": "fx:payment_method_set";
"fx:payment_gateways": "fx:payment_gateway";
"fx:items": "fx:item";
"fx:item_categories": "fx:item_category";
"fx:item_options": "fx:item_option";
"fx:receipt_templates": "fx:receipt_template";
"fx:shipping_containers": "fx:shipping_container";
"fx:shipping_drop_types": "fx:shipping_drop_type";
"fx:shipping_methods": "fx:shipping_method";
"fx:shipping_services": "fx:shipping_service";
"fx:shipments": "fx:shipment";
"fx:stores": "fx:store";
"fx:store_versions": "fx:store_version";
"fx:store_shipping_methods": "fx:shipping_method";
"fx:store_shipping_services": "fx:shipping_service";
"fx:transactions": "fx:transaction";
"fx:subscriptions": "fx:subscription";
"fx:subscription_settings": "fx:subscription_settings";
"fx:template_configs": "fx:template_config";
"fx:template_sets": "fx:template_set";
"fx:users": "fx:user";
}
23 changes: 22 additions & 1 deletion src/types/graph.ts → src/types/api/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,31 @@ interface Customer {
"fx:customer_addresses": IndexedCollection<Address>;
}

interface DiscountDetail {
"self": DiscountDetail;
"fx:store": Store;
"fx:item": Item;
"fx:transaction": Transaction;
}

interface CouponDetail {
"self": CouponDetail;
"fx:store": Store;
"fx:item": Item;
"fx:coupon": Coupon;
"fx:coupon_code": CouponCode;
"fx:transaction": Transaction;
}

interface Item {
"self": Item;
"fx:store": Store;
"fx:cart": Cart;
"fx:item_category": ItemCategory;
"fx:item_options": IndexedCollection<ItemOption>;
"shipment": Shipment;
"fx:shipment": Shipment;
"fx:discount_details": IndexedCollection<DiscountDetail>;
"fx:coupon_details": IndexedCollection<CouponDetail>;
}

interface Tax {
Expand Down Expand Up @@ -207,6 +225,8 @@ interface Cart {
"fx:customer": IndexedCollection<Customer>;
"fx:subscription": Subscription;
"fx:items": IndexedCollection<Item>;
"fx:discounts": IndexedCollection<Discount>;
"fx:custom_fields": IndexedCollection<CustomField>;
}

interface StoreAttribute {
Expand Down Expand Up @@ -343,6 +363,7 @@ interface Subscription {
"fx:attributes": IndexedCollection<SubscriptionAttribute>;
"fx:store": Store;
"fx:customer": Customer;
"fx:transactions": IndexedCollection<Transaction>;
"fx:original_transaction": Transaction;
"fx:last_transaction": Transaction;
"fx:transaction_template": Cart;
Expand Down
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 04369bc

Please sign in to comment.