Skip to content

Commit 8cd58f8

Browse files
committed
feat: add quran manifest endpoint
GET /v3/quran/manifest returns version metadata, CDN paths, and checksums for the app's image-based Quran reader.
1 parent 8072d53 commit 8cd58f8

5 files changed

Lines changed: 218 additions & 2 deletions

File tree

src/app.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { Elysia } from "elysia";
66
import { athkarModule } from "@/modules/athkar";
77
import { healthModule } from "@/modules/health";
88
import { locationsModule } from "@/modules/locations";
9-
import { mushafModule } from "@/modules/mushaf";
109
import { prayerModule } from "@/modules/prayers";
10+
import { quranModule } from "@/modules/quran";
1111
import { statsModule } from "@/modules/stats";
1212
import { telemetry } from "@/observability/telemetry";
1313
// Plugins
@@ -36,6 +36,10 @@ export const app = new Elysia()
3636
name: "Health",
3737
description: "Health check endpoint",
3838
},
39+
{
40+
name: "Quran",
41+
description: "Quran mushaf manifest and assets",
42+
},
3943
],
4044
},
4145
}),
@@ -49,7 +53,7 @@ export const app = new Elysia()
4953
.use(healthModule)
5054
.use(prayerModule)
5155
.use(locationsModule)
52-
.use(mushafModule)
56+
.use(quranModule)
5357
.use(statsModule),
5458
);
5559

src/modules/quran/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Elysia } from "elysia";
2+
import { QuranManifestResponse } from "@/modules/quran/quran.schemas";
3+
import { QuranService } from "@/modules/quran/quran.service";
4+
5+
export const quranModule = new Elysia({
6+
name: "quranModule",
7+
prefix: "/quran",
8+
detail: {
9+
tags: ["Quran"],
10+
},
11+
}).get("/manifest", () => QuranService.getManifest(), {
12+
response: {
13+
200: QuranManifestResponse,
14+
},
15+
});

src/modules/quran/quran.schemas.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { type Static, t } from "elysia";
2+
3+
const ChecksumsSchema = t.Object({
4+
boundsDb: t.String(),
5+
manifest: t.String(),
6+
});
7+
8+
const PathsSchema = t.Object({
9+
lines: t.String(),
10+
boundsDb: t.String(),
11+
markers: t.String(),
12+
});
13+
14+
const QuranVersionSchema = t.Object({
15+
id: t.String(),
16+
name: t.String(),
17+
totalPages: t.Number(),
18+
linesPerPage: t.Number(),
19+
imageWidth: t.Number(),
20+
imageHeight: t.Number(),
21+
totalSizeMB: t.Number(),
22+
boundsDbSizeMB: t.Number(),
23+
baseUrl: t.String(),
24+
paths: PathsSchema,
25+
markers: t.Array(t.String()),
26+
checksums: ChecksumsSchema,
27+
});
28+
29+
export const QuranManifestResponse = t.Object({
30+
manifestVersion: t.Number(),
31+
versions: t.Array(QuranVersionSchema),
32+
});
33+
34+
export type QuranManifest = Static<typeof QuranManifestResponse>;
35+
export type QuranVersion = Static<typeof QuranVersionSchema>;

src/modules/quran/quran.service.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { env } from "@/config/env";
2+
import type { QuranManifest, QuranVersion } from "./quran.schemas";
3+
4+
const quranBase = `${env.CDN_URL}/quran`;
5+
6+
const MARKERS = ["marker-light", "marker-dark", "marker-sepia", "surah-frame"];
7+
8+
const VERSIONS: QuranVersion[] = [
9+
{
10+
id: "v1",
11+
name: "Madinah Mushaf V1",
12+
totalPages: 604,
13+
linesPerPage: 15,
14+
imageWidth: 1440,
15+
imageHeight: 232,
16+
totalSizeMB: 96,
17+
boundsDbSizeMB: 5,
18+
baseUrl: `${quranBase}/v1`,
19+
paths: {
20+
lines: "/lines/{page}/{line}.png",
21+
boundsDb: "/bounds.db",
22+
markers: "/markers/{name}.png",
23+
},
24+
markers: MARKERS,
25+
checksums: {
26+
boundsDb:
27+
"sha256:58732bcf3d3485df7708908bdabd76ed8c9f0b3e555a7f543936c8a7712a588d",
28+
manifest:
29+
"sha256:4966f017f4ead78ce9faadcb4eb30c684f8d510659783155f7ed20dc607d1998",
30+
},
31+
},
32+
{
33+
id: "v2",
34+
name: "Madinah Mushaf V2",
35+
totalPages: 604,
36+
linesPerPage: 15,
37+
imageWidth: 1440,
38+
imageHeight: 232,
39+
totalSizeMB: 108,
40+
boundsDbSizeMB: 5,
41+
baseUrl: `${quranBase}/v2`,
42+
paths: {
43+
lines: "/lines/{page}/{line}.png",
44+
boundsDb: "/bounds.db",
45+
markers: "/markers/{name}.png",
46+
},
47+
markers: MARKERS,
48+
checksums: {
49+
boundsDb:
50+
"sha256:d3d87b960c51e035a3dc91c0f9ac961567a5dd0197424de5c28ec864091b73be",
51+
manifest:
52+
"sha256:98dca13b9532db85564b10c66073fccd9858c1e3ff92000b0bfcbfc9fd164df8",
53+
},
54+
},
55+
{
56+
id: "v4",
57+
name: "Madinah Mushaf V4",
58+
totalPages: 604,
59+
linesPerPage: 15,
60+
imageWidth: 1440,
61+
imageHeight: 232,
62+
totalSizeMB: 90,
63+
boundsDbSizeMB: 5,
64+
baseUrl: `${quranBase}/v4`,
65+
paths: {
66+
lines: "/lines/{page}/{line}.png",
67+
boundsDb: "/bounds.db",
68+
markers: "/markers/{name}.png",
69+
},
70+
markers: MARKERS,
71+
checksums: {
72+
boundsDb:
73+
"sha256:a277818bd06ed10fe2397c551ca7990a2fabe3203815ab40f448c108c03e86f2",
74+
manifest:
75+
"sha256:c679a0887d2558659668f02f8aea49216b7bb62f0c4d8ef79afab913d59bfb9f",
76+
},
77+
},
78+
];
79+
80+
const MANIFEST: QuranManifest = {
81+
manifestVersion: 1,
82+
versions: VERSIONS,
83+
};
84+
85+
// biome-ignore lint/complexity/noStaticOnlyClass: follows existing service pattern
86+
export abstract class QuranService {
87+
static getManifest(): QuranManifest {
88+
return MANIFEST;
89+
}
90+
}

test/modules/quran.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { Elysia } from "elysia";
3+
import { env } from "@/config/env";
4+
import { quranModule } from "@/modules/quran";
5+
6+
const app = new Elysia().group("/v3", (app) => app.use(quranModule));
7+
8+
describe("GET /v3/quran/manifest", () => {
9+
test("returns 200 with manifest", async () => {
10+
const response = await app.handle(
11+
new Request("http://localhost/v3/quran/manifest"),
12+
);
13+
expect(response.status).toBe(200);
14+
15+
const body = await response.json();
16+
expect(body.manifestVersion).toBe(1);
17+
});
18+
19+
test("returns all versions", async () => {
20+
const response = await app.handle(
21+
new Request("http://localhost/v3/quran/manifest"),
22+
);
23+
const body = await response.json();
24+
25+
expect(body.versions).toBeArray();
26+
expect(body.versions.length).toBeGreaterThanOrEqual(1);
27+
28+
const ids = body.versions.map((v: { id: string }) => v.id);
29+
expect(ids).toContain("v1");
30+
expect(ids).toContain("v2");
31+
expect(ids).toContain("v4");
32+
});
33+
34+
test("each version has required fields", async () => {
35+
const response = await app.handle(
36+
new Request("http://localhost/v3/quran/manifest"),
37+
);
38+
const body = await response.json();
39+
40+
for (const version of body.versions) {
41+
expect(version.id).toBeString();
42+
expect(version.name).toBeString();
43+
expect(version.totalPages).toBeNumber();
44+
expect(version.linesPerPage).toBeNumber();
45+
expect(version.imageWidth).toBeNumber();
46+
expect(version.imageHeight).toBeNumber();
47+
expect(version.totalSizeMB).toBeNumber();
48+
expect(version.boundsDbSizeMB).toBeNumber();
49+
expect(version.baseUrl).toBeString();
50+
expect(version.paths).toBeDefined();
51+
expect(version.paths.lines).toBeString();
52+
expect(version.paths.boundsDb).toBeString();
53+
expect(version.paths.markers).toBeString();
54+
expect(version.markers).toBeArray();
55+
expect(version.checksums).toBeDefined();
56+
expect(version.checksums.boundsDb).toBeString();
57+
expect(version.checksums.manifest).toBeString();
58+
}
59+
});
60+
61+
test("baseUrl uses CDN_URL", async () => {
62+
const response = await app.handle(
63+
new Request("http://localhost/v3/quran/manifest"),
64+
);
65+
const body = await response.json();
66+
67+
for (const version of body.versions) {
68+
expect(version.baseUrl).toStartWith(env.CDN_URL);
69+
expect(version.baseUrl).toContain(`/quran/${version.id}`);
70+
}
71+
});
72+
});

0 commit comments

Comments
 (0)