Skip to content

feat: add Hygraph support #142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion data/subdomains.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"imagekit.io": "imagekit",
"cloudimg.io": "cloudimage",
"ucarecdn.com": "uploadcare",
"supabase.co": "supabase"
"supabase.co": "supabase",
"graphassets.com": "hygraph"
}
4 changes: 4 additions & 0 deletions demo/src/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,9 @@
"supabase": [
"Supabase",
"https://enlyjtqaeutqbhqgkadn.supabase.co/storage/v1/object/public/sample-public-bucket/alexander-shatov-PHH_0uw9-Qw-unsplash.jpg"
],
"hygraph": [
"Hygraph",
"https://us-west-2.graphassets.com/cm2apl1zp07l506n66dmd9xo8/cm2tr64fx7gvu07n85chjmuno"
]
}
2 changes: 2 additions & 0 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { parse as netlify } from "./transformers/netlify.ts";
import { parse as imagekit } from "./transformers/imagekit.ts";
import { parse as uploadcare } from "./transformers/uploadcare.ts";
import { parse as supabase } from "./transformers/supabase.ts";
import { parse as hygraph } from "./transformers/hygraph.ts";
import { ImageCdn, ParsedUrl, SupportedImageCdn, UrlParser } from "./types.ts";

export const parsers = {
Expand Down Expand Up @@ -52,6 +53,7 @@ export const parsers = {
imagekit,
uploadcare,
supabase,
hygraph
};

export const cdnIsSupportedForParse = (
Expand Down
2 changes: 2 additions & 0 deletions src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { transform as netlify } from "./transformers/netlify.ts";
import { transform as imagekit } from "./transformers/imagekit.ts";
import { transform as uploadcare } from "./transformers/uploadcare.ts";
import { transform as supabase } from "./transformers/supabase.ts";
import { transform as hygraph } from "./transformers/hygraph.ts";
import { ImageCdn, UrlTransformer } from "./types.ts";
import { getCanonicalCdnForUrl } from "./canonical.ts";

Expand Down Expand Up @@ -53,6 +54,7 @@ export const getTransformer = (cdn: ImageCdn) => ({
imagekit,
uploadcare,
supabase,
hygraph
}[cdn]);

/**
Expand Down
88 changes: 88 additions & 0 deletions src/transformers/hygraph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { assertEquals } from "jsr:@std/assert";
import { HygraphParams, parse, transform } from "./hygraph.ts";
import { ParsedUrl } from "../types.ts";

const imageBase = "https://us-west-2.graphassets.com/cm2apl1zp07l506n66dmd9xo8/cm2tr64fx7gvu07n85chjmuno";

const imageWithAutoFormat =
"https://us-west-2.graphassets.com/cm2apl1zp07l506n66dmd9xo8/resize=fit:crop,width:400,height:400/auto_image/cm2tr64fx7gvu07n85chjmuno";

const imageWithExplicitFormat =
"https://us-west-2.graphassets.com/cm2apl1zp07l506n66dmd9xo8/resize=fit:crop,width:400,height:400/output=format:jpg/cm2tr64fx7gvu07n85chjmuno";

Deno.test("hygraph", async (t) => {
await t.step("parses a URL with auto format", () => {
const parsed = parse(imageWithAutoFormat);
const expected: ParsedUrl<HygraphParams> = {
base: imageWithAutoFormat,
cdn: "hygraph",
format: "auto",
width: 400,
height: 400,
params: {
transformations: {
resize: {
width: 400,
height: 400,
fit: "crop",
},
auto_image: {},
},
region: "us-west-2",
envId: "cm2apl1zp07l506n66dmd9xo8",
handle: "cm2tr64fx7gvu07n85chjmuno",
},
};

assertEquals(parsed, expected);
});

await t.step("parses a URL with explicit format", () => {
const parsed = parse(imageWithExplicitFormat);
const expected: ParsedUrl<HygraphParams> = {
base: imageWithExplicitFormat,
cdn: "hygraph",
format: "jpg",
width: 400,
height: 400,
params: {
transformations: {
resize: {
width: 400,
height: 400,
fit: "crop",
},
output: {
format: "jpg",
},
},
region: "us-west-2",
envId: "cm2apl1zp07l506n66dmd9xo8",
handle: "cm2tr64fx7gvu07n85chjmuno",
},
};

assertEquals(parsed, expected);
});

await t.step("transforms a URL with auto format", () => {
const result = transform({
url: imageBase,
width: 400,
height: 400,
});

assertEquals(result?.toString(), imageWithAutoFormat);
});

await t.step("transforms a URL with explicit format", () => {
const result = transform({
url: imageBase,
width: 400,
height: 400,
format: "jpg",
});

assertEquals(result?.toString(), imageWithExplicitFormat);
});
});
121 changes: 121 additions & 0 deletions src/transformers/hygraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { UrlGenerator, UrlGeneratorOptions, UrlParser, UrlTransformer } from "../types.ts";

const hygraphRegex =
/https:\/\/(?<region>[a-z0-9-]+)\.graphassets\.com\/(?<envId>[a-z0-9]+)(?:\/(?<transformations>.*?))?\/(?<handle>[a-z0-9]+)$/;

export interface HygraphParams {
region?: string;
envId?: string;
transformations: Record<string, Record<string, string | number>>;
handle?: string;
}

export const parse: UrlParser<HygraphParams> = (url) => {
const base = url.toString();
const matches = base.match(hygraphRegex);

if (!matches?.length) {
throw new Error("Invalid Hygraph URL");
}

const group = matches.groups || {};
const { transformations: unparsedTransformations, ...params } = group;
const transformations = parseTransformations(unparsedTransformations || "");

return {
base,
width: Number(transformations.resize?.width) || undefined,
height: Number(transformations.resize?.height) || undefined,
format: transformations.auto_image ? "auto" : transformations.output?.format?.toString() || undefined,
params: { transformations, ...params },
cdn: "hygraph",
};
};

export const generate: UrlGenerator<HygraphParams> = ({ base, width, height, format, params }) => {
const parsed = parse(base.toString());
const props: HygraphParams = {
transformations: {},
...parsed.params,
...params,
};

if (width || height) {
props.transformations.resize ||= {};
}

if (width && height) {
props.transformations.resize.fit ||= "crop";
}

if (width) {
props.transformations.resize.width = width;
}

if (height) {
props.transformations.resize.height = height;
}

if (format === "auto") {
props.transformations.auto_image = {};
} else if (format) {
props.transformations.output ||= {};
props.transformations.output.format = format;
}

const url = new URL(base);
url.pathname = `/${props.envId}/${formatTransformations(props.transformations)}/${props.handle}`;
return url.toString();
};

export const transform: UrlTransformer = ({ url: originalUrl, width, height, format = "auto" }) => {
const parsed = parse(originalUrl);

if (!parsed) {
throw new Error("Invalid Hygraph URL");
}

const props: UrlGeneratorOptions<HygraphParams> = {
...parsed,
width,
height,
format,
};

return generate(props);
};

const parseTransformations = (transformations: string): Record<string, Record<string, string | number>> => {
if (!transformations) {
return {};
}

return transformations.split("/").reduce((result: Record<string, Record<string, string | number>>, part) => {
const [operation, params] = part.split("=");

if (params) {
result[operation] = params.split(",").reduce((obj: Record<string, string | number>, param) => {
const [key, value] = param.split(":");
obj[key] = isNaN(Number(value)) ? value : Number(value);
return obj;
}, {});
} else {
result[operation] = {};
}

return result;
}, {});
};

const formatTransformations = (transformations: Record<string, Record<string, string | number>>): string => {
return Object.entries(transformations)
.filter(([key, value]) => Boolean(key) && value !== undefined)
.map(([key, value]) =>
Object.keys(value).length === 0
? key
: `${key}=${Object.entries(value)
.map(([key, value]) => `${key}:${value}`)
.join(",")}`
)
.join("/");
};
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export type ImageCdn =
| "netlify"
| "imagekit"
| "uploadcare"
| "supabase";
| "supabase"
| "hygraph";

export type SupportedImageCdn = ImageCdn;
Loading