feat: add admin accounts and image templates
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { readFile, rename, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
|
||||
import type { AppState, Asset, GenerationCapability, GenerationJob, GenerationStatus, Project, UsageEvent } from "@/lib/types";
|
||||
import type { AppState, Asset, GenerationCapability, GenerationJob, GenerationStatus, ImageTemplate, Project, UsageEvent } from "@/lib/types";
|
||||
import { createId } from "@/lib/server/ids";
|
||||
import { dataDir, DEFAULT_OWNER_ID, ensureRuntimeDirs } from "@/lib/server/runtime";
|
||||
|
||||
@@ -11,6 +11,7 @@ let localWriteQueue: Promise<unknown> = Promise.resolve();
|
||||
type AssetInput = Omit<Asset, "id" | "createdAt" | "updatedAt"> & Partial<Pick<Asset, "id" | "createdAt" | "updatedAt">>;
|
||||
type JobInput = Omit<GenerationJob, "id" | "createdAt" | "updatedAt"> & Partial<Pick<GenerationJob, "id" | "createdAt" | "updatedAt">>;
|
||||
type UsageInput = Omit<UsageEvent, "id" | "createdAt"> & Partial<Pick<UsageEvent, "id" | "createdAt">>;
|
||||
type ImageTemplateInput = Omit<ImageTemplate, "id" | "createdAt" | "updatedAt"> & Partial<Pick<ImageTemplate, "id" | "createdAt" | "updatedAt">>;
|
||||
|
||||
export type GenerationJobListFilters = {
|
||||
ownerId?: string;
|
||||
@@ -339,6 +340,108 @@ export async function listProjects(ownerId = DEFAULT_OWNER_ID): Promise<Project[
|
||||
return state.projects.filter((project) => project.ownerId === ownerId).sort(sortNewest);
|
||||
}
|
||||
|
||||
export async function listImageTemplates(ownerId = DEFAULT_OWNER_ID): Promise<ImageTemplate[]> {
|
||||
const supabase = getSupabaseAdmin();
|
||||
if (supabase) {
|
||||
const { data, error } = await supabase
|
||||
.from("image_templates")
|
||||
.select("*")
|
||||
.eq("owner_id", ownerId)
|
||||
.order("sort_order", { ascending: true })
|
||||
.order("updated_at", { ascending: false });
|
||||
if (error) throw new Error(error.message);
|
||||
return (data || []).map(imageTemplateFromRow);
|
||||
}
|
||||
const state = await readState();
|
||||
return state.imageTemplates
|
||||
.filter((template) => template.ownerId === ownerId)
|
||||
.sort(sortTemplates);
|
||||
}
|
||||
|
||||
export async function getImageTemplate(id: string): Promise<ImageTemplate | null> {
|
||||
const supabase = getSupabaseAdmin();
|
||||
if (supabase) {
|
||||
const { data, error } = await supabase.from("image_templates").select("*").eq("id", id).maybeSingle();
|
||||
if (error) throw new Error(error.message);
|
||||
return data ? imageTemplateFromRow(data) : null;
|
||||
}
|
||||
const state = await readState();
|
||||
return state.imageTemplates.find((template) => template.id === id) || null;
|
||||
}
|
||||
|
||||
export async function createImageTemplate(input: ImageTemplateInput): Promise<ImageTemplate> {
|
||||
const now = new Date().toISOString();
|
||||
const template: ImageTemplate = {
|
||||
...input,
|
||||
id: input.id || createId("tmpl"),
|
||||
ownerId: input.ownerId || DEFAULT_OWNER_ID,
|
||||
settings: input.settings || {},
|
||||
sortOrder: input.sortOrder ?? 0,
|
||||
createdAt: input.createdAt || now,
|
||||
updatedAt: input.updatedAt || now
|
||||
};
|
||||
const supabase = getSupabaseAdmin();
|
||||
if (supabase) {
|
||||
const { data, error } = await supabase.from("image_templates").insert(imageTemplateToRow(template)).select("*").single();
|
||||
if (error) throw new Error(error.message);
|
||||
return imageTemplateFromRow(data);
|
||||
}
|
||||
return mutateLocalState((state) => {
|
||||
state.imageTemplates.unshift(template);
|
||||
state.imageTemplates.sort(sortTemplates);
|
||||
return template;
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateImageTemplate(
|
||||
id: string,
|
||||
ownerId: string,
|
||||
patch: Partial<Omit<ImageTemplate, "id" | "ownerId" | "createdAt" | "updatedAt">>
|
||||
): Promise<ImageTemplate | null> {
|
||||
const existing = await getImageTemplate(id);
|
||||
if (!existing || existing.ownerId !== ownerId) return null;
|
||||
const updated: ImageTemplate = {
|
||||
...existing,
|
||||
...patch,
|
||||
settings: patch.settings || existing.settings,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
const supabase = getSupabaseAdmin();
|
||||
if (supabase) {
|
||||
const { data, error } = await supabase
|
||||
.from("image_templates")
|
||||
.update(imageTemplateToRow(updated))
|
||||
.eq("id", id)
|
||||
.eq("owner_id", ownerId)
|
||||
.select("*")
|
||||
.maybeSingle();
|
||||
if (error) throw new Error(error.message);
|
||||
return data ? imageTemplateFromRow(data) : null;
|
||||
}
|
||||
return mutateLocalState((state) => {
|
||||
const index = state.imageTemplates.findIndex((template) => template.id === id && template.ownerId === ownerId);
|
||||
if (index === -1) return null;
|
||||
state.imageTemplates[index] = updated;
|
||||
state.imageTemplates.sort(sortTemplates);
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteImageTemplate(id: string, ownerId: string): Promise<ImageTemplate | null> {
|
||||
const existing = await getImageTemplate(id);
|
||||
if (!existing || existing.ownerId !== ownerId) return null;
|
||||
const supabase = getSupabaseAdmin();
|
||||
if (supabase) {
|
||||
const { error } = await supabase.from("image_templates").delete().eq("id", id).eq("owner_id", ownerId);
|
||||
if (error) throw new Error(error.message);
|
||||
return existing;
|
||||
}
|
||||
return mutateLocalState((state) => {
|
||||
state.imageTemplates = state.imageTemplates.filter((template) => !(template.id === id && template.ownerId === ownerId));
|
||||
return existing;
|
||||
});
|
||||
}
|
||||
|
||||
async function readState(): Promise<AppState> {
|
||||
await ensureRuntimeDirs();
|
||||
const path = join(dataDir(), STORE_FILE);
|
||||
@@ -378,7 +481,8 @@ function normalizeState(raw: Partial<AppState>): AppState {
|
||||
assets: raw.assets || [],
|
||||
generationJobs: raw.generationJobs || [],
|
||||
usageEvents: raw.usageEvents || [],
|
||||
projects: raw.projects || []
|
||||
projects: raw.projects || [],
|
||||
imageTemplates: raw.imageTemplates || []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -395,6 +499,12 @@ function sortNewest<T extends { createdAt: string }>(a: T, b: T): number {
|
||||
return b.createdAt.localeCompare(a.createdAt);
|
||||
}
|
||||
|
||||
function sortTemplates(a: ImageTemplate, b: ImageTemplate): number {
|
||||
const sortOrder = (a.sortOrder || 0) - (b.sortOrder || 0);
|
||||
if (sortOrder !== 0) return sortOrder;
|
||||
return b.updatedAt.localeCompare(a.updatedAt);
|
||||
}
|
||||
|
||||
function isClaimableJob(job: GenerationJob, nowIso: string, staleBefore: string): boolean {
|
||||
if (["succeeded", "failed", "expired", "cancelled"].includes(job.status)) return false;
|
||||
if ((job.scheduledAt || job.createdAt) > nowIso) return false;
|
||||
@@ -545,6 +655,43 @@ function usageFromRow(row: Record<string, unknown>): UsageEvent {
|
||||
};
|
||||
}
|
||||
|
||||
function imageTemplateToRow(template: Partial<ImageTemplate>) {
|
||||
const row: Record<string, unknown> = {};
|
||||
if (template.id !== undefined) row.id = template.id;
|
||||
if (template.ownerId !== undefined) row.owner_id = template.ownerId;
|
||||
if (template.name !== undefined) row.name = template.name;
|
||||
if (template.description !== undefined) row.description = template.description || null;
|
||||
if (template.prompt !== undefined) row.prompt = template.prompt;
|
||||
if (template.previewImageUrl !== undefined) row.preview_image_url = template.previewImageUrl || null;
|
||||
if (template.settings !== undefined) row.settings = template.settings;
|
||||
if (template.sortOrder !== undefined) row.sort_order = template.sortOrder;
|
||||
if (template.createdAt !== undefined) row.created_at = template.createdAt;
|
||||
if (template.updatedAt !== undefined) row.updated_at = template.updatedAt;
|
||||
return row;
|
||||
}
|
||||
|
||||
function imageTemplateFromRow(row: Record<string, unknown>): ImageTemplate {
|
||||
return {
|
||||
id: String(row.id),
|
||||
ownerId: String(row.owner_id),
|
||||
name: String(row.name || ""),
|
||||
description: optionalString(row.description),
|
||||
prompt: String(row.prompt || ""),
|
||||
previewImageUrl: optionalString(row.preview_image_url),
|
||||
settings: isRecord(row.settings) ? {
|
||||
engine: row.settings.engine === "jimeng" || row.settings.engine === "evolink" ? row.settings.engine : undefined,
|
||||
width: optionalNumber(row.settings.width),
|
||||
height: optionalNumber(row.settings.height),
|
||||
forceSingle: typeof row.settings.forceSingle === "boolean" ? row.settings.forceSingle : undefined,
|
||||
scale: optionalNumber(row.settings.scale),
|
||||
quality: row.settings.quality === "low" || row.settings.quality === "medium" || row.settings.quality === "high" ? row.settings.quality : undefined
|
||||
} : {},
|
||||
sortOrder: optionalNumber(row.sort_order) ?? 0,
|
||||
createdAt: String(row.created_at),
|
||||
updatedAt: String(row.updated_at)
|
||||
};
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user