Skip to content

Commit

Permalink
feat: add Supabase support (#123)
Browse files Browse the repository at this point in the history
* feat: add supabase support

* fix(supabase): Image format

* chore: fix format workflow

---------

Co-authored-by: ascorbic <ascorbic@gmail.com>
Co-authored-by: Matt Kane <m@mk.gg>
  • Loading branch information
3 people committed Mar 12, 2024
1 parent 50020fd commit 0aa185a
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 5 deletions.
14 changes: 12 additions & 2 deletions .github/workflows/format.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
name: Format
on:
pull_request:
pull_request_target:
types:
- opened
- edited
- synchronize
push:
branches: [main]

permissions:
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup Deno
uses: denoland/setup-deno@v1
with:
Expand Down
4 changes: 3 additions & 1 deletion data/paths.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
"/is/image": "scene7",
"/_ipx/": "ipx",
"/_image": "astro",
"/.netlify/images": "netlify"
"/.netlify/images": "netlify",
"/storage/v1/object/public/": "supabase",
"/storage/v1/render/image/public/": "supabase"
}
3 changes: 2 additions & 1 deletion data/subdomains.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
"imgeng.in": "imageengine",
"imagekit.io": "imagekit",
"cloudimg.io": "cloudimage",
"ucarecdn.com": "uploadcare"
"ucarecdn.com": "uploadcare",
"supabase.co": "supabase"
}
4 changes: 4 additions & 0 deletions demo/src/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,9 @@
"uploadcare": [
"Uploadcare",
"https://ucarecdn.com/661bd414-064c-477a-b50f-8ffd8f66aa49/"
],
"supabase": [
"Supabase",
"https://enlyjtqaeutqbhqgkadn.supabase.co/storage/v1/object/public/sample-public-bucket/alexander-shatov-PHH_0uw9-Qw-unsplash.jpg"
]
}
2 changes: 2 additions & 0 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { parse as astro } from "./transformers/astro.ts";
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 { ImageCdn, ParsedUrl, SupportedImageCdn, UrlParser } from "./types.ts";

export const parsers = {
Expand Down Expand Up @@ -50,6 +51,7 @@ export const parsers = {
netlify,
imagekit,
uploadcare,
supabase,
};

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 @@ -23,6 +23,7 @@ import { transform as astro } from "./transformers/astro.ts";
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 { ImageCdn, UrlTransformer } from "./types.ts";
import { getCanonicalCdnForUrl } from "./canonical.ts";

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

/**
Expand Down
130 changes: 130 additions & 0 deletions src/transformers/supabase.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { assertEquals } from "https://deno.land/std@0.172.0/testing/asserts.ts";
import { ParsedUrl } from "../types.ts";
import { generate, parse, SupabaseParams, transform } from "./supabase.ts";

const img =
"https://enlyjtqaeutqbhqgkadn.supabase.co/storage/v1/render/image/public/sample-public-bucket/alexander-shatov-PHH_0uw9-Qw-unsplash.jpg?width=600&height=500&quality=50&resize=contain&format=origin";

const imgNoTransforms =
"https://enlyjtqaeutqbhqgkadn.supabase.co/storage/v1/object/public/sample-public-bucket/alexander-shatov-PHH_0uw9-Qw-unsplash.jpg";

const imgCustom =
"https://api.some-custom-domain.com/storage/v1/render/image/public/sample-public-bucket/alexander-shatov-PHH_0uw9-Qw-unsplash.jpg?width=600&height=500&quality=50&resize=contain&format=origin";

Deno.test("supabase parser", async (t) => {
await t.step("parses a URL", () => {
const parsed = parse(img);

const expected: ParsedUrl<SupabaseParams> = {
base: imgNoTransforms,
cdn: "supabase",
width: 600,
height: 500,
format: "origin",
params: {
quality: 50,
resize: "contain",
},
};
assertEquals(parsed, expected);
});

await t.step("parses a URL without transforms", () => {
const parsed = parse(imgNoTransforms);
const expected: ParsedUrl<SupabaseParams> = {
base: imgNoTransforms,
cdn: "supabase",
};
assertEquals(parsed, expected);
});

await t.step("parses a URL with custom domain", () => {
const parsed = parse(imgCustom);
const expected: ParsedUrl<SupabaseParams> = {
base:
"https://api.some-custom-domain.com/storage/v1/object/public/sample-public-bucket/alexander-shatov-PHH_0uw9-Qw-unsplash.jpg",
cdn: "supabase",
width: 600,
height: 500,
format: "origin",
params: {
quality: 50,
resize: "contain",
},
};
assertEquals(parsed, expected);
});
});

Deno.test("supabase transformer", async (t) => {
await t.step("transforms a URL", () => {
const result = transform({
url: img,
width: 100,
height: 200,
});
assertEquals(
result?.toString(),
"https://enlyjtqaeutqbhqgkadn.supabase.co/storage/v1/render/image/public/sample-public-bucket/alexander-shatov-PHH_0uw9-Qw-unsplash.jpg?width=100&height=200&format=origin&quality=50&resize=contain",
);
});

await t.step("rounds non-integer values", () => {
const result = transform({
url: img,
width: 100.6,
height: 200.2,
});
assertEquals(
result?.toString(),
"https://enlyjtqaeutqbhqgkadn.supabase.co/storage/v1/render/image/public/sample-public-bucket/alexander-shatov-PHH_0uw9-Qw-unsplash.jpg?width=101&height=200&format=origin&quality=50&resize=contain",
);
});

await t.step("transforms a URL without parsed transforms", () => {
const result = transform({
url: imgNoTransforms,
width: 100,
height: 200,
});
assertEquals(
result?.toString(),
"https://enlyjtqaeutqbhqgkadn.supabase.co/storage/v1/render/image/public/sample-public-bucket/alexander-shatov-PHH_0uw9-Qw-unsplash.jpg?width=100&height=200",
);
});
});

Deno.test("supabase generator", async (t) => {
await t.step("generates a URL", () => {
const result = generate({
base: img,
width: 100,
height: 200,
params: {
quality: 80,
resize: "cover",
},
});
assertEquals(
result?.toString(),
"https://enlyjtqaeutqbhqgkadn.supabase.co/storage/v1/render/image/public/sample-public-bucket/alexander-shatov-PHH_0uw9-Qw-unsplash.jpg?width=100&height=200&format=origin&quality=80&resize=cover",
);
});

await t.step("generates a URL without parsed transforms", () => {
const result = generate({
base: imgNoTransforms,
width: 100,
height: 200,
format: "origin",
params: {
quality: 80,
resize: "cover",
},
});
assertEquals(
result?.toString(),
"https://enlyjtqaeutqbhqgkadn.supabase.co/storage/v1/render/image/public/sample-public-bucket/alexander-shatov-PHH_0uw9-Qw-unsplash.jpg?width=100&height=200&format=origin&quality=80&resize=cover",
);
});
});
125 changes: 125 additions & 0 deletions src/transformers/supabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
UrlGenerator,
UrlGeneratorOptions,
UrlParser,
UrlTransformer,
} from "../types.ts";
import { roundIfNumeric, toUrl } from "../utils.ts";

const ALLOWED_FORMATS = ["origin"];

const STORAGE_URL_PREFIX = "/storage/v1/object/public/";
const RENDER_URL_PREFIX = "/storage/v1/render/image/public/";

const isRenderUrl = (url: URL) => url.pathname.startsWith(RENDER_URL_PREFIX);

export interface SupabaseParams {
/**
* The quality of the returned image - a value from 20 to 100 (with 100 being the highest quality).
*
* @type {number}
* @default 80
* @see https://supabase.com/docs/guides/storage/serving/image-transformations#optimizing
*/
quality?: number;
/**
* You can use different resizing modes to fit your needs, each of them uses a different approach to resize the image.
* - `cover`: resizes the image while keeping the aspect ratio to fill a given size and crops projecting parts. (default)
* - `contain`: resizes the image while keeping the aspect ratio to fit a given size.
* - `fill`: resizes the image without keeping the aspect ratio.
*
* @type {"cover" | "contain" | "fill"}
* @default "cover"
* @see https://supabase.com/docs/guides/storage/serving/image-transformations#modes
*/
resize?: "cover" | "contain" | "fill";
}

export const parse: UrlParser<SupabaseParams> = (
imageUrl,
) => {
const url = toUrl(imageUrl);
const isRender = isRenderUrl(url);

if (!isRender) {
return {
cdn: "supabase",
base: url.origin + url.pathname,
};
}

const imagePath = url.pathname.replace(RENDER_URL_PREFIX, "");

const quality = url.searchParams.has("quality")
? Number(url.searchParams.get("quality"))
: undefined;
const width = url.searchParams.has("width")
? Number(url.searchParams.get("width"))
: undefined;
const height = url.searchParams.has("height")
? Number(url.searchParams.get("height")!)
: undefined;
const format = url.searchParams.has("format")
? url.searchParams.get("format")!
: undefined;
const resize = url.searchParams.has("resize")
? url.searchParams.get("resize") as "cover" | "contain" | "fill"
: undefined;

return {
cdn: "supabase",
base: url.origin + STORAGE_URL_PREFIX + imagePath,
width,
height,
format,
params: {
quality,
resize,
},
};
};

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

base = parsed.base;
width = width || parsed.width;
height = height || parsed.height;
format = format || parsed.format;
params = { ...parsed.params, ...params };

const searchParams = new URLSearchParams();

if (width) searchParams.set("width", roundIfNumeric(width).toString());

if (height) searchParams.set("height", roundIfNumeric(height).toString());

if (format && ALLOWED_FORMATS.includes(format)) searchParams.set("format", format);

if (params?.quality) {
searchParams.set("quality", roundIfNumeric(params.quality).toString());
}

if (params?.resize) searchParams.set("resize", params.resize);

if (searchParams.toString() === "") return base;

return parsed.base.replace(STORAGE_URL_PREFIX, RENDER_URL_PREFIX) + "?" +
searchParams.toString();
};

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

return generate({
base: parsed.base,
width: width || parsed.width,
height: height || parsed.height,
format: format || parsed.format,
params: cdnOptions?.supabase || parsed.params,
});
};
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export type ImageCdn =
| "astro"
| "netlify"
| "imagekit"
| "uploadcare";
| "uploadcare"
| "supabase";

export type SupportedImageCdn = ImageCdn;

0 comments on commit 0aa185a

Please sign in to comment.