/** * Schema bridge layer for converting between Drizzle models and Lex-validated data */ import { l } from '@atproto/lex' import type { InferSelectModel } from 'drizzle-orm' import { questions, answers, users } from './schema' import * as lexicons from '../lexicons/index.js' import { SOURCE_TYPES, type SourceType } from './shared-schemas' // Type aliases for Drizzle models export type DrizzleQuestion = InferSelectModel export type DrizzleAnswer = InferSelectModel export type DrizzleUser = InferSelectModel // Type aliases for Lex records export type LexQuestion = lexicons.question.Main export type LexAnswer = lexicons.answer.Main export type LexProfile = lexicons.profile.Main /** * Convert a Drizzle question record to a Lex question record */ export function questionToLexRecord(dbQuestion: DrizzleQuestion): LexQuestion { const built = lexicons.question.$build({ content: dbQuestion.content, targetDid: dbQuestion.targetDid as l.DidString, authorDid: dbQuestion.authorDid as l.DidString, sourceType: (dbQuestion.sourceType as SourceType) || SOURCE_TYPES.ASKIMUT, anonymous: dbQuestion.anonymous, createdAt: l.toDatetimeString(dbQuestion.createdAt), // Tags are not currently stored in the database, but could be added later tags: undefined, sourceUri: dbQuestion.sourceUri || undefined, sourceData: dbQuestion.sourceData ? JSON.parse(dbQuestion.sourceData) : undefined }) return built as LexQuestion } /** * Convert a Lex question record to a partial Drizzle question record */ export function lexRecordToQuestion(lexQuestion: LexQuestion): Partial { return { content: lexQuestion.content as string, targetDid: lexQuestion.targetDid as string, authorDid: lexQuestion.authorDid as string, sourceType: (lexQuestion.sourceType as string) || SOURCE_TYPES.ASKIMUT, anonymous: (lexQuestion.anonymous as boolean) ?? false, createdAt: new Date(lexQuestion.createdAt as string), sourceUri: lexQuestion.sourceUri as string || null, sourceData: lexQuestion.sourceData ? JSON.stringify(lexQuestion.sourceData) : null, // Note: id, atUri, and reindexed are handled separately } } /** * Convert a Drizzle answer record to a Lex answer record */ export function answerToLexRecord(dbAnswer: DrizzleAnswer, questionAtUri: string): LexAnswer { const built = lexicons.answer.$build({ content: dbAnswer.content, questionUri: questionAtUri as l.AtUriString, authorDid: dbAnswer.authorDid as l.DidString, sourceType: (dbAnswer.sourceType as SourceType) || SOURCE_TYPES.ASKIMUT, createdAt: l.toDatetimeString(dbAnswer.createdAt), sourceUri: dbAnswer.sourceUri || undefined, sourceData: dbAnswer.sourceData ? JSON.parse(dbAnswer.sourceData) : undefined }) return built as LexAnswer } /** * Convert a Lex answer record to a partial Drizzle answer record */ export function lexRecordToAnswer(lexAnswer: LexAnswer): Partial { return { content: lexAnswer.content as string, authorDid: lexAnswer.authorDid as string, sourceType: (lexAnswer.sourceType as string) || SOURCE_TYPES.ASKIMUT, createdAt: new Date(lexAnswer.createdAt as string), sourceUri: lexAnswer.sourceUri as string || null, sourceData: lexAnswer.sourceData ? JSON.stringify(lexAnswer.sourceData) : null, // Note: questionId, id, atUri, and reindexed are handled separately } } /** * Convert a Drizzle user record to a Lex profile record */ export function userToLexProfile(dbUser: DrizzleUser): LexProfile { const built = lexicons.profile.$build({ displayName: dbUser.displayName || undefined, description: undefined, // Not currently stored in database questionsOpen: dbUser.questionsOpen, avatar: undefined // Avatar handling would need to be implemented }) return built as LexProfile } /** * Convert a Lex profile record to a partial Drizzle user record */ export function lexProfileToUser(lexProfile: LexProfile): Partial { return { displayName: (lexProfile.displayName as string) || null, questionsOpen: lexProfile.questionsOpen as boolean, // Note: handle, did, avatarUrl, and timestamps are handled separately } } /** * Validate and convert data using a Lex schema */ export function validateAndConvert( data: unknown, schema: { $validate: (data: unknown) => T } ): T { return schema.$validate(data) } /** * Safely validate and convert data using a Lex schema */ export function safeValidateAndConvert( data: unknown, schema: { $safeParse: (data: unknown) => { success: boolean; value?: T; error?: any } } ): { success: true; value: T } | { success: false; error: any } { const result = schema.$safeParse(data) if (result.success && result.value) { return { success: true, value: result.value } } else { return { success: false, error: result.error } } } /** * Create a question with validation */ export function createValidatedQuestion(data: { content: string targetDid: string authorDid: string sourceType?: SourceType anonymous?: boolean tags?: string[] sourceUri?: string sourceData?: Record }): { lexRecord: LexQuestion; dbData: Partial } { const built = lexicons.question.$build({ content: data.content, targetDid: data.targetDid as l.DidString, authorDid: data.authorDid as l.DidString, sourceType: data.sourceType || SOURCE_TYPES.ASKIMUT, anonymous: data.anonymous ?? false, createdAt: l.toDatetimeString(new Date()), tags: data.tags, sourceUri: data.sourceUri, sourceData: data.sourceData }) const lexRecord = built as LexQuestion // Validate the record lexicons.question.$validate(lexRecord) const dbData: Partial = { content: data.content, targetDid: data.targetDid, authorDid: data.authorDid, sourceType: data.sourceType || SOURCE_TYPES.ASKIMUT, anonymous: data.anonymous ?? false, createdAt: new Date(), sourceUri: data.sourceUri || null, sourceData: data.sourceData ? JSON.stringify(data.sourceData) : null } return { lexRecord, dbData } } /** * Create an answer with validation */ export function createValidatedAnswer(data: { content: string questionId: string questionAtUri: string authorDid: string sourceType?: SourceType sourceUri?: string sourceData?: Record }): { lexRecord: LexAnswer; dbData: Partial } { const built = lexicons.answer.$build({ content: data.content, questionUri: data.questionAtUri as l.AtUriString, authorDid: data.authorDid as l.DidString, sourceType: data.sourceType || SOURCE_TYPES.ASKIMUT, createdAt: l.toDatetimeString(new Date()), sourceUri: data.sourceUri, sourceData: data.sourceData }) const lexRecord = built as LexAnswer // Validate the record lexicons.answer.$validate(lexRecord) const dbData: Partial = { content: data.content, questionId: data.questionId, authorDid: data.authorDid, sourceType: data.sourceType || SOURCE_TYPES.ASKIMUT, createdAt: new Date(), sourceUri: data.sourceUri || null, sourceData: data.sourceData ? JSON.stringify(data.sourceData) : null } return { lexRecord, dbData } } /** * Update a profile with validation */ export function createValidatedProfile(data: { displayName?: string description?: string questionsOpen: boolean }): { lexRecord: LexProfile; dbData: Partial } { const built = lexicons.profile.$build({ displayName: data.displayName, description: data.description, questionsOpen: data.questionsOpen, avatar: undefined // Avatar handling would need to be implemented }) const lexRecord = built as LexProfile // Validate the record lexicons.profile.$validate(lexRecord) const dbData: Partial = { displayName: data.displayName || null, questionsOpen: data.questionsOpen, updatedAt: new Date() } return { lexRecord, dbData } } /** * Create a question from Bluesky post data */ export function createQuestionFromBlueSky(data: { content: string targetDid: string authorDid: string bskyUri: string bskyPost: Record anonymous?: boolean }): { lexRecord: LexQuestion; dbData: Partial } { return createValidatedQuestion({ content: data.content, targetDid: data.targetDid, authorDid: data.authorDid, sourceType: SOURCE_TYPES.BLUESKY, anonymous: data.anonymous ?? false, sourceUri: data.bskyUri, sourceData: { bskyPost: data.bskyPost, platform: 'bluesky' } }) } /** * Create an answer from Bluesky post data */ export function createAnswerFromBlueSky(data: { content: string questionId: string questionAtUri: string authorDid: string bskyUri: string bskyPost: Record }): { lexRecord: LexAnswer; dbData: Partial } { return createValidatedAnswer({ content: data.content, questionId: data.questionId, questionAtUri: data.questionAtUri, authorDid: data.authorDid, sourceType: SOURCE_TYPES.BLUESKY, sourceUri: data.bskyUri, sourceData: { bskyPost: data.bskyPost, platform: 'bluesky' } }) } /** * Create a question from standard.site data */ export function createQuestionFromStandardSite(data: { content: string targetDid: string authorDid: string standardSiteUri: string standardSiteData: Record anonymous?: boolean }): { lexRecord: LexQuestion; dbData: Partial } { return createValidatedQuestion({ content: data.content, targetDid: data.targetDid, authorDid: data.authorDid, sourceType: SOURCE_TYPES.STANDARD_SITE, anonymous: data.anonymous ?? false, sourceUri: data.standardSiteUri, sourceData: { standardSite: data.standardSiteData, platform: 'standard.site' } }) } /** * Detect source type from URI or data */ export function detectSourceType(uri?: string, data?: Record): SourceType { if (!uri && !data) { return SOURCE_TYPES.ASKIMUT } if (uri) { if (uri.includes('bsky.app') || uri.includes('at://')) { return SOURCE_TYPES.BLUESKY } if (uri.includes('standard.site')) { return SOURCE_TYPES.STANDARD_SITE } if (uri.includes('mastodon') || uri.includes('/@')) { return SOURCE_TYPES.MASTODON } if (uri.includes('nostr:') || uri.includes('npub')) { return SOURCE_TYPES.NOSTR } } if (data?.platform) { const platform = data.platform as string switch (platform.toLowerCase()) { case 'bluesky': case 'bsky': return SOURCE_TYPES.BLUESKY case 'standard.site': return SOURCE_TYPES.STANDARD_SITE case 'mastodon': return SOURCE_TYPES.MASTODON case 'nostr': return SOURCE_TYPES.NOSTR } } return SOURCE_TYPES.ASKIMUT }