diff --git a/app/config.ts b/app/config.ts index 1f9e991..d5358f8 100644 --- a/app/config.ts +++ b/app/config.ts @@ -40,4 +40,9 @@ export const config: ContrailConfig = { }, }, }, + + profiles: [ + "app.bsky.actor.profile", + { collection: "site.standard.publication", rkey: "blento.self" } + ] }; diff --git a/lexicons-generated/community/lexicon/calendar/event/getRecord.json b/lexicons-generated/community/lexicon/calendar/event/getRecord.json index 35f6452..894e157 100644 --- a/lexicons-generated/community/lexicon/calendar/event/getRecord.json +++ b/lexicons-generated/community/lexicon/calendar/event/getRecord.json @@ -194,8 +194,11 @@ "type": "string" }, "record": { - "type": "ref", - "ref": "#appBskyActorProfile" + "type": "union", + "refs": [ + "#appBskyActorProfile", + "#siteStandardPublication" + ] } } }, @@ -261,6 +264,50 @@ "type": "ref" } } + }, + "siteStandardPublication": { + "type": "object", + "required": [ + "url", + "name" + ], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." + }, + "icon": { + "type": "blob", + "accept": [ + "image/*" + ], + "maxSize": 1000000, + "description": "Square image to identify the publication. Should be at least 256x256." + }, + "name": { + "type": "string", + "maxLength": 5000, + "description": "Name of the publication.", + "maxGraphemes": 500 + }, + "basicTheme": { + "ref": "site.standard.theme.basic", + "type": "ref", + "description": "Simplified publication theme for tools and apps to utilize when displaying content." + }, + "description": { + "type": "string", + "maxLength": 30000, + "description": "Brief description of the publication.", + "maxGraphemes": 3000 + }, + "preferences": { + "ref": "#preferences", + "type": "ref", + "description": "Object containing platform specific preferences (with a few shared properties)." + } + } } } } diff --git a/lexicons-generated/community/lexicon/calendar/event/listRecords.json b/lexicons-generated/community/lexicon/calendar/event/listRecords.json index 9f38cab..873f649 100644 --- a/lexicons-generated/community/lexicon/calendar/event/listRecords.json +++ b/lexicons-generated/community/lexicon/calendar/event/listRecords.json @@ -303,8 +303,11 @@ "type": "string" }, "record": { - "type": "ref", - "ref": "#appBskyActorProfile" + "type": "union", + "refs": [ + "#appBskyActorProfile", + "#siteStandardPublication" + ] } } }, @@ -370,6 +373,50 @@ "type": "ref" } } + }, + "siteStandardPublication": { + "type": "object", + "required": [ + "url", + "name" + ], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." + }, + "icon": { + "type": "blob", + "accept": [ + "image/*" + ], + "maxSize": 1000000, + "description": "Square image to identify the publication. Should be at least 256x256." + }, + "name": { + "type": "string", + "maxLength": 5000, + "description": "Name of the publication.", + "maxGraphemes": 500 + }, + "basicTheme": { + "ref": "site.standard.theme.basic", + "type": "ref", + "description": "Simplified publication theme for tools and apps to utilize when displaying content." + }, + "description": { + "type": "string", + "maxLength": 30000, + "description": "Brief description of the publication.", + "maxGraphemes": 3000 + }, + "preferences": { + "ref": "#preferences", + "type": "ref", + "description": "Object containing platform specific preferences (with a few shared properties)." + } + } } } } diff --git a/lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json b/lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json index a56272b..9ab8d01 100644 --- a/lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json +++ b/lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json @@ -143,8 +143,11 @@ "type": "string" }, "record": { - "type": "ref", - "ref": "#appBskyActorProfile" + "type": "union", + "refs": [ + "#appBskyActorProfile", + "#siteStandardPublication" + ] } } }, @@ -210,6 +213,50 @@ "type": "ref" } } + }, + "siteStandardPublication": { + "type": "object", + "required": [ + "url", + "name" + ], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." + }, + "icon": { + "type": "blob", + "accept": [ + "image/*" + ], + "maxSize": 1000000, + "description": "Square image to identify the publication. Should be at least 256x256." + }, + "name": { + "type": "string", + "maxLength": 5000, + "description": "Name of the publication.", + "maxGraphemes": 500 + }, + "basicTheme": { + "ref": "site.standard.theme.basic", + "type": "ref", + "description": "Simplified publication theme for tools and apps to utilize when displaying content." + }, + "description": { + "type": "string", + "maxLength": 30000, + "description": "Brief description of the publication.", + "maxGraphemes": 3000 + }, + "preferences": { + "ref": "#preferences", + "type": "ref", + "description": "Object containing platform specific preferences (with a few shared properties)." + } + } } } } diff --git a/lexicons-generated/community/lexicon/calendar/rsvp/listRecords.json b/lexicons-generated/community/lexicon/calendar/rsvp/listRecords.json index b5a0b4f..d0359bc 100644 --- a/lexicons-generated/community/lexicon/calendar/rsvp/listRecords.json +++ b/lexicons-generated/community/lexicon/calendar/rsvp/listRecords.json @@ -191,8 +191,11 @@ "type": "string" }, "record": { - "type": "ref", - "ref": "#appBskyActorProfile" + "type": "union", + "refs": [ + "#appBskyActorProfile", + "#siteStandardPublication" + ] } } }, @@ -258,6 +261,50 @@ "type": "ref" } } + }, + "siteStandardPublication": { + "type": "object", + "required": [ + "url", + "name" + ], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." + }, + "icon": { + "type": "blob", + "accept": [ + "image/*" + ], + "maxSize": 1000000, + "description": "Square image to identify the publication. Should be at least 256x256." + }, + "name": { + "type": "string", + "maxLength": 5000, + "description": "Name of the publication.", + "maxGraphemes": 500 + }, + "basicTheme": { + "ref": "site.standard.theme.basic", + "type": "ref", + "description": "Simplified publication theme for tools and apps to utilize when displaying content." + }, + "description": { + "type": "string", + "maxLength": 30000, + "description": "Brief description of the publication.", + "maxGraphemes": 3000 + }, + "preferences": { + "ref": "#preferences", + "type": "ref", + "description": "Object containing platform specific preferences (with a few shared properties)." + } + } } } } diff --git a/lexicons-generated/rsvp/atmo/getCursor.json b/lexicons-generated/rsvp/atmo/getCursor.json new file mode 100644 index 0000000..03fc0da --- /dev/null +++ b/lexicons-generated/rsvp/atmo/getCursor.json @@ -0,0 +1,27 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.getCursor", + "defs": { + "main": { + "type": "query", + "description": "Get the current cursor position", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "time_us": { + "type": "integer" + }, + "date": { + "type": "string" + }, + "seconds_ago": { + "type": "integer" + } + } + } + } + } + } +} diff --git a/lexicons-generated/rsvp/atmo/getOverview.json b/lexicons-generated/rsvp/atmo/getOverview.json new file mode 100644 index 0000000..e349ced --- /dev/null +++ b/lexicons-generated/rsvp/atmo/getOverview.json @@ -0,0 +1,51 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.getOverview", + "defs": { + "main": { + "type": "query", + "description": "Get an overview of all indexed collections", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "total_records", + "collections" + ], + "properties": { + "total_records": { + "type": "integer" + }, + "collections": { + "type": "array", + "items": { + "type": "ref", + "ref": "#collectionStats" + } + } + } + } + } + }, + "collectionStats": { + "type": "object", + "required": [ + "collection", + "records", + "unique_users" + ], + "properties": { + "collection": { + "type": "string" + }, + "records": { + "type": "integer" + }, + "unique_users": { + "type": "integer" + } + } + } + } +} diff --git a/lexicons-generated/rsvp/atmo/getProfile.json b/lexicons-generated/rsvp/atmo/getProfile.json index e503959..622dd6a 100644 --- a/lexicons-generated/rsvp/atmo/getProfile.json +++ b/lexicons-generated/rsvp/atmo/getProfile.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a user's profile by DID or handle", + "description": "Get a user's profiles by DID or handle", "parameters": { "type": "params", "required": [ @@ -21,8 +21,19 @@ "output": { "encoding": "application/json", "schema": { - "type": "ref", - "ref": "#profileEntry" + "type": "object", + "required": [ + "profiles" + ], + "properties": { + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } } } }, @@ -54,8 +65,11 @@ "type": "string" }, "record": { - "type": "ref", - "ref": "#appBskyActorProfile" + "type": "union", + "refs": [ + "#appBskyActorProfile", + "#siteStandardPublication" + ] } } }, @@ -121,6 +135,50 @@ "type": "ref" } } + }, + "siteStandardPublication": { + "type": "object", + "required": [ + "url", + "name" + ], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." + }, + "icon": { + "type": "blob", + "accept": [ + "image/*" + ], + "maxSize": 1000000, + "description": "Square image to identify the publication. Should be at least 256x256." + }, + "name": { + "type": "string", + "maxLength": 5000, + "description": "Name of the publication.", + "maxGraphemes": 500 + }, + "basicTheme": { + "ref": "site.standard.theme.basic", + "type": "ref", + "description": "Simplified publication theme for tools and apps to utilize when displaying content." + }, + "description": { + "type": "string", + "maxLength": 30000, + "description": "Brief description of the publication.", + "maxGraphemes": 3000 + }, + "preferences": { + "ref": "#preferences", + "type": "ref", + "description": "Object containing platform specific preferences (with a few shared properties)." + } + } } } } diff --git a/lexicons-pulled/README.md b/lexicons-pulled/README.md index 378ad0f..69dd796 100644 --- a/lexicons-pulled/README.md +++ b/lexicons-pulled/README.md @@ -2,4 +2,4 @@ this directory contains lexicon documents pulled from the following sources: -- atproto (nsids: app.bsky.actor.profile, community.lexicon.calendar.event, community.lexicon.calendar.rsvp, community.lexicon.location.address, community.lexicon.location.fsq, community.lexicon.location.geo, community.lexicon.location.hthree) +- atproto (nsids: app.bsky.actor.profile, community.lexicon.calendar.event, community.lexicon.calendar.rsvp, community.lexicon.location.address, community.lexicon.location.fsq, community.lexicon.location.geo, community.lexicon.location.hthree, site.standard.publication, site.standard.theme.basic, site.standard.theme.color) diff --git a/lexicons-pulled/site/standard/publication.json b/lexicons-pulled/site/standard/publication.json new file mode 100644 index 0000000..456f730 --- /dev/null +++ b/lexicons-pulled/site/standard/publication.json @@ -0,0 +1,62 @@ +{ + "id": "site.standard.publication", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": ["url", "name"], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Base publication url (ex: https://standard.site). The canonical document URL is formed by combining this value with the document path." + }, + "icon": { + "type": "blob", + "accept": ["image/*"], + "maxSize": 1000000, + "description": "Square image to identify the publication. Should be at least 256x256." + }, + "name": { + "type": "string", + "maxLength": 5000, + "description": "Name of the publication.", + "maxGraphemes": 500 + }, + "basicTheme": { + "ref": "site.standard.theme.basic", + "type": "ref", + "description": "Simplified publication theme for tools and apps to utilize when displaying content." + }, + "description": { + "type": "string", + "maxLength": 30000, + "description": "Brief description of the publication.", + "maxGraphemes": 3000 + }, + "preferences": { + "ref": "#preferences", + "type": "ref", + "description": "Object containing platform specific preferences (with a few shared properties)." + } + } + }, + "description": "A publication record representing a blog, website, or content platform. Publications serve as containers for documents and define the overall branding and settings." + }, + "preferences": { + "type": "object", + "properties": { + "showInDiscover": { + "type": "boolean", + "default": true, + "description": "Boolean which decides whether the publication should appear in discovery feeds." + } + }, + "description": "Platform-specific preferences for the publication, including discovery and visibility settings." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons-pulled/site/standard/theme/basic.json b/lexicons-pulled/site/standard/theme/basic.json new file mode 100644 index 0000000..88c7cba --- /dev/null +++ b/lexicons-pulled/site/standard/theme/basic.json @@ -0,0 +1,34 @@ +{ + "id": "site.standard.theme.basic", + "defs": { + "main": { + "type": "object", + "required": ["background", "foreground", "accent", "accentForeground"], + "properties": { + "accent": { + "refs": ["site.standard.theme.color#rgb"], + "type": "union", + "description": "Color used for links and button backgrounds." + }, + "background": { + "refs": ["site.standard.theme.color#rgb"], + "type": "union", + "description": "Color used for content background." + }, + "foreground": { + "refs": ["site.standard.theme.color#rgb"], + "type": "union", + "description": "Color used for content text." + }, + "accentForeground": { + "refs": ["site.standard.theme.color#rgb"], + "type": "union", + "description": "Color used for button text." + } + }, + "description": "A simplified theme definition for publications, providing basic color customization for content display across different platforms and applications." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons-pulled/site/standard/theme/color.json b/lexicons-pulled/site/standard/theme/color.json new file mode 100644 index 0000000..5dde553 --- /dev/null +++ b/lexicons-pulled/site/standard/theme/color.json @@ -0,0 +1,54 @@ +{ + "id": "site.standard.theme.color", + "defs": { + "rgb": { + "type": "object", + "required": ["r", "g", "b"], + "properties": { + "b": { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + "g": { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + "r": { + "type": "integer", + "maximum": 255, + "minimum": 0 + } + } + }, + "rgba": { + "type": "object", + "required": ["r", "g", "b", "a"], + "properties": { + "a": { + "type": "integer", + "maximum": 100, + "minimum": 0 + }, + "b": { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + "g": { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + "r": { + "type": "integer", + "maximum": 255, + "minimum": 0 + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/src/core/router/collection.ts b/src/core/router/collection.ts index 4dbd08f..81c47ad 100644 --- a/src/core/router/collection.ts +++ b/src/core/router/collection.ts @@ -164,7 +164,7 @@ export async function runPipeline( return { records: formattedRecords, cursor: result.cursor, - ...(profileMap ? { profiles: Object.values(profileMap) } : {}), + ...(profileMap ? { profiles: Object.values(profileMap).flat() } : {}), }; } @@ -247,7 +247,7 @@ export function registerCollectionRoutes( return c.json({ ...formatted, - ...(profileMap ? { profiles: Object.values(profileMap) } : {}), + ...(profileMap ? { profiles: Object.values(profileMap).flat() } : {}), }); }); diff --git a/src/core/router/index.ts b/src/core/router/index.ts index 59f96e4..d6806b3 100644 --- a/src/core/router/index.ts +++ b/src/core/router/index.ts @@ -1,6 +1,7 @@ import { Hono } from "hono"; import { cors } from "hono/cors"; import type { Database, ContrailConfig } from "../types"; +import { normalizeProfileConfig } from "../types"; import { registerAdminRoutes } from "./admin"; import { registerCollectionRoutes } from "./collection"; import { registerFeedRoutes } from "./feed"; @@ -30,16 +31,16 @@ export function createApp( if (!did) return c.json({ error: "Could not resolve actor" }, 400); // Ensure profile records are backfilled - const profiles = config.profiles ?? ["app.bsky.actor.profile"]; - for (const collection of profiles) { - await backfillUser(db, did, collection, Date.now() + 10_000, config); + const profileConfigs = (config.profiles ?? []).map(normalizeProfileConfig); + for (const pc of profileConfigs) { + await backfillUser(db, did, pc.collection, Date.now() + 10_000, config); } const profileMap = await resolveProfiles(db, config, [did]); - const profile = profileMap[did]; - if (!profile) return c.json({ error: "Profile not found" }, 404); + const profiles = profileMap[did]; + if (!profiles || profiles.length === 0) return c.json({ error: "Profile not found" }, 404); - return c.json(profile); + return c.json({ profiles }); }); registerAdminRoutes(app, db, config); diff --git a/src/core/router/profiles.ts b/src/core/router/profiles.ts index b66f397..491c9d2 100644 --- a/src/core/router/profiles.ts +++ b/src/core/router/profiles.ts @@ -1,5 +1,5 @@ -import type { Database, ContrailConfig, RecordRow } from "../types"; -import { recordsTableName } from "../types"; +import type { Database, ContrailConfig, RecordRow, ProfileConfig } from "../types"; +import { recordsTableName, normalizeProfileConfig } from "../types"; import { resolveIdentities } from "../identity"; import { getPDS } from "../client"; import type { Did } from "@atcute/lexicons"; @@ -37,20 +37,20 @@ export async function resolveProfiles( db: Database, config: ContrailConfig, dids: string[] -): Promise> { +): Promise> { if (dids.length === 0 || !config.profiles || config.profiles.length === 0) { return {}; } - const result: Record = {}; - - // Batch-lookup profile records for each configured profile collection (first match wins) - for (const collection of config.profiles) { - const remaining = dids.filter((d) => !result[d]); - if (remaining.length === 0) break; + const profileConfigs = config.profiles.map(normalizeProfileConfig); + const result: Record = {}; + // Batch-lookup profile records for each configured profile collection + for (const pc of profileConfigs) { + const { collection, rkey: configRkey } = pc; + const rkey = configRkey ?? "self"; const table = recordsTableName(collection); - const uris = remaining.map((did) => `at://${did}/${collection}/self`); + const uris = dids.map((did) => `at://${did}/${collection}/${rkey}`); const rows = await batchedInQuery>( db, @@ -68,7 +68,8 @@ export async function resolveProfiles( record = row.record; } } - result[row.did] = { + if (!result[row.did]) result[row.did] = []; + result[row.did].push({ did: row.did, handle: null, // filled below uri: row.uri, @@ -76,7 +77,7 @@ export async function resolveProfiles( rkey: row.rkey, cid: row.cid, record, - }; + }); } } @@ -85,10 +86,11 @@ export async function resolveProfiles( // Fetch missing profile records from PDS on demand const missingDids = dids.filter((d) => !result[d]); - if (missingDids.length > 0 && config.profiles && config.profiles.length > 0) { + if (missingDids.length > 0 && profileConfigs.length > 0) { const fetched = await fetchMissingProfiles(db, config, missingDids); - for (const [did, entry] of Object.entries(fetched)) { - result[did] = entry; + for (const [did, entries] of Object.entries(fetched)) { + if (!result[did]) result[did] = []; + result[did].push(...entries); } } @@ -98,9 +100,11 @@ export async function resolveProfiles( const handle = identity?.handle ?? null; if (result[did]) { - result[did].handle = handle; + for (const entry of result[did]) { + entry.handle = handle; + } } else { - result[did] = { did, handle }; + result[did] = [{ did, handle }]; } } @@ -109,59 +113,65 @@ export async function resolveProfiles( /** * Fetch profile records from PDS for DIDs not yet in the index. - * Fetches in parallel, indexes the results into D1 for future requests. + * Fetches in parallel across all configured profile collections, + * indexes the results into D1 for future requests. */ async function fetchMissingProfiles( db: Database, config: ContrailConfig, dids: string[] -): Promise> { - const result: Record = {}; - const collection = config.profiles![0]; - const table = recordsTableName(collection); +): Promise> { + const result: Record = {}; + const profileConfigs = config.profiles!.map(normalizeProfileConfig); await Promise.all( - dids.map(async (did) => { - try { - const pds = await getPDS(did as Did, db); - if (!pds) return; - - const url = new URL("/xrpc/com.atproto.repo.getRecord", pds); - url.searchParams.set("repo", did); - url.searchParams.set("collection", collection); - url.searchParams.set("rkey", "self"); - - const res = await fetch(url.toString()); - if (!res.ok) return; - - const data = (await res.json()) as { uri?: string; value?: unknown; cid?: string }; - if (!data.value || !data.cid) return; - - const uri = data.uri ?? `at://${did}/${collection}/self`; - const record = data.value; - const cid = data.cid; - - // Index into D1 for future requests - await db - .prepare( - `INSERT INTO ${table} (uri, did, rkey, cid, record, time_us, indexed_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(uri) DO UPDATE SET cid = excluded.cid, record = excluded.record, indexed_at = excluded.indexed_at` - ) - .bind(uri, did, "self", cid, JSON.stringify(record), Date.now() * 1000, Date.now()) - .run(); - - result[did] = { - did, - handle: null, - uri, - collection, - rkey: "self", - cid, - record, - }; - } catch { - // Skip failures silently - } - }) + dids.flatMap((did) => + profileConfigs.map(async (pc) => { + const { collection, rkey: configRkey } = pc; + const rkey = configRkey ?? "self"; + const table = recordsTableName(collection); + try { + const pds = await getPDS(did as Did, db); + if (!pds) return; + + const url = new URL("/xrpc/com.atproto.repo.getRecord", pds); + url.searchParams.set("repo", did); + url.searchParams.set("collection", collection); + url.searchParams.set("rkey", rkey); + + const res = await fetch(url.toString()); + if (!res.ok) return; + + const data = (await res.json()) as { uri?: string; value?: unknown; cid?: string }; + if (!data.value || !data.cid) return; + + const uri = data.uri ?? `at://${did}/${collection}/${rkey}`; + const record = data.value; + const cid = data.cid; + + // Index into D1 for future requests + await db + .prepare( + `INSERT INTO ${table} (uri, did, rkey, cid, record, time_us, indexed_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(uri) DO UPDATE SET cid = excluded.cid, record = excluded.record, indexed_at = excluded.indexed_at` + ) + .bind(uri, did, rkey, cid, JSON.stringify(record), Date.now() * 1000, Date.now()) + .run(); + + if (!result[did]) result[did] = []; + result[did].push({ + did, + handle: null, + uri, + collection, + rkey, + cid, + record, + }); + } catch { + // Skip failures silently + } + }) + ) ); return result; diff --git a/src/core/types.ts b/src/core/types.ts index 67089ba..78b2862 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -79,7 +79,21 @@ export interface CollectionConfig { searchable?: string[] | false; } -export const DEFAULT_PROFILES = ["app.bsky.actor.profile"]; +export interface ProfileConfig { + collection: string; + rkey?: string; // defaults to "self" +} + +export const DEFAULT_PROFILES: ProfileConfig[] = [ + { collection: "app.bsky.actor.profile" }, +]; + +/** Normalize a profiles config entry (string or object) into ProfileConfig. */ +export function normalizeProfileConfig( + p: string | ProfileConfig +): ProfileConfig { + return typeof p === "string" ? { collection: p } : p; +} export const DEFAULT_JETSTREAMS = [ "wss://jetstream1.us-east.bsky.network", @@ -101,7 +115,7 @@ export interface Logger { export interface ContrailConfig { namespace: string; collections: Record; - profiles?: string[]; + profiles?: (string | ProfileConfig)[]; relays?: string[]; jetstreams?: string[]; feeds?: Record; @@ -131,11 +145,13 @@ export interface ResolvedContrailConfig extends ContrailConfig { * Resolve config: apply defaults, auto-add profile collections, compute queryable maps. */ export function resolveConfig(config: ContrailConfig): ResolvedContrailConfig { - const profiles = config.profiles ?? DEFAULT_PROFILES; + const profiles = (config.profiles ?? DEFAULT_PROFILES).map( + normalizeProfileConfig + ); const collections = { ...config.collections }; - for (const col of profiles) { - if (!collections[col]) { - collections[col] = { discover: false }; + for (const p of profiles) { + if (!collections[p.collection]) { + collections[p.collection] = { discover: false }; } } diff --git a/src/generate.ts b/src/generate.ts index ab2bbed..140f27d 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -234,7 +234,9 @@ export function generateLexicons(options: GenerateOptions): Record (typeof p === "string" ? p : p.collection) + ); const extraDefs: Record = {}; const objectRefs: string[] = []; for (const col of profiles) { @@ -290,7 +292,7 @@ export function generateLexicons(options: GenerateOptions): Record (typeof p === "string" ? p : p.collection) + ); const feedFollowNsids = config.feeds ? Object.values(config.feeds).map((f) => f.follow) : []; const pullNsids = new Set([...collectionNsids, ...profileNsids, ...feedFollowNsids]); for (const ref of allRefs) { diff --git a/tests/types.test.ts b/tests/types.test.ts index dfddf3f..bc71eb6 100644 --- a/tests/types.test.ts +++ b/tests/types.test.ts @@ -138,7 +138,7 @@ describe("resolveConfig", () => { collections: {}, profiles: ["custom.profile"], }); - expect(resolved.profiles).toEqual(["custom.profile"]); + expect(resolved.profiles).toEqual([{ collection: "custom.profile" }]); expect(resolved.collections["custom.profile"]).toEqual({ discover: false }); expect(resolved.collections["app.bsky.actor.profile"]).toBeUndefined(); });