import { t as __exportAll } from "./chunk-ClPoSABd.mjs";
import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
import { i as isPostgres, r as currentTimestampValue } from "./dialect-helpers-DhTzaUxP.mjs";
import { n as decodeCursor, r as encodeCursor } from "./types-CMMN0pNg.mjs";
import { getRequestContext } from "./request-context.mjs";
import { Kysely, sql } from "kysely";

//#region src/loader.ts
var loader_exports = /* @__PURE__ */ __exportAll({
	emdashLoader: () => emdashLoader,
	getDb: () => getDb
});
const FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
/**
* System columns that are not part of the content data
*/
/**
* System columns excluded from entry.data
* Note: slug is intentionally NOT excluded - it's useful as data.slug in templates
*/
const SYSTEM_COLUMNS = new Set([
	"id",
	"status",
	"author_id",
	"primary_byline_id",
	"created_at",
	"updated_at",
	"published_at",
	"scheduled_at",
	"deleted_at",
	"version",
	"live_revision_id",
	"draft_revision_id",
	"locale",
	"translation_group"
]);
/**
* Get the table name for a collection type
*/
function getTableName(type) {
	validateIdentifier(type, "collection type");
	return `ec_${type}`;
}
/**
* Cache for taxonomy names (only used for the primary database).
* Skipped when a per-request DB override is active (e.g. preview mode)
* because the override DB may have different taxonomies.
*/
let taxonomyNames = null;
/**
* Get all taxonomy names (cached for primary DB, fresh for overrides)
*/
async function getTaxonomyNames(db) {
	const hasDbOverride = !!getRequestContext()?.db;
	if (!hasDbOverride && taxonomyNames) return taxonomyNames;
	try {
		const defs = await db.selectFrom("_emdash_taxonomy_defs").select("name").execute();
		const names = new Set(defs.map((d) => d.name));
		if (!hasDbOverride) taxonomyNames = names;
		return names;
	} catch {
		const empty = /* @__PURE__ */ new Set();
		if (!hasDbOverride) taxonomyNames = empty;
		return empty;
	}
}
/**
* System columns to include in data (mapped to camelCase where needed)
*/
const INCLUDE_IN_DATA = {
	id: "id",
	status: "status",
	author_id: "authorId",
	primary_byline_id: "primaryBylineId",
	created_at: "createdAt",
	updated_at: "updatedAt",
	published_at: "publishedAt",
	scheduled_at: "scheduledAt",
	draft_revision_id: "draftRevisionId",
	live_revision_id: "liveRevisionId",
	locale: "locale",
	translation_group: "translationGroup"
};
/** System date columns that should be converted to Date objects */
const DATE_COLUMNS = new Set([
	"created_at",
	"updated_at",
	"published_at",
	"scheduled_at"
]);
/** Safely extract a string value from a record, returning fallback if not a string */
function rowStr(row, key, fallback = "") {
	const val = row[key];
	return typeof val === "string" ? val : fallback;
}
/**
* Map a database row to entry data
* Extracts content fields (non-system columns) and parses JSON where needed.
* System columns needed for templates (id, status, dates) are included with camelCase names.
*/
function mapRowToData(row) {
	const data = {};
	for (const [key, value] of Object.entries(row)) {
		if (key in INCLUDE_IN_DATA) {
			if (DATE_COLUMNS.has(key)) data[INCLUDE_IN_DATA[key]] = typeof value === "string" ? new Date(value) : null;
			else data[INCLUDE_IN_DATA[key]] = value;
			continue;
		}
		if (SYSTEM_COLUMNS.has(key)) continue;
		if (typeof value === "string") try {
			if (value.startsWith("{") || value.startsWith("[")) data[key] = JSON.parse(value);
			else data[key] = value;
		} catch {
			data[key] = value;
		}
		else data[key] = value;
	}
	return data;
}
/**
* Map revision data (already-parsed JSON object) to entry data.
* Strips _-prefixed metadata keys (e.g. _slug) used internally by revisions.
*/
function mapRevisionData(data) {
	const result = {};
	for (const [key, value] of Object.entries(data)) {
		if (key.startsWith("_")) continue;
		result[key] = value;
	}
	return result;
}
let virtualConfig;
let virtualCreateDialect;
async function loadVirtualModules() {
	if (virtualConfig === void 0) virtualConfig = (await import("virtual:emdash/config")).default;
	if (virtualCreateDialect === void 0) virtualCreateDialect = (await import("virtual:emdash/dialect")).createDialect;
}
/**
* Build WHERE clause for status filtering.
* When filtering for 'published' status, also include scheduled content
* whose scheduled_at time has passed (treating it as effectively published).
*/
function buildStatusCondition(db, status, tablePrefix) {
	const statusField = tablePrefix ? `${tablePrefix}.status` : "status";
	const scheduledAtField = tablePrefix ? `${tablePrefix}.scheduled_at` : "scheduled_at";
	if (status === "published") {
		const scheduledAtExpr = isPostgres(db) ? sql`${sql.ref(scheduledAtField)}::timestamptz` : sql.ref(scheduledAtField);
		return sql`(${sql.ref(statusField)} = 'published' OR (${sql.ref(statusField)} = 'scheduled' AND ${scheduledAtExpr} <= ${currentTimestampValue(db)}))`;
	}
	return sql`${sql.ref(statusField)} = ${status}`;
}
/**
* Get the primary sort field from an orderBy spec (first valid field, or default).
*/
function getPrimarySort(orderBy, tablePrefix) {
	if (orderBy) {
		for (const [field, direction] of Object.entries(orderBy)) if (FIELD_NAME_PATTERN.test(field)) return {
			field: tablePrefix ? `${tablePrefix}.${field}` : field,
			direction
		};
	}
	return {
		field: tablePrefix ? `${tablePrefix}.created_at` : "created_at",
		direction: "desc"
	};
}
/**
* Build ORDER BY clause from orderBy spec
* Validates field names to prevent SQL injection (alphanumeric + underscore only)
* Supports multiple sort fields in object key order
*/
function buildOrderByClause(orderBy, tablePrefix) {
	if (!orderBy || Object.keys(orderBy).length === 0) {
		const field = tablePrefix ? `${tablePrefix}.created_at` : "created_at";
		return sql`ORDER BY ${sql.ref(field)} DESC, ${sql.ref(tablePrefix ? `${tablePrefix}.id` : "id")} DESC`;
	}
	const sortParts = [];
	for (const [field, direction] of Object.entries(orderBy)) {
		if (!FIELD_NAME_PATTERN.test(field)) continue;
		const fullField = tablePrefix ? `${tablePrefix}.${field}` : field;
		const dir = direction === "asc" ? sql`ASC` : sql`DESC`;
		sortParts.push(sql`${sql.ref(fullField)} ${dir}`);
	}
	if (sortParts.length === 0) {
		const defaultField = tablePrefix ? `${tablePrefix}.created_at` : "created_at";
		return sql`ORDER BY ${sql.ref(defaultField)} DESC, ${sql.ref(tablePrefix ? `${tablePrefix}.id` : "id")} DESC`;
	}
	const primary = getPrimarySort(orderBy, tablePrefix);
	const idField = tablePrefix ? `${tablePrefix}.id` : "id";
	const idDir = primary.direction === "asc" ? sql`ASC` : sql`DESC`;
	sortParts.push(sql`${sql.ref(idField)} ${idDir}`);
	return sql`ORDER BY ${sql.join(sortParts, sql`, `)}`;
}
/**
* Build a cursor WHERE condition for keyset pagination.
* Uses the primary sort field + id as tiebreaker for stable ordering.
*/
function buildCursorCondition(cursor, orderBy, tablePrefix) {
	const decoded = decodeCursor(cursor);
	if (!decoded) return null;
	const { orderValue, id: cursorId } = decoded;
	const primary = getPrimarySort(orderBy, tablePrefix);
	const idField = tablePrefix ? `${tablePrefix}.id` : "id";
	if (primary.direction === "desc") return sql`(${sql.ref(primary.field)} < ${orderValue} OR (${sql.ref(primary.field)} = ${orderValue} AND ${sql.ref(idField)} < ${cursorId}))`;
	return sql`(${sql.ref(primary.field)} > ${orderValue} OR (${sql.ref(primary.field)} = ${orderValue} AND ${sql.ref(idField)} > ${cursorId}))`;
}
let dbInstance = null;
/**
* Get the database instance. Used by query wrapper functions and middleware.
*
* Checks the ALS request context first — if a per-request DB override is set
* (e.g. by DO preview middleware), it takes precedence over the module-level
* cached instance. This allows preview mode to route queries to an isolated
* Durable Object database without modifying any calling code.
*
* Initializes the default database on first call using config from virtual module.
*/
async function getDb() {
	const ctx = getRequestContext();
	if (ctx?.db) return ctx.db;
	if (!dbInstance) {
		await loadVirtualModules();
		if (!virtualConfig?.database || typeof virtualCreateDialect !== "function") throw new Error("EmDash database not configured. Add database config to emdash() in astro.config.mjs");
		dbInstance = new Kysely({ dialect: virtualCreateDialect(virtualConfig.database.config) });
	}
	return dbInstance;
}
/**
* Create an EmDash Live Collections loader
*
* This loader handles ALL content types in a single Astro collection.
* Use `getEmDashCollection()` and `getEmDashEntry()` to query
* specific content types.
*
* Database is configured in astro.config.mjs via the emdash() integration.
*
* @example
* ```ts
* // src/live.config.ts
* import { defineLiveCollection } from "astro:content";
* import { emdashLoader } from "emdash";
*
* export const collections = {
*   emdash: defineLiveCollection({
*     loader: emdashLoader(),
*   }),
* };
* ```
*/
function emdashLoader() {
	return {
		name: "emdash",
		async loadCollection({ filter }) {
			try {
				const db = await getDb();
				const type = filter?.type;
				if (!type) return { error: /* @__PURE__ */ new Error("type filter is required. Use getEmDashCollection() instead of getLiveCollection() directly.") };
				const tableName = getTableName(type);
				const status = filter?.status || "published";
				const limit = filter?.limit;
				const cursor = filter?.cursor;
				const where = filter?.where;
				const orderBy = filter?.orderBy;
				const locale = filter?.locale;
				const fetchLimit = limit ? limit + 1 : void 0;
				const cursorCondition = cursor ? buildCursorCondition(cursor, orderBy) : null;
				const cursorConditionPrefixed = cursor ? buildCursorCondition(cursor, orderBy, tableName) : null;
				let result;
				if (where && Object.keys(where).length > 0) {
					const taxNames = await getTaxonomyNames(db);
					const taxonomyFilters = {};
					for (const [key, value] of Object.entries(where)) if (taxNames.has(key)) taxonomyFilters[key] = value;
					if (Object.keys(taxonomyFilters).length > 0) {
						const [taxName, termSlugs] = Object.entries(taxonomyFilters)[0];
						const slugs = Array.isArray(termSlugs) ? termSlugs : [termSlugs];
						const orderByClause = buildOrderByClause(orderBy, tableName);
						const statusCondition = buildStatusCondition(db, status, tableName);
						const localeCondition = locale ? sql`AND ${sql.ref(tableName)}.locale = ${locale}` : sql``;
						const cursorCond = cursorConditionPrefixed ? sql`AND ${cursorConditionPrefixed}` : sql``;
						result = await sql`
							SELECT DISTINCT ${sql.ref(tableName)}.* FROM ${sql.ref(tableName)}
							INNER JOIN content_taxonomies ct
								ON ct.collection = ${type}
								AND ct.entry_id = ${sql.ref(tableName)}.id
							INNER JOIN taxonomies t
								ON t.id = ct.taxonomy_id
							WHERE ${sql.ref(tableName)}.deleted_at IS NULL
								AND ${statusCondition}
								${localeCondition}
								${cursorCond}
								AND t.name = ${taxName}
								AND t.slug IN (${sql.join(slugs.map((s) => sql`${s}`))})
							${orderByClause}
							${fetchLimit ? sql`LIMIT ${fetchLimit}` : sql``}
						`.execute(db);
					} else {
						const orderByClause = buildOrderByClause(orderBy);
						const statusCondition = buildStatusCondition(db, status);
						const localeFilter = locale ? sql`AND locale = ${locale}` : sql``;
						const cursorCond = cursorCondition ? sql`AND ${cursorCondition}` : sql``;
						result = await sql`
							SELECT * FROM ${sql.ref(tableName)}
							WHERE deleted_at IS NULL
							AND ${statusCondition}
							${localeFilter}
							${cursorCond}
							${orderByClause}
							${fetchLimit ? sql`LIMIT ${fetchLimit}` : sql``}
						`.execute(db);
					}
				} else {
					const orderByClause = buildOrderByClause(orderBy);
					const statusCondition = buildStatusCondition(db, status);
					const localeFilter = locale ? sql`AND locale = ${locale}` : sql``;
					const cursorCond = cursorCondition ? sql`AND ${cursorCondition}` : sql``;
					result = await sql`
						SELECT * FROM ${sql.ref(tableName)}
						WHERE deleted_at IS NULL
						AND ${statusCondition}
						${localeFilter}
						${cursorCond}
						${orderByClause}
						${fetchLimit ? sql`LIMIT ${fetchLimit}` : sql``}
					`.execute(db);
				}
				const hasMore = limit ? result.rows.length > limit : false;
				const rows = hasMore ? result.rows.slice(0, limit) : result.rows;
				const i18nConfig = virtualConfig?.i18n;
				const i18nEnabled = i18nConfig && i18nConfig.locales.length > 1;
				const entries = rows.map((row) => {
					const slug = rowStr(row, "slug") || rowStr(row, "id");
					const rowLocale = rowStr(row, "locale");
					return {
						id: i18nEnabled && rowLocale !== "" && (rowLocale !== i18nConfig.defaultLocale || i18nConfig.prefixDefaultLocale) ? `${rowLocale}/${slug}` : slug,
						slug: rowStr(row, "slug"),
						status: rowStr(row, "status", "draft"),
						data: mapRowToData(row),
						cacheHint: {
							tags: [rowStr(row, "id")],
							lastModified: row.updated_at ? new Date(rowStr(row, "updated_at")) : void 0
						}
					};
				});
				let nextCursor;
				if (hasMore && rows.length > 0) {
					const lastRow = rows.at(-1);
					const primary = getPrimarySort(orderBy);
					const lastOrderValue = lastRow[primary.field.includes(".") ? primary.field.split(".").pop() : primary.field];
					nextCursor = encodeCursor(typeof lastOrderValue === "string" || typeof lastOrderValue === "number" ? String(lastOrderValue) : "", String(lastRow.id));
				}
				let collectionLastModified;
				for (const row of rows) if (row.updated_at) {
					const d = new Date(rowStr(row, "updated_at"));
					if (!collectionLastModified || d > collectionLastModified) collectionLastModified = d;
				}
				return {
					entries,
					nextCursor,
					cacheHint: {
						tags: [type],
						lastModified: collectionLastModified
					}
				};
			} catch (error) {
				const message = error instanceof Error ? error.message : String(error);
				const lowerMessage = message.toLowerCase();
				if (lowerMessage.includes("no such table") || lowerMessage.includes("table") && lowerMessage.includes("does not exist") || lowerMessage.includes("relation") && lowerMessage.includes("does not exist")) return { entries: [] };
				return { error: /* @__PURE__ */ new Error(`Failed to load collection: ${message}`) };
			}
		},
		async loadEntry({ filter }) {
			try {
				const db = await getDb();
				const type = filter?.type;
				const id = filter?.id;
				if (!type || !id) return { error: /* @__PURE__ */ new Error("type and id filters are required. Use getEmDashEntry() instead of getLiveEntry() directly.") };
				const tableName = getTableName(type);
				const locale = filter?.locale;
				const row = (locale ? await sql`
							SELECT * FROM ${sql.ref(tableName)}
							WHERE deleted_at IS NULL
							AND ((slug = ${id} AND locale = ${locale}) OR id = ${id})
							LIMIT 1
						`.execute(db) : await sql`
							SELECT * FROM ${sql.ref(tableName)}
							WHERE deleted_at IS NULL
							AND (slug = ${id} OR id = ${id})
							LIMIT 1
						`.execute(db)).rows[0];
				if (!row) return;
				const i18nConfig = virtualConfig?.i18n;
				const i18nEnabled = i18nConfig && i18nConfig.locales.length > 1;
				const entrySlug = rowStr(row, "slug") || rowStr(row, "id");
				const entryLocale = rowStr(row, "locale");
				const entryId = i18nEnabled && entryLocale !== "" && (entryLocale !== i18nConfig.defaultLocale || i18nConfig.prefixDefaultLocale) ? `${entryLocale}/${entrySlug}` : entrySlug;
				const revisionId = filter?.revisionId;
				if (revisionId) {
					const revData = (await sql`
						SELECT data FROM revisions
						WHERE id = ${revisionId}
						LIMIT 1
					`.execute(db)).rows[0];
					if (revData) {
						const parsed = JSON.parse(revData.data);
						const systemData = {};
						for (const [key, mappedKey] of Object.entries(INCLUDE_IN_DATA)) if (key in row) if (DATE_COLUMNS.has(key)) systemData[mappedKey] = typeof row[key] === "string" ? new Date(row[key]) : null;
						else systemData[mappedKey] = row[key];
						const slug = typeof parsed._slug === "string" ? parsed._slug : rowStr(row, "slug");
						const revSlug = slug || rowStr(row, "id");
						const revLocale = rowStr(row, "locale");
						return {
							id: i18nEnabled && revLocale !== "" && (revLocale !== i18nConfig.defaultLocale || i18nConfig.prefixDefaultLocale) ? `${revLocale}/${revSlug}` : revSlug,
							slug,
							status: rowStr(row, "status", "draft"),
							data: {
								...systemData,
								slug,
								...mapRevisionData(parsed)
							},
							cacheHint: {
								tags: [rowStr(row, "id")],
								lastModified: row.updated_at ? new Date(rowStr(row, "updated_at")) : void 0
							}
						};
					}
				}
				return {
					id: entryId,
					slug: rowStr(row, "slug"),
					status: rowStr(row, "status", "draft"),
					data: mapRowToData(row),
					cacheHint: {
						tags: [rowStr(row, "id")],
						lastModified: row.updated_at ? new Date(rowStr(row, "updated_at")) : void 0
					}
				};
			} catch (error) {
				const message = error instanceof Error ? error.message : String(error);
				const lowerMessage = message.toLowerCase();
				if (lowerMessage.includes("no such table") || lowerMessage.includes("table") && lowerMessage.includes("does not exist") || lowerMessage.includes("relation") && lowerMessage.includes("does not exist")) return;
				return { error: /* @__PURE__ */ new Error(`Failed to load entry: ${message}`) };
			}
		}
	};
}

//#endregion
export { getDb as n, loader_exports as r, emdashLoader as t };
//# sourceMappingURL=loader-BYzwzORf.mjs.map