Files
kovakyazilim/lib/appwrite-rest.ts
T
egecankomur 2e001680bf feat: Çözümler bölümü + mobil menü; admin parser düzeltmeleri
- Çözümler: solutions tablosu, /cozumler liste + detay sayfası, anasayfa
  bölümü, tam admin CRUD (/admin/cozumler), header & footer linkleri,
  projelerde solution_slug ilişkisi, services-grid genelleştirildi
- Mobil menü (hamburger drawer) eklendi — header artık < lg'de gezilebilir
- Site ayarları parser: textarea CRLF (\r\n) normalizasyonu — neden biz,
  süreç adımları, değerler ve SSS blokları artık doğru parçalanıyor
- homepage_faq + garanti (title/description/items) saveSiteSettings'e
  bağlandı (daha önce hiç kaydedilmiyordu)
2026-06-02 18:21:58 +03:00

320 lines
9.1 KiB
TypeScript

import "server-only";
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT!;
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!;
if (!ENDPOINT || !PROJECT_ID) {
throw new Error(
"Missing NEXT_PUBLIC_APPWRITE_ENDPOINT or NEXT_PUBLIC_APPWRITE_PROJECT_ID",
);
}
export const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!;
export const MEDIA_BUCKET_ID =
process.env.NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID ?? "kovak-yazilim-media";
export const TABLES = {
contactMessages: "contact_messages",
services: "services",
solutions: "solutions",
projects: "projects",
blogPosts: "blog_posts",
testimonials: "testimonials",
seoPages: "seo_pages",
seoSettings: "seo_settings",
siteSettings: "site_settings",
teamMembers: "team_members",
industries: "industries",
} as const;
export class AppwriteError extends Error {
code: number;
type?: string;
constructor(message: string, code: number, type?: string) {
super(message);
this.name = "AppwriteError";
this.code = code;
this.type = type;
}
}
type FetchOpts = {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
body?: unknown;
session?: string;
query?: Record<string, string | string[] | number | undefined>;
formData?: FormData;
};
async function awRaw(path: string, opts: FetchOpts = {}): Promise<Response> {
const url = new URL(ENDPOINT + path);
if (opts.query) {
for (const [k, v] of Object.entries(opts.query)) {
if (v === undefined) continue;
if (Array.isArray(v)) v.forEach((x) => url.searchParams.append(k + "[]", String(x)));
else url.searchParams.set(k, String(v));
}
}
const headers: Record<string, string> = {
"X-Appwrite-Project": PROJECT_ID,
"X-Appwrite-Response-Format": "1.9.0",
};
if (opts.session) headers["X-Appwrite-Session"] = opts.session;
let body: BodyInit | undefined;
if (opts.formData) {
body = opts.formData;
} else if (opts.body !== undefined) {
headers["Content-Type"] = "application/json";
body = JSON.stringify(opts.body);
}
// GET requests are cached (ISR-style) with 60s revalidate so public pages
// stay fast for ad traffic; mutations and session calls bypass cache.
const method = opts.method ?? "GET";
const isAuthenticated = !!opts.session;
const fetchInit: RequestInit & { next?: { revalidate?: number; tags?: string[] } } = {
method,
headers,
body,
};
if (method === "GET" && !isAuthenticated) {
fetchInit.next = { revalidate: 60 };
} else {
fetchInit.cache = "no-store";
}
const res = await fetch(url, fetchInit);
if (!res.ok) {
let data: { message?: string; type?: string } = {};
try {
data = await res.json();
} catch {
/* ignore */
}
throw new AppwriteError(
data.message || `HTTP ${res.status}`,
res.status,
data.type,
);
}
return res;
}
async function aw<T>(path: string, opts: FetchOpts = {}): Promise<T> {
const res = await awRaw(path, opts);
const ct = res.headers.get("content-type") ?? "";
if (ct.includes("application/json")) {
return (await res.json()) as T;
}
return undefined as T;
}
function extractSessionFromHeaders(res: Response): string | null {
// Appwrite returns the session secret in X-Fallback-Cookies as JSON,
// or in Set-Cookie header. The response body may have empty/redacted secret.
const fallback = res.headers.get("x-fallback-cookies");
if (fallback) {
try {
const parsed = JSON.parse(fallback) as Record<string, string>;
const first = Object.values(parsed)[0];
if (first) return first;
} catch {
/* ignore */
}
}
const setCookie = res.headers.get("set-cookie");
if (setCookie) {
const m = setCookie.match(/a_session_[^=]+=([^;]+)/);
if (m?.[1]) return decodeURIComponent(m[1]);
}
return null;
}
// ─── Query helpers ───────────────────────────────────────────────
export const Q = {
equal: (attr: string, value: string | number | boolean | string[]) =>
JSON.stringify({
method: "equal",
attribute: attr,
values: Array.isArray(value) ? value : [value],
}),
notEqual: (attr: string, value: string | number) =>
JSON.stringify({ method: "notEqual", attribute: attr, values: [value] }),
orderAsc: (attr: string) =>
JSON.stringify({ method: "orderAsc", attribute: attr }),
orderDesc: (attr: string) =>
JSON.stringify({ method: "orderDesc", attribute: attr }),
limit: (n: number) => JSON.stringify({ method: "limit", values: [n] }),
offset: (n: number) => JSON.stringify({ method: "offset", values: [n] }),
};
// ─── Types ───────────────────────────────────────────────────────
export interface AwRow {
$id: string;
$createdAt: string;
$updatedAt: string;
[k: string]: unknown;
}
export interface AwListResponse<T> {
total: number;
rows: T[];
}
export interface AwFile {
$id: string;
name: string;
sizeOriginal: number;
mimeType: string;
$createdAt: string;
}
export interface AwSession {
$id: string;
secret: string;
expire: string;
userId: string;
provider: string;
}
export interface AwUser {
$id: string;
email: string;
name: string;
status: boolean;
}
// ─── Account ─────────────────────────────────────────────────────
export const account = {
async createEmailPasswordSession(email: string, password: string) {
const res = await awRaw("/account/sessions/email", {
method: "POST",
body: { email, password },
});
const body = (await res.json()) as AwSession;
// Appwrite redacts `secret` in the response body — read it from the
// X-Fallback-Cookies header (or Set-Cookie) where the real secret lives.
const secretFromHeader = extractSessionFromHeaders(res);
return { ...body, secret: secretFromHeader || body.secret };
},
get(session: string) {
return aw<AwUser>("/account", { session });
},
deleteSession(sessionId: string, session: string) {
return aw<void>(`/account/sessions/${sessionId}`, {
method: "DELETE",
session,
});
},
};
// ─── TablesDB ────────────────────────────────────────────────────
export const tablesDB = {
listRows<T = AwRow>(
databaseId: string,
tableId: string,
queries: string[] = [],
session?: string,
) {
return aw<AwListResponse<T>>(
`/tablesdb/${databaseId}/tables/${tableId}/rows`,
{ query: { queries }, session },
);
},
getRow<T = AwRow>(
databaseId: string,
tableId: string,
rowId: string,
session?: string,
) {
return aw<T>(`/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, {
session,
});
},
createRow<T = AwRow>(
databaseId: string,
tableId: string,
rowId: string,
data: Record<string, unknown>,
session?: string,
) {
return aw<T>(`/tablesdb/${databaseId}/tables/${tableId}/rows`, {
method: "POST",
body: { rowId, data },
session,
});
},
updateRow<T = AwRow>(
databaseId: string,
tableId: string,
rowId: string,
data: Record<string, unknown>,
session?: string,
) {
return aw<T>(`/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, {
method: "PATCH",
body: { data },
session,
});
},
deleteRow(
databaseId: string,
tableId: string,
rowId: string,
session?: string,
) {
return aw<void>(`/tablesdb/${databaseId}/tables/${tableId}/rows/${rowId}`, {
method: "DELETE",
session,
});
},
};
// ─── Storage ─────────────────────────────────────────────────────
export const storage = {
listFiles(bucketId: string, queries: string[] = [], session?: string) {
return aw<{ total: number; files: AwFile[] }>(
`/storage/buckets/${bucketId}/files`,
{ query: { queries }, session },
);
},
createFile(
bucketId: string,
fileId: string,
file: File,
session?: string,
) {
const fd = new FormData();
fd.append("fileId", fileId);
fd.append("file", file);
return aw<AwFile>(`/storage/buckets/${bucketId}/files`, {
method: "POST",
formData: fd,
session,
});
},
deleteFile(bucketId: string, fileId: string, session?: string) {
return aw<void>(`/storage/buckets/${bucketId}/files/${fileId}`, {
method: "DELETE",
session,
});
},
fileViewUrl(bucketId: string, fileId: string) {
return `${ENDPOINT}/storage/buckets/${bucketId}/files/${fileId}/view?project=${PROJECT_ID}`;
},
};
// ─── ID generator (Appwrite "unique()") ──────────────────────────
export const ID = {
unique() {
return "unique()";
},
custom(id: string) {
return id;
},
};