Skip to content

Commit 574af67

Browse files
authored
feat: add Appwrite support (#160)
* add Appwrite support * code formatting * remove unnecessary comments
1 parent c40d984 commit 574af67

File tree

9 files changed

+233
-3
lines changed

9 files changed

+233
-3
lines changed

data/paths.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
"/_image": "astro",
99
"/.netlify/images": "netlify",
1010
"/storage/v1/object/public/": "supabase",
11-
"/storage/v1/render/image/public/": "supabase"
11+
"/storage/v1/render/image/public/": "supabase",
12+
"/v1/storage/buckets/": "appwrite"
1213
}

demo/src/examples.json

+4
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,9 @@
9191
"hygraph": [
9292
"Hygraph",
9393
"https://us-west-2.graphassets.com/cm2apl1zp07l506n66dmd9xo8/cm2tr64fx7gvu07n85chjmuno"
94+
],
95+
"appwrite": [
96+
"Appwrite",
97+
"https://cloud.appwrite.io/v1/storage/buckets/unpic/files/679d127100131f67b6d8/view?project=unpic-test"
9498
]
9599
}

deno.jsonc

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@
5757
]
5858
},
5959
"license": "MIT"
60-
}
60+
}

src/extract.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
} from "./providers/types.ts";
77
import type { ImageCdn, ParseURLResult, URLExtractor } from "./types.ts";
88

9+
import { extract as appwrite } from "./providers/appwrite.ts";
910
import { extract as astro } from "./providers/astro.ts";
1011
import { extract as builder } from "./providers/builder.io.ts";
1112
import { extract as bunny } from "./providers/bunny.ts";
@@ -34,6 +35,7 @@ import { extract as vercel } from "./providers/vercel.ts";
3435
import { extract as wordpress } from "./providers/wordpress.ts";
3536

3637
export const parsers: URLExtractorMap = {
38+
appwrite,
3739
astro,
3840
"builder.io": builder,
3941
bunny,

src/providers/appwrite.test.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { extract, generate, transform } from "./appwrite.ts";
2+
import { assertEqualIgnoringQueryOrder } from "../test-utils.ts";
3+
import { assertEquals } from "jsr:@std/assert";
4+
5+
const imageUrl =
6+
"https://cloud.appwrite.io/v1/storage/buckets/unpic/files/679d127100131f67b6d8/view?project=unpic-test";
7+
8+
// Tests for generate, extract, and transform
9+
10+
Deno.test("Appwrite - generate", async (t) => {
11+
await t.step("should generate a URL with transformations", () => {
12+
const result = generate(imageUrl, { width: 800, height: 600 });
13+
assertEqualIgnoringQueryOrder(
14+
result,
15+
"https://cloud.appwrite.io/v1/storage/buckets/unpic/files/679d127100131f67b6d8/preview?project=unpic-test&width=800&height=600",
16+
);
17+
});
18+
19+
await t.step("should generate a URL with quality and format", () => {
20+
const result = generate(imageUrl, {
21+
width: 800,
22+
quality: 75,
23+
format: "webp",
24+
});
25+
assertEqualIgnoringQueryOrder(
26+
result,
27+
"https://cloud.appwrite.io/v1/storage/buckets/unpic/files/679d127100131f67b6d8/preview?project=unpic-test&width=800&quality=75&output=webp",
28+
);
29+
});
30+
});
31+
32+
Deno.test("Appwrite - extract", async (t) => {
33+
await t.step(
34+
"should extract transformations from a transformed URL",
35+
() => {
36+
const parsed = extract(
37+
"https://cloud.appwrite.io/v1/storage/buckets/unpic/files/679d127100131f67b6d8/preview?project=unpic-test&width=800&height=600&quality=75&output=webp",
38+
);
39+
assertEquals(parsed, {
40+
src:
41+
"https://cloud.appwrite.io/v1/storage/buckets/unpic/files/679d127100131f67b6d8/preview?project=unpic-test",
42+
operations: {
43+
width: 800,
44+
height: 600,
45+
format: "webp",
46+
quality: 75,
47+
},
48+
});
49+
},
50+
);
51+
});
52+
53+
Deno.test("Appwrite - transform", async (t) => {
54+
await t.step("should transform a URL with new operations", () => {
55+
const result = transform(
56+
"https://cloud.appwrite.io/v1/storage/buckets/unpic/files/679d127100131f67b6d8/preview?project=unpic-test&width=300&height=400",
57+
{ width: 800, height: 600, quality: 80, format: "webp" },
58+
);
59+
assertEqualIgnoringQueryOrder(
60+
result,
61+
"https://cloud.appwrite.io/v1/storage/buckets/unpic/files/679d127100131f67b6d8/preview?project=unpic-test&width=800&height=600&quality=80&output=webp",
62+
);
63+
});
64+
});

src/providers/appwrite.ts

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { getProviderForUrlByPath } from "../detect.ts";
2+
import type {
3+
ImageFormat,
4+
Operations,
5+
URLExtractor,
6+
URLGenerator,
7+
URLTransformer,
8+
} from "../types.ts";
9+
import {
10+
createExtractAndGenerate,
11+
createOperationsHandlers,
12+
toCanonicalUrlString,
13+
toUrl,
14+
} from "../utils.ts";
15+
16+
type AppwriteOutputFormats =
17+
| ImageFormat
18+
| "gif";
19+
20+
const VIEW_URL_SUFFIX = "/view?";
21+
const PREVIEW_URL_SUFFIX = "/preview?";
22+
23+
/**
24+
* @see https://appwrite.io/docs/products/storage/images
25+
*/
26+
export interface AppwriteOperations extends Operations<AppwriteOutputFormats> {
27+
/**
28+
* Set the width of the output image in pixels.
29+
* Output image will be resized keeping the aspect ratio intact.
30+
* @type {number} Range: 0-4000
31+
*/
32+
width?: number;
33+
34+
/**
35+
* Set the height of the output image in pixels.
36+
* Output image will be resized keeping the aspect ratio intact.
37+
* @type {number} Range: 0-4000
38+
*/
39+
height?: number;
40+
41+
/**
42+
* Set the gravity while cropping the output image, providing either width, height, or both.
43+
*/
44+
gravity?:
45+
| "center"
46+
| "top-left"
47+
| "top"
48+
| "top-right"
49+
| "left"
50+
| "right"
51+
| "bottom-left"
52+
| "bottom"
53+
| "bottom-right";
54+
55+
/**
56+
* Set the quality of the output image
57+
* @type {number} Range: 0-100
58+
*/
59+
quality?: number;
60+
61+
/**
62+
* Set a border with the given width in pixels for the output image.
63+
* @type {number} Range: 0-100
64+
*/
65+
borderWidth?: number;
66+
67+
/**
68+
* Set a border-color for the output image.
69+
* Accepts any valid hex color value without the leading '#'.
70+
*/
71+
borderColor?: string;
72+
73+
/**
74+
* Set the border-radius in pixels.
75+
* @type {number} Range: 0-4000
76+
*/
77+
borderRadius?: number;
78+
79+
/**
80+
* Set opacity for the output image.
81+
* Works only with output formats supporting alpha channels, like 'png'.
82+
* @type {number} Range: 0-1
83+
*/
84+
opacity?: number;
85+
86+
/**
87+
* Rotates the output image by a degree.
88+
* @type {number} Range: -360-360
89+
*/
90+
rotation?: number;
91+
92+
/**
93+
* Set a background-color for the output image.
94+
* Accepts any valid hex color value without the leading '#'.
95+
* Works only with output formats supporting alpha channels, like 'png'.
96+
*/
97+
background?: string;
98+
99+
/**
100+
* Set the output image format.
101+
* If not provided, will use the original image's format.
102+
*/
103+
output?: AppwriteOutputFormats;
104+
}
105+
106+
const { operationsGenerator, operationsParser } = createOperationsHandlers<
107+
AppwriteOperations
108+
>({
109+
keyMap: {
110+
format: "output",
111+
},
112+
kvSeparator: "=",
113+
paramSeparator: "&",
114+
});
115+
116+
export const generate: URLGenerator<"appwrite"> = (src, modifiers) => {
117+
const url = toUrl(
118+
src.toString().replace(VIEW_URL_SUFFIX, PREVIEW_URL_SUFFIX),
119+
);
120+
const projectParam = url.searchParams.get("project") ?? "";
121+
122+
const operations = operationsGenerator(modifiers);
123+
url.search = operations;
124+
url.searchParams.append("project", projectParam);
125+
126+
return toCanonicalUrlString(url);
127+
};
128+
129+
export const extract: URLExtractor<"appwrite"> = (url) => {
130+
if (getProviderForUrlByPath(url) !== "appwrite") {
131+
return null;
132+
}
133+
const parsedUrl = toUrl(url);
134+
const operations = operationsParser(parsedUrl);
135+
// deno-lint-ignore no-explicit-any
136+
delete (operations as any).project;
137+
138+
const projectParam = parsedUrl.searchParams.get("project") ?? "";
139+
parsedUrl.search = "";
140+
parsedUrl.searchParams.append("project", projectParam);
141+
142+
const sourceUrl = parsedUrl.href;
143+
144+
return {
145+
src: sourceUrl,
146+
operations,
147+
};
148+
};
149+
150+
export const transform: URLTransformer<
151+
"appwrite"
152+
> = createExtractAndGenerate(extract, generate);

src/providers/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
URLGenerator,
55
URLTransformer,
66
} from "../types.ts";
7+
import type { AppwriteOperations } from "./appwrite.ts";
78
import type { AstroOperations, AstroOptions } from "./astro.ts";
89
import type { BuilderOperations } from "./builder.io.ts";
910
import type { BunnyOperations } from "./bunny.ts";
@@ -38,6 +39,7 @@ import type { VercelOperations, VercelOptions } from "./vercel.ts";
3839
import type { WordPressOperations } from "./wordpress.ts";
3940

4041
export interface ProviderOperations {
42+
appwrite: AppwriteOperations;
4143
astro: AstroOperations;
4244
"builder.io": BuilderOperations;
4345
bunny: BunnyOperations;
@@ -67,6 +69,7 @@ export interface ProviderOperations {
6769
}
6870

6971
export interface ProviderOptions {
72+
appwrite: undefined;
7073
astro: AstroOptions;
7174
"builder.io": undefined;
7275
bunny: undefined;

src/transform.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getProviderForUrl } from "./detect.ts";
2+
import { transform as appwrite } from "./providers/appwrite.ts";
23
import { transform as astro } from "./providers/astro.ts";
34
import { transform as builderio } from "./providers/builder.io.ts";
45
import { transform as bunny } from "./providers/bunny.ts";
@@ -37,6 +38,7 @@ import type {
3738
} from "./providers/types.ts";
3839

3940
const transformerMap: URLTransformerMap = {
41+
appwrite,
4042
astro,
4143
"builder.io": builderio,
4244
bunny,

src/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ export type ImageCdn =
5858
| "imagekit"
5959
| "uploadcare"
6060
| "supabase"
61-
| "hygraph";
61+
| "hygraph"
62+
| "appwrite";
6263

6364
export const SupportedProviders: Record<ImageCdn, string> = {
6465
astro: "Astro image service",
@@ -87,6 +88,7 @@ export const SupportedProviders: Record<ImageCdn, string> = {
8788
uploadcare: "Uploadcare",
8889
vercel: "Vercel",
8990
wordpress: "WordPress",
91+
appwrite: "Appwrite",
9092
} as const;
9193

9294
export type OperationFormatter<T extends Operations = Operations> = (

0 commit comments

Comments
 (0)