- Project settings gain siteAddress, workStage, projectLead and projectEditor (project.update-settings schema version 3, migration 0009); versions 2 and 1 stay executable and keep the current cover values. Snapshot schema 6 carries the fields; older versions upgrade with null. - Global office profile (office name/address) next to the document logo: table document_branding_settings, PUT /api/document-branding/office, editable from the print view toolbar. - Cover page reads all fields; empty values show the "[ergänzen]" marker. The Planstände editor column shows the project editor. - Shared logo constants moved to a dependency-free module so the Next.js bundler can import them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
2.5 KiB
TypeScript
86 lines
2.5 KiB
TypeScript
import { eq, inArray } from "drizzle-orm";
|
|
import type { AppDatabase } from "../database-context.js";
|
|
import {
|
|
documentBrandingAssets,
|
|
type DocumentBrandingAsset,
|
|
} from "../schema/document-branding-assets.js";
|
|
import { documentBrandingSettings } from "../schema/document-branding-settings.js";
|
|
|
|
// Plain repository without revision semantics: branding is global office
|
|
// configuration, not a project mutation.
|
|
export class DocumentBrandingRepository {
|
|
constructor(private readonly db: AppDatabase) {}
|
|
|
|
get(key: string): DocumentBrandingAsset | undefined {
|
|
return this.db
|
|
.select()
|
|
.from(documentBrandingAssets)
|
|
.where(eq(documentBrandingAssets.key, key))
|
|
.get();
|
|
}
|
|
|
|
upsert(asset: {
|
|
key: string;
|
|
mimeType: string;
|
|
fileName: string | null;
|
|
data: Buffer;
|
|
}): DocumentBrandingAsset {
|
|
const updatedAt = new Date();
|
|
return this.db
|
|
.insert(documentBrandingAssets)
|
|
.values({ ...asset, updatedAt })
|
|
.onConflictDoUpdate({
|
|
target: documentBrandingAssets.key,
|
|
set: {
|
|
mimeType: asset.mimeType,
|
|
fileName: asset.fileName,
|
|
data: asset.data,
|
|
updatedAt,
|
|
},
|
|
})
|
|
.returning()
|
|
.get();
|
|
}
|
|
|
|
delete(key: string): boolean {
|
|
return (
|
|
this.db
|
|
.delete(documentBrandingAssets)
|
|
.where(eq(documentBrandingAssets.key, key))
|
|
.run().changes > 0
|
|
);
|
|
}
|
|
|
|
getSettings<TKey extends string>(keys: readonly TKey[]): Record<TKey, string | null> {
|
|
const rows = this.db
|
|
.select()
|
|
.from(documentBrandingSettings)
|
|
.where(inArray(documentBrandingSettings.key, [...keys]))
|
|
.all();
|
|
return Object.fromEntries(
|
|
keys.map((key) => [key, rows.find((row) => row.key === key)?.value ?? null])
|
|
) as Record<TKey, string | null>;
|
|
}
|
|
|
|
/** Stores the given values atomically; `null` removes a setting. */
|
|
setSettings(values: Record<string, string | null>) {
|
|
const updatedAt = new Date();
|
|
this.db.transaction((tx) => {
|
|
for (const [key, value] of Object.entries(values)) {
|
|
if (value === null) {
|
|
tx.delete(documentBrandingSettings)
|
|
.where(eq(documentBrandingSettings.key, key))
|
|
.run();
|
|
} else {
|
|
tx.insert(documentBrandingSettings)
|
|
.values({ key, value, updatedAt })
|
|
.onConflictDoUpdate({
|
|
target: documentBrandingSettings.key,
|
|
set: { value, updatedAt },
|
|
})
|
|
.run();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|