Skip to content

Commit 6ab3833

Browse files
authored
feat: Task 1D — engineConfig singleton + mediaAsset + automatedVideo v2
Consolidates 6+1 config singletons into single engineConfig singleton. Adds mediaAsset document type for shared media (logos, screenshots, etc). Extends automatedVideo with v2 fields (quality gate, review, shorts, thumbnails, social posts, workflow tracking). Fixes applied: - Legacy v1 status values preserved with 'Legacy:' prefix for backward compat - URL fields use string type (R2/CDN paths may include query params) - Real backward compat wrappers for getConfig/getConfigValue (not just alias) PR #665
1 parent c717b80 commit 6ab3833

7 files changed

Lines changed: 29269 additions & 182 deletions

File tree

lib/config.ts

Lines changed: 52 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,76 @@
11
import { writeClient } from "@/lib/sanity-write-client";
2-
import type { ConfigTable, ConfigTypeMap } from "@/lib/types/config";
2+
import type { EngineConfig } from "@/lib/types/engine-config";
33

4-
/**
5-
* Sanity config module with in-memory caching.
6-
*
7-
* Each config "table" maps to a Sanity singleton document type.
8-
* Uses writeClient.fetch for server-side reads.
9-
*
10-
* Caching: 5-minute TTL with stale-while-revalidate.
11-
* Sanity changes propagate on next cache miss.
12-
*/
4+
const DEFAULT_TTL_MS = 5 * 60 * 1000;
135

14-
const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
15-
16-
interface CacheEntry<T> {
17-
data: T;
6+
interface CacheEntry {
7+
data: EngineConfig;
188
fetchedAt: number;
199
refreshing: boolean;
2010
}
2111

22-
const cache = new Map<string, CacheEntry<unknown>>();
23-
24-
// Map config table names to Sanity document type names
25-
const TABLE_TO_TYPE: Record<ConfigTable, string> = {
26-
pipeline_config: "pipelineConfig",
27-
remotion_config: "remotionConfig",
28-
content_config: "contentConfig",
29-
sponsor_config: "sponsorConfig",
30-
distribution_config: "distributionConfig",
31-
gcs_config: "gcsConfig",
32-
};
12+
let cache: CacheEntry | null = null;
3313

34-
async function refreshConfig<T extends ConfigTable>(
35-
table: T,
36-
): Promise<ConfigTypeMap[T]> {
37-
const sanityType = TABLE_TO_TYPE[table];
38-
const data = await writeClient.fetch(
39-
`*[_type == $type][0]`,
40-
{ type: sanityType } as Record<string, unknown>,
14+
async function refreshConfig(): Promise<EngineConfig> {
15+
const data = await writeClient.fetch<EngineConfig>(
16+
`*[_type == "engineConfig"][0]`
4117
);
42-
4318
if (!data) {
44-
throw new Error(`Config not found for ${sanityType} — create the singleton document in Sanity Studio`);
19+
throw new Error(
20+
"engineConfig singleton not found — create it in Sanity Studio"
21+
);
4522
}
46-
47-
cache.set(table, {
48-
data,
49-
fetchedAt: Date.now(),
50-
refreshing: false,
51-
});
52-
53-
return data as ConfigTypeMap[T];
23+
cache = { data, fetchedAt: Date.now(), refreshing: false };
24+
return data;
5425
}
5526

56-
export async function getConfig<T extends ConfigTable>(
57-
table: T,
58-
ttlMs = DEFAULT_TTL_MS,
59-
): Promise<ConfigTypeMap[T]> {
60-
const cached = cache.get(table) as CacheEntry<ConfigTypeMap[T]> | undefined;
27+
export async function getEngineConfig(
28+
ttlMs = DEFAULT_TTL_MS
29+
): Promise<EngineConfig> {
6130
const now = Date.now();
62-
63-
// Fresh cache — return immediately
64-
if (cached && now - cached.fetchedAt < ttlMs) {
65-
return cached.data;
66-
}
67-
68-
// Stale cache — return stale, refresh in background
69-
if (cached && !cached.refreshing) {
70-
cached.refreshing = true;
71-
refreshConfig(table).catch((err) => {
72-
console.error(`[config] Background refresh failed for ${table}:`, err);
73-
const entry = cache.get(table) as CacheEntry<unknown> | undefined;
74-
if (entry) entry.refreshing = false;
31+
if (cache && now - cache.fetchedAt < ttlMs) return cache.data;
32+
if (cache && !cache.refreshing) {
33+
cache.refreshing = true;
34+
refreshConfig().catch((err) => {
35+
console.error("[config] Background refresh failed:", err);
36+
if (cache) cache.refreshing = false;
7537
});
76-
return cached.data;
38+
return cache.data;
7739
}
78-
79-
// No cache — must fetch synchronously
80-
return refreshConfig(table);
40+
return refreshConfig();
8141
}
8242

83-
/**
84-
* Get a single config value with optional env var fallback.
85-
* Useful during migration period.
86-
*/
87-
export async function getConfigValue<
88-
T extends ConfigTable,
89-
K extends keyof ConfigTypeMap[T],
90-
>(
91-
table: T,
43+
export function getEngineConfigValue<K extends keyof EngineConfig>(
44+
config: EngineConfig,
9245
key: K,
93-
fallback?: ConfigTypeMap[T][K],
94-
): Promise<ConfigTypeMap[T][K]> {
95-
try {
96-
const config = await getConfig(table);
97-
const value = config[key];
98-
// Use fallback when field is undefined/null (not yet set in Sanity)
99-
if (value === undefined || value === null) {
100-
if (fallback !== undefined) return fallback;
101-
}
102-
return value;
103-
} catch {
46+
fallback?: EngineConfig[K]
47+
): EngineConfig[K] {
48+
const value = config[key];
49+
if (value === undefined || value === null) {
10450
if (fallback !== undefined) return fallback;
105-
throw new Error(`Config value ${String(key)} not found in ${table}`);
10651
}
52+
return value;
10753
}
10854

109-
/**
110-
* Force-clear cached config. Called when config is known to have changed.
111-
*/
112-
export function invalidateConfig(table?: ConfigTable) {
113-
if (table) {
114-
cache.delete(table);
115-
} else {
116-
cache.clear();
117-
}
55+
export function invalidateEngineConfig() {
56+
cache = null;
57+
}
58+
59+
// Backward compatibility wrapper — old code calls getConfig('pipeline_config').
60+
// Ignores the table name and returns the unified engineConfig.
61+
// Remove after all callers are migrated to getEngineConfig() (Task 1F).
62+
export async function getConfig(_tableName?: string): Promise<EngineConfig> {
63+
return getEngineConfig();
64+
}
65+
66+
// Backward compatibility wrapper — old code calls getConfigValue('pipeline_config', 'geminiModel').
67+
// Ignores the table name and reads from the unified engineConfig.
68+
// Remove after all callers are migrated (Task 1F).
69+
export async function getConfigValue<K extends keyof EngineConfig>(
70+
_tableName: string,
71+
key: K,
72+
fallback?: EngineConfig[K],
73+
): Promise<EngineConfig[K]> {
74+
const config = await getEngineConfig();
75+
return getEngineConfigValue(config, key, fallback);
11876
}

lib/types/engine-config.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
export interface EngineConfig {
2+
_id: string;
3+
_type: 'engineConfig';
4+
_updatedAt: string;
5+
6+
// Pipeline control
7+
autoPublish: boolean;
8+
qualityThreshold: number;
9+
reviewTimeoutDays: number;
10+
reviewNotification: 'email' | 'slack' | 'webhook';
11+
maxIdeasPerRun: number;
12+
13+
// Content cadence
14+
longFormPerWeek: number;
15+
shortsPerDay: number;
16+
blogsPerWeek: number;
17+
newsletterFrequency: 'weekly' | 'biweekly' | 'monthly';
18+
publishDays: string[];
19+
contentCategories: string[];
20+
21+
// Trend discovery
22+
trendSources: string[];
23+
topicFocus: string[];
24+
rssFeeds: { name: string; url: string }[];
25+
trendSourcesEnabled: Record<string, boolean>;
26+
dedupWindowDays: number;
27+
28+
// AI & Generation
29+
geminiModel: string;
30+
infographicModel: string;
31+
infographicPromptPrefix: string;
32+
systemInstruction: string;
33+
deepResearchAgent: string;
34+
deepResearchPromptTemplate: string;
35+
enableDeepResearch: boolean;
36+
enableHorizontalInfographics: boolean;
37+
thumbnailEnabled: boolean;
38+
infographicInstructions: string[];
39+
targetVideoDurationSec: number;
40+
sceneCountMin: number;
41+
sceneCountMax: number;
42+
43+
// Audio
44+
elevenLabsVoiceId: string;
45+
46+
// Distribution
47+
youtubeEnabled: boolean;
48+
twitterEnabled: boolean;
49+
linkedinEnabled: boolean;
50+
tiktokEnabled: boolean;
51+
instagramEnabled: boolean;
52+
blueskyEnabled: boolean;
53+
newsletterEnabled: boolean;
54+
youtubeUploadVisibility: string;
55+
youtubeChannelId: string;
56+
youtubeDescriptionTemplate: string;
57+
youtubeDefaultTags: string[];
58+
notificationEmails: string[];
59+
resendFromEmail: string;
60+
61+
// Sponsor
62+
cooldownDays: number;
63+
rateCardTiers: { name: string; description: string; price: number }[];
64+
outreachEmailTemplate: string;
65+
maxOutreachPerRun: number;
66+
67+
// Brand
68+
brandPrimary: string;
69+
brandBackground: string;
70+
brandText: string;
71+
72+
// Legacy
73+
awsRegion?: string;
74+
remotionFunctionName?: string;
75+
remotionServeUrl?: string;
76+
gcsBucketName?: string;
77+
gcsProjectId?: string;
78+
}

0 commit comments

Comments
 (0)