feat(server): system_provider_configs and character_covers (#905)

This commit is contained in:
RainbowBird
2026-01-07 21:44:05 +08:00
committed by GitHub
parent 5c2218dd49
commit 371a0b105f
16 changed files with 1928 additions and 130 deletions
+47
View File
@@ -0,0 +1,47 @@
CREATE TABLE "character_covers" (
"id" text PRIMARY KEY NOT NULL,
"character_id" text NOT NULL,
"foreground_url" text NOT NULL,
"background_url" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"deleted_at" timestamp
);
--> statement-breakpoint
CREATE TABLE "system_provider_configs" (
"id" text PRIMARY KEY NOT NULL,
"definition_id" text NOT NULL,
"name" text NOT NULL,
"config" jsonb DEFAULT '{}'::jsonb NOT NULL,
"validated" boolean DEFAULT false NOT NULL,
"validation_bypassed" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"deleted_at" timestamp
);
--> statement-breakpoint
ALTER TABLE "user_settings_provider_configs" RENAME TO "user_provider_configs";--> statement-breakpoint
ALTER TABLE "character_bookmarks" RENAME TO "user_character_bookmarks";--> statement-breakpoint
ALTER TABLE "character_likes" RENAME TO "user_character_likes";--> statement-breakpoint
ALTER TABLE "user_character_bookmarks" DROP CONSTRAINT "character_bookmarks_user_id_user_id_fk";
--> statement-breakpoint
ALTER TABLE "user_character_bookmarks" DROP CONSTRAINT "character_bookmarks_character_id_characters_id_fk";
--> statement-breakpoint
ALTER TABLE "user_character_likes" DROP CONSTRAINT "character_likes_user_id_user_id_fk";
--> statement-breakpoint
ALTER TABLE "user_character_likes" DROP CONSTRAINT "character_likes_character_id_characters_id_fk";
--> statement-breakpoint
ALTER TABLE "user_provider_configs" DROP CONSTRAINT "user_settings_provider_configs_owner_id_user_id_fk";
--> statement-breakpoint
ALTER TABLE "user_character_bookmarks" DROP CONSTRAINT "character_bookmarks_user_id_character_id_pk";--> statement-breakpoint
ALTER TABLE "user_character_likes" DROP CONSTRAINT "character_likes_user_id_character_id_pk";--> statement-breakpoint
ALTER TABLE "user_character_bookmarks" ADD CONSTRAINT "user_character_bookmarks_user_id_character_id_pk" PRIMARY KEY("user_id","character_id");--> statement-breakpoint
ALTER TABLE "user_character_likes" ADD CONSTRAINT "user_character_likes_user_id_character_id_pk" PRIMARY KEY("user_id","character_id");--> statement-breakpoint
ALTER TABLE "character_covers" ADD CONSTRAINT "character_covers_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_character_bookmarks" ADD CONSTRAINT "user_character_bookmarks_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_character_bookmarks" ADD CONSTRAINT "user_character_bookmarks_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_character_likes" ADD CONSTRAINT "user_character_likes_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_character_likes" ADD CONSTRAINT "user_character_likes_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_provider_configs" ADD CONSTRAINT "user_provider_configs_owner_id_user_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "characters" DROP COLUMN "character_avatar_url";--> statement-breakpoint
ALTER TABLE "characters" DROP COLUMN "cover_background_url";
File diff suppressed because it is too large Load Diff
+7
View File
@@ -36,6 +36,13 @@
"when": 1767692253453,
"tag": "0004_faithful_lilith",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1767792829241,
"tag": "0005_true_namor",
"breakpoints": true
}
]
}
+6 -4
View File
@@ -57,6 +57,9 @@ export const InsertCharacterCapabilitySchema = createInsertSchema(schema.charact
export const CharacterI18nSchema = createSelectSchema(schema.characterI18n)
export const InsertCharacterI18nSchema = createInsertSchema(schema.characterI18n)
export const CharacterCoverSchema = createSelectSchema(schema.characterCovers)
export const InsertCharacterCoverSchema = createInsertSchema(schema.characterCovers)
export const CharacterPromptSchema = createSelectSchema(schema.characterPrompts)
export const InsertCharacterPromptSchema = createInsertSchema(schema.characterPrompts)
@@ -70,11 +73,12 @@ export const CreateCharacterSchema = object({
creatorId: optional(string()),
ownerId: optional(string()),
avatarUrl: optional(string()),
characterAvatarUrl: optional(string()),
coverBackgroundUrl: optional(string()),
creatorRole: optional(string()),
priceCredit: optional(string()),
}),
cover: optional(createInsertSchema(schema.characterCovers, {
characterId: optional(string()),
})),
capabilities: optional(array(createInsertSchema(schema.characterCapabilities, {
characterId: optional(string()),
type: CharacterCapabilityTypeSchema,
@@ -99,8 +103,6 @@ export const UpdateCharacterSchema = createInsertSchema(schema.character, {
version: optional(string()),
coverUrl: optional(string()),
avatarUrl: optional(string()),
characterAvatarUrl: optional(string()),
coverBackgroundUrl: optional(string()),
creatorRole: optional(string()),
priceCredit: optional(string()),
creatorId: optional(string()),
+5 -2
View File
@@ -3,8 +3,11 @@ import { boolean, object, optional, record, string } from 'valibot'
import * as schema from '../schemas/providers'
export const ProviderConfigSchema = createSelectSchema(schema.providerConfigs)
export const InsertProviderConfigSchema = createInsertSchema(schema.providerConfigs)
export const UserProviderConfigSchema = createSelectSchema(schema.userProviderConfigs)
export const InsertUserProviderConfigSchema = createInsertSchema(schema.userProviderConfigs)
export const SystemProviderConfigSchema = createSelectSchema(schema.systemProviderConfigs)
export const InsertSystemProviderConfigSchema = createInsertSchema(schema.systemProviderConfigs)
export const CreateProviderConfigSchema = object({
definitionId: string(),
@@ -64,10 +64,11 @@ describe('characterRoutes', () => {
expect(await res.json()).toEqual([])
})
it('post / should create character', async () => {
it('post / should create character with cover', async () => {
const payload = {
character: { version: '1', coverUrl: 'url', characterId: 'cid' },
i18n: [{ language: 'en', name: 'Aster', description: 'desc', tags: [] }],
cover: { foregroundUrl: 'fg', backgroundUrl: 'bg' },
}
const res = await app.fetch(new Request('http://localhost/', {
@@ -79,6 +80,9 @@ describe('characterRoutes', () => {
expect(res.status).toBe(201)
const data = await res.json()
expect(data.id).toBeDefined()
const char = await characterService.findById(data.id)
expect(char?.cover?.foregroundUrl).toBe('fg')
})
it('get / should return created character', async () => {
@@ -58,7 +58,7 @@ describe('providerRoutes', () => {
expect(res.status).toBe(401)
})
it('get / should return empty list initially', async () => {
it('get / should return empty list initially (only user configs, system configs tested later)', async () => {
const res = await app.fetch(new Request('http://localhost/'), { user: testUser } as any)
expect(res.status).toBe(200)
expect(await res.json()).toEqual([])
@@ -83,26 +83,43 @@ describe('providerRoutes', () => {
expect(data.name).toBe('My OpenAI')
})
it('get / should return created provider', async () => {
it('get / should return unified list (user + system)', async () => {
// Create a system config directly in DB
await db.insert(schema.systemProviderConfigs).values({
id: 'sys-1',
definitionId: 'anthropic',
name: 'System Anthropic',
config: { apiKey: 'sys-sk' },
})
const res = await app.fetch(new Request('http://localhost/'), { user: testUser } as any)
expect(res.status).toBe(200)
const data = await res.json()
expect(data.length).toBe(1)
expect(data[0].definitionId).toBe('openai')
expect(data.length).toBe(2)
expect(data.some((p: any) => p.isSystem === true)).toBe(true)
expect(data.some((p: any) => p.isSystem === false)).toBe(true)
})
it('get /:id should return specific provider', async () => {
const providers = await providerService.findByOwnerId(testUser.id)
it('get /:id should return specific provider (user or system)', async () => {
const providers = await providerService.findUserConfigsByOwnerId(testUser.id)
const providerId = providers[0].id
const res = await app.fetch(new Request(`http://localhost/${providerId}`), { user: testUser } as any)
expect(res.status).toBe(200)
const data = await res.json()
expect(data.id).toBe(providerId)
expect(data.isSystem).toBe(false)
// Test system config access
const resSys = await app.fetch(new Request('http://localhost/sys-1'), { user: testUser } as any)
expect(resSys.status).toBe(200)
const dataSys = await resSys.json()
expect(dataSys.id).toBe('sys-1')
expect(dataSys.isSystem).toBe(true)
})
it('patch /:id should update provider config', async () => {
const providers = await providerService.findByOwnerId(testUser.id)
const providers = await providerService.findUserConfigsByOwnerId(testUser.id)
const providerId = providers[0].id
const res = await app.fetch(new Request(`http://localhost/${providerId}`, {
@@ -112,7 +129,7 @@ describe('providerRoutes', () => {
}), { user: testUser } as any)
expect(res.status).toBe(200)
const updated = await providerService.findById(providerId)
const updated = await providerService.findUserConfigById(providerId)
expect(updated?.name).toBe('Updated Name')
})
@@ -124,7 +141,7 @@ describe('providerRoutes', () => {
email: 'other@example.com',
}).returning()
const providers = await providerService.findByOwnerId(testUser.id)
const providers = await providerService.findUserConfigsByOwnerId(testUser.id)
const providerId = providers[0].id
const res = await app.fetch(new Request(`http://localhost/${providerId}`, {
@@ -137,7 +154,7 @@ describe('providerRoutes', () => {
})
it('delete /:id should soft delete provider', async () => {
const providers = await providerService.findByOwnerId(testUser.id)
const providers = await providerService.findUserConfigsByOwnerId(testUser.id)
const providerId = providers[0].id
const res = await app.fetch(new Request(`http://localhost/${providerId}`, {
@@ -145,7 +162,7 @@ describe('providerRoutes', () => {
}), { user: testUser } as any)
expect(res.status).toBe(204)
const deleted = await providerService.findById(providerId)
const deleted = await providerService.findUserConfigById(providerId)
expect(deleted).toBeUndefined()
})
})
+9 -8
View File
@@ -14,13 +14,14 @@ export function createProviderRoutes(providerService: ProviderService) {
.get('/', async (c) => {
const user = c.get('user')!
const providers = await providerService.findByOwnerId(user.id)
const providers = await providerService.findAll(user.id)
return c.json(providers)
})
.get('/:id', async (c) => {
const user = c.get('user')!
const id = c.req.param('id')
const provider = await providerService.findById(id)
const provider = await providerService.findById(id, user.id)
if (!provider)
throw createNotFoundError()
@@ -36,10 +37,10 @@ export function createProviderRoutes(providerService: ProviderService) {
throw createBadRequestError('Invalid Request', 'INVALID_REQUEST', result.issues)
}
const provider = await providerService.create({
const provider = await providerService.createUserConfig({
...result.output,
ownerId: user.id,
})
} as any)
return c.json(provider, 201)
})
@@ -54,13 +55,13 @@ export function createProviderRoutes(providerService: ProviderService) {
throw createBadRequestError('Invalid Request', 'INVALID_REQUEST', result.issues)
}
const existing = await providerService.findById(id)
const existing = await providerService.findUserConfigById(id)
if (!existing)
throw createNotFoundError()
if (existing.ownerId !== user.id)
throw createForbiddenError()
const updated = await providerService.update(id, result.output)
const updated = await providerService.updateUserConfig(id, result.output)
return c.json(updated)
})
@@ -68,13 +69,13 @@ export function createProviderRoutes(providerService: ProviderService) {
const user = c.get('user')!
const id = c.req.param('id')
const existing = await providerService.findById(id)
const existing = await providerService.findUserConfigById(id)
if (!existing)
throw createNotFoundError()
if (existing.ownerId !== user.id)
throw createForbiddenError()
await providerService.delete(id)
await providerService.deleteUserConfig(id)
return c.body(null, 204)
})
}
+37 -55
View File
@@ -4,10 +4,11 @@ import type { AvatarModelConfig } from '../types/character-avatar-model'
import type { CharacterCapabilityConfig } from '../types/character-capability'
import { relations } from 'drizzle-orm'
import { integer, jsonb, pgTable, primaryKey, text, timestamp } from 'drizzle-orm/pg-core'
import { integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
import { nanoid } from '../utils/id'
import { user } from './accounts'
import { characterBookmarks, characterLikes } from './user-character'
export const character = pgTable(
'characters',
@@ -22,8 +23,6 @@ export const character = pgTable(
ownerId: text('owner_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
characterId: text('character_id').notNull(),
avatarUrl: text('avatar_url'),
characterAvatarUrl: text('character_avatar_url'),
coverBackgroundUrl: text('cover_background_url'),
creatorRole: text('creator_role'),
priceCredit: text('price_credit').default('0').notNull(),
@@ -41,6 +40,23 @@ export const character = pgTable(
export type Character = InferSelectModel<typeof character>
export type NewCharacter = InferInsertModel<typeof character>
export const characterCovers = pgTable(
'character_covers',
{
id: text('id').primaryKey().$defaultFn(() => nanoid()),
characterId: text('character_id').notNull().references(() => character.id, { onDelete: 'cascade' }),
foregroundUrl: text('foreground_url').notNull(),
backgroundUrl: text('background_url').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
deletedAt: timestamp('deleted_at'),
},
)
export type CharacterCover = InferSelectModel<typeof characterCovers>
export type NewCharacterCover = InferInsertModel<typeof characterCovers>
export const avatarModel = pgTable(
'avatar_model',
{
@@ -124,30 +140,6 @@ export const characterPrompts = pgTable(
},
)
export const characterLikes = pgTable(
'character_likes',
{
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
characterId: text('character_id').notNull().references(() => character.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
},
table => [
primaryKey({ columns: [table.userId, table.characterId] }),
],
)
export const characterBookmarks = pgTable(
'character_bookmarks',
{
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
characterId: text('character_id').notNull().references(() => character.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
},
table => [
primaryKey({ columns: [table.userId, table.characterId] }),
],
)
export type CharacterPrompt = InferSelectModel<typeof characterPrompts>
export type NewCharacterPrompt = InferInsertModel<typeof characterPrompts>
@@ -164,6 +156,24 @@ export const characterRelations = relations(
fields: [character.ownerId],
references: [user.id],
}),
creator: one(user, {
fields: [character.creatorId],
references: [user.id],
}),
cover: one(characterCovers, {
fields: [character.id],
references: [characterCovers.characterId],
}),
}),
)
export const characterCoversRelations = relations(
characterCovers,
({ one }) => ({
character: one(character, {
fields: [characterCovers.characterId],
references: [character.id],
}),
}),
)
@@ -206,31 +216,3 @@ export const characterPromptsRelations = relations(
}),
}),
)
export const characterLikesRelations = relations(
characterLikes,
({ one }) => ({
user: one(user, {
fields: [characterLikes.userId],
references: [user.id],
}),
character: one(character, {
fields: [characterLikes.characterId],
references: [character.id],
}),
}),
)
export const characterBookmarksRelations = relations(
characterBookmarks,
({ one }) => ({
user: one(user, {
fields: [characterBookmarks.userId],
references: [user.id],
}),
character: one(character, {
fields: [characterBookmarks.characterId],
references: [character.id],
}),
}),
)
+1
View File
@@ -2,3 +2,4 @@ export * from './accounts'
export * from './characters'
export * from './chats'
export * from './providers'
export * from './user-character'
+27 -7
View File
@@ -6,8 +6,8 @@ import { boolean, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
import { nanoid } from '../utils/id'
import { user } from './accounts'
export const providerConfigs = pgTable(
'provider_configs',
export const userProviderConfigs = pgTable(
'user_provider_configs',
{
id: text('id').primaryKey().$defaultFn(() => nanoid()),
ownerId: text('owner_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
@@ -16,21 +16,41 @@ export const providerConfigs = pgTable(
config: jsonb('config').notNull().default({}),
validated: boolean('validated').notNull().default(false),
validationBypassed: boolean('validation_bypassed').notNull().default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
deletedAt: timestamp('deleted_at'),
},
)
export type ProviderConfig = InferSelectModel<typeof providerConfigs>
export type NewProviderConfig = InferInsertModel<typeof providerConfigs>
export type UserProviderConfig = InferSelectModel<typeof userProviderConfigs>
export type NewUserProviderConfig = InferInsertModel<typeof userProviderConfigs>
export const providerConfigsRelations = relations(
providerConfigs,
export const userProviderConfigsRelations = relations(
userProviderConfigs,
({ one }) => ({
owner: one(user, {
fields: [providerConfigs.ownerId],
fields: [userProviderConfigs.ownerId],
references: [user.id],
}),
}),
)
export const systemProviderConfigs = pgTable(
'system_provider_configs',
{
id: text('id').primaryKey().$defaultFn(() => nanoid()),
definitionId: text('definition_id').notNull(),
name: text('name').notNull(),
config: jsonb('config').notNull().default({}),
validated: boolean('validated').notNull().default(false),
validationBypassed: boolean('validation_bypassed').notNull().default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
deletedAt: timestamp('deleted_at'),
},
)
export type SystemProviderConfig = InferSelectModel<typeof systemProviderConfigs>
export type NewSystemProviderConfig = InferInsertModel<typeof systemProviderConfigs>
+65
View File
@@ -0,0 +1,65 @@
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
import { pgTable, primaryKey, text, timestamp } from 'drizzle-orm/pg-core'
import { relations } from 'drizzle-orm/relations'
import { user } from './accounts'
import { character } from './characters'
export const characterLikes = pgTable(
'user_character_likes',
{
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
characterId: text('character_id').notNull().references(() => character.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
},
table => [
primaryKey({ columns: [table.userId, table.characterId] }),
],
)
export type CharacterLike = InferSelectModel<typeof characterLikes>
export type NewCharacterLike = InferInsertModel<typeof characterLikes>
export const characterBookmarks = pgTable(
'user_character_bookmarks',
{
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
characterId: text('character_id').notNull().references(() => character.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
},
table => [
primaryKey({ columns: [table.userId, table.characterId] }),
],
)
export type CharacterBookmark = InferSelectModel<typeof characterBookmarks>
export type NewCharacterBookmark = InferInsertModel<typeof characterBookmarks>
export const characterLikesRelations = relations(
characterLikes,
({ one }) => ({
user: one(user, {
fields: [characterLikes.userId],
references: [user.id],
}),
character: one(character, {
fields: [characterLikes.characterId],
references: [character.id],
}),
}),
)
export const characterBookmarksRelations = relations(
characterBookmarks,
({ one }) => ({
user: one(user, {
fields: [characterBookmarks.userId],
references: [user.id],
}),
character: one(character, {
fields: [characterBookmarks.characterId],
references: [character.id],
}),
}),
)
@@ -36,12 +36,14 @@ describe('characterService', () => {
const result = await service.create({
character: characterData,
i18n: [{ language: 'en', name: 'Aster', description: 'desc', tags: [] }],
cover: { foregroundUrl: 'fg', backgroundUrl: 'bg' },
})
expect(result.id).toBe('char-1')
const found = await service.findById('char-1')
expect(found?.i18n[0].name).toBe('Aster')
expect(found?.cover?.foregroundUrl).toBe('fg')
})
it('findAll should return characters with relations', async () => {
@@ -23,7 +23,7 @@ describe('providerService', () => {
testUser = user
})
it('create should handle provider config creation', async () => {
it('createUserConfig should handle provider config creation', async () => {
const providerData = {
id: 'prov-1',
ownerId: testUser.id,
@@ -34,30 +34,58 @@ describe('providerService', () => {
validationBypassed: false,
}
const result = await service.create(providerData)
const result = await service.createUserConfig(providerData)
expect(result.id).toBe('prov-1')
expect(result.name).toBe('My OpenAI')
const found = await service.findById('prov-1')
const found = await service.findUserConfigById('prov-1')
expect(found?.definitionId).toBe('openai')
expect((found?.config as any).apiKey).toBe('sk-123')
})
it('findByOwnerId should return providers for the user', async () => {
const result = await service.findByOwnerId(testUser.id)
it('findUserConfigsByOwnerId should return providers for the user', async () => {
const result = await service.findUserConfigsByOwnerId(testUser.id)
expect(result.length).toBe(1)
expect(result[0].ownerId).toBe(testUser.id)
})
it('update should update provider fields', async () => {
await service.update('prov-1', { name: 'Updated OpenAI' })
const prov = await service.findById('prov-1')
it('findAll should return both user and system configs', async () => {
// Create a system config
await db.insert(schema.systemProviderConfigs).values({
id: 'sys-1',
definitionId: 'anthropic',
name: 'System Anthropic',
config: { apiKey: 'sys-sk' },
})
const result = await service.findAll(testUser.id)
expect(result.length).toBe(2)
const userConfig = result.find(r => r.id === 'prov-1')
const systemConfig = result.find(r => r.id === 'sys-1')
expect(userConfig?.isSystem).toBe(false)
expect(systemConfig?.isSystem).toBe(true)
expect(systemConfig?.name).toBe('System Anthropic')
})
it('findById should find both user and system configs', async () => {
const userFound = await service.findById('prov-1', testUser.id)
expect(userFound?.isSystem).toBe(false)
const sysFound = await service.findById('sys-1', testUser.id)
expect(sysFound?.isSystem).toBe(true)
})
it('updateUserConfig should update provider fields', async () => {
await service.updateUserConfig('prov-1', { name: 'Updated OpenAI' })
const prov = await service.findUserConfigById('prov-1')
expect(prov?.name).toBe('Updated OpenAI')
})
it('delete should soft delete provider', async () => {
await service.delete('prov-1')
const prov = await service.findById('prov-1')
it('deleteUserConfig should soft delete provider', async () => {
await service.deleteUserConfig('prov-1')
const prov = await service.findUserConfigById('prov-1')
expect(prov).toBeUndefined()
})
})
+24 -12
View File
@@ -4,6 +4,7 @@ import type { Database } from './db'
import { and, eq, isNull, sql } from 'drizzle-orm'
import * as schema from '../schemas/characters'
import * as userCharacterSchema from '../schemas/user-character'
export function createCharacterService(db: Database<typeof fullSchema>) {
return {
@@ -21,6 +22,7 @@ export function createCharacterService(db: Database<typeof fullSchema>) {
prompts: true,
likes: true,
bookmarks: true,
cover: true,
}
: undefined,
})
@@ -37,6 +39,7 @@ export function createCharacterService(db: Database<typeof fullSchema>) {
capabilities: true,
likes: true,
bookmarks: true,
cover: true,
},
})
},
@@ -50,6 +53,7 @@ export function createCharacterService(db: Database<typeof fullSchema>) {
capabilities: true,
likes: true,
bookmarks: true,
cover: true,
}
: undefined,
})
@@ -59,16 +63,16 @@ export function createCharacterService(db: Database<typeof fullSchema>) {
return await db.transaction(async (tx) => {
const existing = await tx.query.characterLikes.findFirst({
where: and(
eq(schema.characterLikes.userId, userId),
eq(schema.characterLikes.characterId, characterId),
eq(userCharacterSchema.characterLikes.userId, userId),
eq(userCharacterSchema.characterLikes.characterId, characterId),
),
})
if (existing) {
await tx.delete(schema.characterLikes)
await tx.delete(userCharacterSchema.characterLikes)
.where(and(
eq(schema.characterLikes.userId, userId),
eq(schema.characterLikes.characterId, characterId),
eq(userCharacterSchema.characterLikes.userId, userId),
eq(userCharacterSchema.characterLikes.characterId, characterId),
))
await tx.update(schema.character)
@@ -80,7 +84,7 @@ export function createCharacterService(db: Database<typeof fullSchema>) {
return { liked: false }
}
else {
await tx.insert(schema.characterLikes).values({ userId, characterId })
await tx.insert(userCharacterSchema.characterLikes).values({ userId, characterId })
await tx.update(schema.character)
.set({
@@ -97,16 +101,16 @@ export function createCharacterService(db: Database<typeof fullSchema>) {
return await db.transaction(async (tx) => {
const existing = await tx.query.characterBookmarks.findFirst({
where: and(
eq(schema.characterBookmarks.userId, userId),
eq(schema.characterBookmarks.characterId, characterId),
eq(userCharacterSchema.characterBookmarks.userId, userId),
eq(userCharacterSchema.characterBookmarks.characterId, characterId),
),
})
if (existing) {
await tx.delete(schema.characterBookmarks)
await tx.delete(userCharacterSchema.characterBookmarks)
.where(and(
eq(schema.characterBookmarks.userId, userId),
eq(schema.characterBookmarks.characterId, characterId),
eq(userCharacterSchema.characterBookmarks.userId, userId),
eq(userCharacterSchema.characterBookmarks.characterId, characterId),
))
await tx.update(schema.character)
@@ -118,7 +122,7 @@ export function createCharacterService(db: Database<typeof fullSchema>) {
return { bookmarked: false }
}
else {
await tx.insert(schema.characterBookmarks).values({ userId, characterId })
await tx.insert(userCharacterSchema.characterBookmarks).values({ userId, characterId })
await tx.update(schema.character)
.set({
@@ -133,6 +137,7 @@ export function createCharacterService(db: Database<typeof fullSchema>) {
async create(data: {
character: schema.NewCharacter
cover?: Omit<schema.NewCharacterCover, 'characterId'>
capabilities?: Omit<schema.NewCharacterCapability, 'characterId'>[]
avatarModels?: Omit<schema.NewAvatarModel, 'characterId'>[]
i18n?: Omit<schema.NewCharacterI18n, 'characterId'>[]
@@ -141,6 +146,13 @@ export function createCharacterService(db: Database<typeof fullSchema>) {
return await db.transaction(async (tx) => {
const [inserted] = await tx.insert(schema.character).values(data.character).returning()
if (data.cover) {
await tx.insert(schema.characterCovers).values({
...data.cover,
characterId: inserted.id,
} as schema.NewCharacterCover)
}
if (data.capabilities?.length) {
await tx.insert(schema.characterCapabilities).values(
data.capabilities.map(c => ({ ...c, characterId: inserted.id }) as schema.NewCharacterCapability),
+127 -19
View File
@@ -1,52 +1,160 @@
import type * as fullSchema from '../schemas'
import type { Database } from './db'
import { and, eq, isNull } from 'drizzle-orm'
import { and, eq, isNull, sql } from 'drizzle-orm'
import * as schema from '../schemas/providers'
export function createProviderService(db: Database<typeof fullSchema>) {
return {
async findByOwnerId(ownerId: string) {
return await db.query.providerConfigs.findMany({
async findAll(ownerId: string) {
const userConfigs = db
.select({
id: schema.userProviderConfigs.id,
definitionId: schema.userProviderConfigs.definitionId,
name: schema.userProviderConfigs.name,
config: schema.userProviderConfigs.config,
validated: schema.userProviderConfigs.validated,
validationBypassed: schema.userProviderConfigs.validationBypassed,
createdAt: schema.userProviderConfigs.createdAt,
updatedAt: schema.userProviderConfigs.updatedAt,
isSystem: sql<boolean>`false`.as('is_system'),
})
.from(schema.userProviderConfigs)
.where(
and(
eq(schema.userProviderConfigs.ownerId, ownerId),
isNull(schema.userProviderConfigs.deletedAt),
),
)
const systemConfigs = db
.select({
id: schema.systemProviderConfigs.id,
definitionId: schema.systemProviderConfigs.definitionId,
name: schema.systemProviderConfigs.name,
config: schema.systemProviderConfigs.config,
validated: schema.systemProviderConfigs.validated,
validationBypassed: schema.systemProviderConfigs.validationBypassed,
createdAt: schema.systemProviderConfigs.createdAt,
updatedAt: schema.systemProviderConfigs.updatedAt,
isSystem: sql<boolean>`true`.as('is_system'),
})
.from(schema.systemProviderConfigs)
.where(isNull(schema.systemProviderConfigs.deletedAt))
return await userConfigs.unionAll(systemConfigs)
},
async findUserConfigsByOwnerId(ownerId: string) {
return await db.query.userProviderConfigs.findMany({
where: and(
eq(schema.providerConfigs.ownerId, ownerId),
isNull(schema.providerConfigs.deletedAt),
eq(schema.userProviderConfigs.ownerId, ownerId),
isNull(schema.userProviderConfigs.deletedAt),
),
})
},
async findById(id: string) {
return await db.query.providerConfigs.findFirst({
async findById(id: string, ownerId: string) {
const userConfig = await db.query.userProviderConfigs.findFirst({
where: and(
eq(schema.providerConfigs.id, id),
isNull(schema.providerConfigs.deletedAt),
eq(schema.userProviderConfigs.id, id),
eq(schema.userProviderConfigs.ownerId, ownerId),
isNull(schema.userProviderConfigs.deletedAt),
),
})
if (userConfig) {
return { ...userConfig, isSystem: false }
}
const systemConfig = await db.query.systemProviderConfigs.findFirst({
where: and(
eq(schema.systemProviderConfigs.id, id),
isNull(schema.systemProviderConfigs.deletedAt),
),
})
if (systemConfig) {
return { ...systemConfig, isSystem: true }
}
return null
},
async findUserConfigById(id: string) {
return await db.query.userProviderConfigs.findFirst({
where: and(
eq(schema.userProviderConfigs.id, id),
isNull(schema.userProviderConfigs.deletedAt),
),
})
},
async create(data: schema.NewProviderConfig) {
const [inserted] = await db.insert(schema.providerConfigs).values(data).returning()
async createUserConfig(data: schema.NewUserProviderConfig) {
const [inserted] = await db.insert(schema.userProviderConfigs).values(data).returning()
return inserted
},
async update(id: string, data: Partial<schema.NewProviderConfig>) {
const [updated] = await db.update(schema.providerConfigs)
async updateUserConfig(id: string, data: Partial<schema.NewUserProviderConfig>) {
const [updated] = await db.update(schema.userProviderConfigs)
.set({ ...data, updatedAt: new Date() })
.where(and(
eq(schema.providerConfigs.id, id),
isNull(schema.providerConfigs.deletedAt),
eq(schema.userProviderConfigs.id, id),
isNull(schema.userProviderConfigs.deletedAt),
))
.returning()
return updated
},
async delete(id: string) {
return await db.update(schema.providerConfigs)
async deleteUserConfig(id: string) {
return await db.update(schema.userProviderConfigs)
.set({ deletedAt: new Date() })
.where(and(
eq(schema.providerConfigs.id, id),
isNull(schema.providerConfigs.deletedAt),
eq(schema.userProviderConfigs.id, id),
isNull(schema.userProviderConfigs.deletedAt),
))
.returning()
},
// System Provider Configs
async findSystemConfigs() {
return await db.query.systemProviderConfigs.findMany({
where: isNull(schema.systemProviderConfigs.deletedAt),
})
},
async findSystemConfigById(id: string) {
return await db.query.systemProviderConfigs.findFirst({
where: and(
eq(schema.systemProviderConfigs.id, id),
isNull(schema.systemProviderConfigs.deletedAt),
),
})
},
async createSystemConfig(data: schema.NewSystemProviderConfig) {
const [inserted] = await db.insert(schema.systemProviderConfigs).values(data).returning()
return inserted
},
async updateSystemConfig(id: string, data: Partial<schema.NewSystemProviderConfig>) {
const [updated] = await db.update(schema.systemProviderConfigs)
.set({ ...data, updatedAt: new Date() })
.where(and(
eq(schema.systemProviderConfigs.id, id),
isNull(schema.systemProviderConfigs.deletedAt),
))
.returning()
return updated
},
async deleteSystemConfig(id: string) {
return await db.update(schema.systemProviderConfigs)
.set({ deletedAt: new Date() })
.where(and(
eq(schema.systemProviderConfigs.id, id),
isNull(schema.systemProviderConfigs.deletedAt),
))
.returning()
},