Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions lib/artists/__tests__/createArtistInDb.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { describe, it, expect, vi, beforeEach } from "vitest";

const mockInsertAccount = vi.fn();
const mockInsertAccountInfo = vi.fn();
const mockSelectAccountWithSocials = vi.fn();
const mockInsertAccountArtistId = vi.fn();
const mockAddArtistToOrganization = vi.fn();

vi.mock("@/lib/supabase/accounts/insertAccount", () => ({
insertAccount: (...args: unknown[]) => mockInsertAccount(...args),
}));

vi.mock("@/lib/supabase/account_info/insertAccountInfo", () => ({
insertAccountInfo: (...args: unknown[]) => mockInsertAccountInfo(...args),
}));

vi.mock("@/lib/supabase/accounts/selectAccountWithSocials", () => ({
selectAccountWithSocials: (...args: unknown[]) => mockSelectAccountWithSocials(...args),
}));

vi.mock("@/lib/supabase/account_artist_ids/insertAccountArtistId", () => ({
insertAccountArtistId: (...args: unknown[]) => mockInsertAccountArtistId(...args),
}));

vi.mock("@/lib/supabase/artist_organization_ids/addArtistToOrganization", () => ({
addArtistToOrganization: (...args: unknown[]) => mockAddArtistToOrganization(...args),
}));

import { createArtistInDb } from "../createArtistInDb";

describe("createArtistInDb", () => {
const mockAccount = {
id: "artist-123",
name: "Test Artist",
created_at: "2026-01-15T00:00:00Z",
updated_at: "2026-01-15T00:00:00Z",
};

const mockAccountInfo = {
id: "info-123",
account_id: "artist-123",
image: null,
instruction: null,
knowledges: null,
label: null,
organization: null,
company_name: null,
job_title: null,
role_type: null,
onboarding_status: null,
onboarding_data: null,
};

const mockFullAccount = {
...mockAccount,
account_socials: [],
account_info: [mockAccountInfo],
};

beforeEach(() => {
vi.clearAllMocks();
});

it("creates an artist account with all required steps", async () => {
mockInsertAccount.mockResolvedValue(mockAccount);
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount);
mockInsertAccountArtistId.mockResolvedValue({ id: "rel-123" });

const result = await createArtistInDb("Test Artist", "owner-456");

expect(mockInsertAccount).toHaveBeenCalledWith({ name: "Test Artist" });
expect(mockInsertAccountInfo).toHaveBeenCalledWith({ account_id: "artist-123" });
expect(mockSelectAccountWithSocials).toHaveBeenCalledWith("artist-123");
expect(mockInsertAccountArtistId).toHaveBeenCalledWith("owner-456", "artist-123");
expect(result).toMatchObject({
id: "artist-123",
account_id: "artist-123",
name: "Test Artist",
});
});

it("links artist to organization when organizationId is provided", async () => {
mockInsertAccount.mockResolvedValue(mockAccount);
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount);
mockInsertAccountArtistId.mockResolvedValue({ id: "rel-123" });
mockAddArtistToOrganization.mockResolvedValue("org-rel-123");

const result = await createArtistInDb("Test Artist", "owner-456", "org-789");

expect(mockAddArtistToOrganization).toHaveBeenCalledWith("artist-123", "org-789");
expect(result).not.toBeNull();
});

it("returns null when account creation fails", async () => {
mockInsertAccount.mockRejectedValue(new Error("Insert failed"));

const result = await createArtistInDb("Test Artist", "owner-456");

expect(result).toBeNull();
expect(mockInsertAccountInfo).not.toHaveBeenCalled();
});

it("returns null when account info creation fails", async () => {
mockInsertAccount.mockResolvedValue(mockAccount);
mockInsertAccountInfo.mockResolvedValue(null);

const result = await createArtistInDb("Test Artist", "owner-456");

expect(result).toBeNull();
expect(mockSelectAccountWithSocials).not.toHaveBeenCalled();
});

it("returns null when fetching full account data fails", async () => {
mockInsertAccount.mockResolvedValue(mockAccount);
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
mockSelectAccountWithSocials.mockResolvedValue(null);

const result = await createArtistInDb("Test Artist", "owner-456");

expect(result).toBeNull();
expect(mockInsertAccountArtistId).not.toHaveBeenCalled();
});

it("returns null when associating artist with owner fails", async () => {
mockInsertAccount.mockResolvedValue(mockAccount);
mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount);
mockInsertAccountArtistId.mockRejectedValue(new Error("Association failed"));

const result = await createArtistInDb("Test Artist", "owner-456");

expect(result).toBeNull();
});
});
57 changes: 57 additions & 0 deletions lib/artists/createArtistInDb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { insertAccount } from "@/lib/supabase/accounts/insertAccount";
import { insertAccountInfo } from "@/lib/supabase/account_info/insertAccountInfo";
import {
selectAccountWithSocials,
type AccountWithSocials,
} from "@/lib/supabase/accounts/selectAccountWithSocials";
import { insertAccountArtistId } from "@/lib/supabase/account_artist_ids/insertAccountArtistId";
import { addArtistToOrganization } from "@/lib/supabase/artist_organization_ids/addArtistToOrganization";

/**
* Result of creating an artist in the database.
*/
export type CreateArtistResult = AccountWithSocials & {
account_id: string;
};

/**
* Create a new artist account in the database and associate it with an owner account.
*
* @param name - Name of the artist to create
* @param accountId - ID of the owner account that will have access to this artist
* @param organizationId - Optional organization ID to link the new artist to
* @returns Created artist object or null if creation failed
*/
export async function createArtistInDb(
name: string,
accountId: string,
organizationId?: string,
): Promise<CreateArtistResult | null> {
try {
// Step 1: Create the account
const account = await insertAccount({ name });

// Step 2: Create account info for the account
const accountInfo = await insertAccountInfo({ account_id: account.id });
if (!accountInfo) return null;

// Step 3: Get the full account data with socials and info
const artist = await selectAccountWithSocials(account.id);
if (!artist) return null;

// Step 4: Associate the artist with the owner via account_artist_ids
await insertAccountArtistId(accountId, account.id);

// Step 5: Link to organization if provided
if (organizationId) {
await addArtistToOrganization(account.id, organizationId);
}

return {
...artist,
account_id: artist.id,
};
} catch (error) {
return null;
}
}
151 changes: 151 additions & 0 deletions lib/mcp/tools/artists/__tests__/registerCreateNewArtistTool.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const mockCreateArtistInDb = vi.fn();
const mockCopyRoom = vi.fn();

vi.mock("@/lib/artists/createArtistInDb", () => ({
createArtistInDb: (...args: unknown[]) => mockCreateArtistInDb(...args),
}));

vi.mock("@/lib/rooms/copyRoom", () => ({
copyRoom: (...args: unknown[]) => mockCopyRoom(...args),
}));

import { registerCreateNewArtistTool } from "../registerCreateNewArtistTool";

describe("registerCreateNewArtistTool", () => {
let mockServer: McpServer;
let registeredHandler: (args: unknown) => Promise<unknown>;

beforeEach(() => {
vi.clearAllMocks();

mockServer = {
registerTool: vi.fn((name, config, handler) => {
registeredHandler = handler;
}),
} as unknown as McpServer;

registerCreateNewArtistTool(mockServer);
});

it("registers the create_new_artist tool", () => {
expect(mockServer.registerTool).toHaveBeenCalledWith(
"create_new_artist",
expect.objectContaining({
description: expect.stringContaining("Create a new artist account"),
}),
expect.any(Function),
);
});

it("creates an artist and returns success", async () => {
const mockArtist = {
id: "artist-123",
account_id: "artist-123",
name: "Test Artist",
account_info: [{ image: null }],
account_socials: [],
};
mockCreateArtistInDb.mockResolvedValue(mockArtist);

const result = await registeredHandler({
name: "Test Artist",
account_id: "owner-456",
});

expect(mockCreateArtistInDb).toHaveBeenCalledWith("Test Artist", "owner-456", undefined);
expect(result).toEqual({
content: [
{
type: "text",
text: expect.stringContaining("Successfully created artist"),
},
],
});
});

it("copies room when active_conversation_id is provided", async () => {
const mockArtist = {
id: "artist-123",
account_id: "artist-123",
name: "Test Artist",
account_info: [{ image: null }],
account_socials: [],
};
mockCreateArtistInDb.mockResolvedValue(mockArtist);
mockCopyRoom.mockResolvedValue("new-room-789");

const result = await registeredHandler({
name: "Test Artist",
account_id: "owner-456",
active_conversation_id: "source-room-111",
});

expect(mockCopyRoom).toHaveBeenCalledWith("source-room-111", "artist-123");
expect(result).toEqual({
content: [
{
type: "text",
text: expect.stringContaining("new-room-789"),
},
],
});
});

it("passes organization_id to createArtistInDb", async () => {
const mockArtist = {
id: "artist-123",
account_id: "artist-123",
name: "Test Artist",
account_info: [{ image: null }],
account_socials: [],
};
mockCreateArtistInDb.mockResolvedValue(mockArtist);

await registeredHandler({
name: "Test Artist",
account_id: "owner-456",
organization_id: "org-999",
});

expect(mockCreateArtistInDb).toHaveBeenCalledWith("Test Artist", "owner-456", "org-999");
});

it("returns error when artist creation fails", async () => {
mockCreateArtistInDb.mockResolvedValue(null);

const result = await registeredHandler({
name: "Test Artist",
account_id: "owner-456",
});

expect(result).toEqual({
content: [
{
type: "text",
text: expect.stringContaining("Failed to create artist"),
},
],
});
});

it("returns error with message when exception is thrown", async () => {
mockCreateArtistInDb.mockRejectedValue(new Error("Database connection failed"));

const result = await registeredHandler({
name: "Test Artist",
account_id: "owner-456",
});

expect(result).toEqual({
content: [
{
type: "text",
text: expect.stringContaining("Database connection failed"),
},
],
});
});
});
11 changes: 11 additions & 0 deletions lib/mcp/tools/artists/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerCreateNewArtistTool } from "./registerCreateNewArtistTool";

/**
* Registers all artist-related MCP tools on the server.
*
* @param server - The MCP server instance to register tools on.
*/
export const registerAllArtistTools = (server: McpServer): void => {
registerCreateNewArtistTool(server);
};
Loading