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
8 changes: 4 additions & 4 deletions convex/lyrics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { query, mutation } from "./_generated/server";
import { query, internalMutation } from "./_generated/server";
import { v, ConvexError } from "convex/values";

export const getBySong = query({
Expand All @@ -11,7 +11,7 @@ export const getBySong = query({
},
});

export const create = mutation({
export const create = internalMutation({
args: {
songId: v.id("songs"),
lineNumber: v.number(),
Expand All @@ -27,7 +27,7 @@ export const create = mutation({
},
});

export const createMany = mutation({
export const createMany = internalMutation({
args: {
songId: v.id("songs"),
lyrics: v.array(
Expand Down Expand Up @@ -57,7 +57,7 @@ export const createMany = mutation({

// Update timestamps for specific lines (used for fixing timestamp issues)
// LOCKED: Requires explicit unlock if song.timestampsLocked is true
export const updateTimestamps = mutation({
export const updateTimestamps = internalMutation({
args: {
songId: v.id("songs"),
updates: v.array(
Expand Down
81 changes: 81 additions & 0 deletions convex/maintenanceVisibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import * as oneTimeMigration from "./oneTimeMigration";
import * as seed from "./seed";
import * as testData from "./testData";
import * as migration from "./migration";
import * as wordProgress from "./wordProgress";
import * as users from "./users";
import * as words from "./words";
import * as transcription from "./transcription";
import * as songs from "./songs";
import * as lyrics from "./lyrics";

// Inspect real registrations without executing any maintenance or PII handler.
const modules: Array<{
name: string;
exports: Readonly<Record<string, unknown>>;
publicNames: string[];
}> = [
{ name: "oneTimeMigration", exports: oneTimeMigration, publicNames: [] },
{ name: "seed", exports: seed, publicNames: [] },
{ name: "testData", exports: testData, publicNames: [] },
{ name: "migration", exports: migration, publicNames: ["migrateAnonymousData"] },
{ name: "users", exports: users, publicNames: ["checkEmailExists", "ensureAppUser"] },
{ name: "songs", exports: songs, publicNames: ["list", "getById"] },
{ name: "lyrics", exports: lyrics, publicNames: ["getBySong"] },
{ name: "words", exports: words, publicNames: ["getByLine", "getBySong"] },
{ name: "transcription", exports: transcription, publicNames: [] },
{
name: "wordProgress", exports: wordProgress,
publicNames: [
"getByUser", "getByUserWord", "getByUserPersian", "getByUserPersians",
"getByUserWords", "incrementViewCount", "incrementPlayCount", "toggleLearned",
"setLearned", "getVocabularyByLanguage", "getWordDetailsWithSongs",
"getWordPracticeHistory",
],
},
];

function registrations(exports: Readonly<Record<string, unknown>>) {
return Object.entries(exports).filter(([, fn]) => typeof fn === "function" && (
"isQuery" in fn || "isMutation" in fn || "isAction" in fn
));
}

const functions = modules.flatMap(({ name: module, exports, publicNames }) =>
registrations(exports).map(([name, fn]) => ({
name: `${module}.${name}`, fn, isPublic: publicNames.includes(name),
})),
);

describe("maintenance API boundary", () => {
it.each(modules)("$name exposes only its approved public API", ({ exports, publicNames }) => {
const actual = registrations(exports)
.filter(([, fn]) => typeof fn === "function" && "isPublic" in fn && fn.isPublic === true)
.map(([name]) => name);
expect(actual.sort()).toEqual([...publicNames].sort());
});

it.each(functions)(
"$name has its approved client visibility",
({ fn, isPublic }) => {
expect(fn).toHaveProperty(isPublic ? "isPublic" : "isInternal", true);
expect(fn).not.toHaveProperty(isPublic ? "isInternal" : "isPublic", true);
},
);

it("has no frontend references to internal functions", () => {
const sources = import.meta.glob<string>("../src/**/*.{ts,tsx}", {
eager: true,
query: "?raw",
import: "default",
});
const internalNames = new Set(functions.filter((fn) => !fn.isPublic).map((fn) => fn.name));
const references = Object.entries(sources).flatMap(([file, source]) =>
[...source.matchAll(/\bapi\.(\w+)\.(\w+)/g)]
.filter((match) => internalNames.has(`${match[1]}.${match[2]}`))
.map((match) => `${file}: ${match[0]}`),
);
expect(references).toEqual([]);
});
});
10 changes: 5 additions & 5 deletions convex/migration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mutation, internalMutation, query } from "./_generated/server";
import { mutation, internalMutation, internalQuery } from "./_generated/server";
import { v } from "convex/values";
import { requireAuth } from "./authHelpers";
import type { Id } from "./_generated/dataModel";
Expand Down Expand Up @@ -110,10 +110,10 @@ export const migrateUserIdFromBetterAuthToAppUsers = internalMutation({
});

/**
* Public wrapper to run the migration (for CLI execution)
* Internal maintenance migration (for authenticated CLI execution)
* Usage: npx convex run migration:runUserIdMigration '{"oldUserId": "...", "newUserId": "..."}'
*/
export const runUserIdMigration = mutation({
export const runUserIdMigration = internalMutation({
args: {
oldUserId: v.string(),
newUserId: v.string(),
Expand Down Expand Up @@ -213,7 +213,7 @@ export const runUserIdMigration = mutation({
/**
* Query to verify progress data counts for a user
*/
export const verifyUserProgressData = query({
export const verifyUserProgressData = internalQuery({
args: { userId: v.string() },
handler: async (ctx, { userId }) => {
const wordProgress = await ctx.db
Expand Down Expand Up @@ -599,7 +599,7 @@ export const migrateAnonymousData = mutation({
* Update lyrics timestamps for a song
* Used for applying WhisperX-generated timestamps
*/
export const updateLyricsTimestamps = mutation({
export const updateLyricsTimestamps = internalMutation({
args: {
songTitle: v.string(),
timestamps: v.array(
Expand Down
22 changes: 11 additions & 11 deletions convex/oneTimeMigration.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { mutation, query } from "./_generated/server";
import { internalMutation, internalQuery } from "./_generated/server";
import { v } from "convex/values";
import { components } from "./_generated/api";

// One-time migration: Migrate visitorId data to a userId (Better Auth user ID)
export const migrateVisitorToUser = mutation({
export const migrateVisitorToUser = internalMutation({
args: {
visitorId: v.string(),
userId: v.string(), // Better Auth user ID directly
Expand Down Expand Up @@ -101,7 +101,7 @@ export const migrateVisitorToUser = mutation({
});

// Query to check what data exists for a visitorId
export const checkVisitorData = query({
export const checkVisitorData = internalQuery({
args: { visitorId: v.string() },
handler: async (ctx, { visitorId }) => {
const wordProgress = await ctx.db
Expand Down Expand Up @@ -157,7 +157,7 @@ export const checkVisitorData = query({
});

// Delete a user record by ID
export const deleteUserRecord = mutation({
export const deleteUserRecord = internalMutation({
args: { userId: v.id("users") },
handler: async (ctx, { userId }) => {
await ctx.db.delete(userId);
Expand All @@ -166,7 +166,7 @@ export const deleteUserRecord = mutation({
});

// Delete test user by email from BOTH Better Auth 'user' table AND app 'users' table
export const deleteTestUserByEmail = mutation({
export const deleteTestUserByEmail = internalMutation({
args: { email: v.string() },
handler: async (ctx, { email }) => {
const results = {
Expand Down Expand Up @@ -219,7 +219,7 @@ export const deleteTestUserByEmail = mutation({
});

// List all app users in the database
export const listAllAppUsers = query({
export const listAllAppUsers = internalQuery({
args: {},
handler: async (ctx) => {
const users = await ctx.db.query("users").collect();
Expand All @@ -228,7 +228,7 @@ export const listAllAppUsers = query({
});

// Fix app user record - backfill authId and correct email
export const fixAppUserRecord = mutation({
export const fixAppUserRecord = internalMutation({
args: {
appUserId: v.id("users"),
correctEmail: v.string(),
Expand All @@ -244,7 +244,7 @@ export const fixAppUserRecord = mutation({
});

// Check what data exists for a userId (in the userId field)
export const checkUserData = query({
export const checkUserData = internalQuery({
args: { userId: v.string() },
handler: async (ctx, { userId }) => {
const wordProgress = await ctx.db
Expand Down Expand Up @@ -302,7 +302,7 @@ export const checkUserData = query({
// BUG-026 FIX: Migrate from visitorId=BetterAuthID to userId=AppUsersID
// The old system stored Better Auth ID in the visitorId field
// The new system uses app users ID in the userId field
export const migrateVisitorIdToUserId = mutation({
export const migrateVisitorIdToUserId = internalMutation({
args: {
oldVisitorId: v.string(), // Better Auth ID that was stored in visitorId
newUserId: v.string(), // App users ID to store in userId
Expand Down Expand Up @@ -420,7 +420,7 @@ export const migrateVisitorIdToUserId = mutation({
});

// Migrate userId field from old Better Auth ID to new app users ID
export const migrateUserId = mutation({
export const migrateUserId = internalMutation({
args: {
oldUserId: v.string(),
newUserId: v.string(),
Expand Down Expand Up @@ -517,7 +517,7 @@ export const migrateUserId = mutation({
});

// List all unique visitorIds in the database
export const listAllVisitorIds = query({
export const listAllVisitorIds = internalQuery({
args: {},
handler: async (ctx) => {
const visitorIds = new Set<string>();
Expand Down
6 changes: 3 additions & 3 deletions convex/seed.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mutation } from "./_generated/server";
import { internalMutation } from "./_generated/server";

// Word-by-word breakdown for all Baraye lyrics
const barayeWords = [
Expand Down Expand Up @@ -328,7 +328,7 @@ const barayeLyrics = [
{ lineNumber: 31, startTime: 145.0, endTime: 151.46, original: "بـــرای آزادی", transliteration: "Barāye āzādi", hebrew: "בָּרָאיֶה אָזָאדִי", english: "For freedom", audioSnippetUrl: "/audio/baraye/baraye_031.mp3" },
];

export const seedBaraye = mutation({
export const seedBaraye = internalMutation({
args: {},
handler: async (ctx) => {
// Check if Baraye already exists (try new youtubeId first, then old)
Expand Down Expand Up @@ -402,7 +402,7 @@ export const seedBaraye = mutation({
});

// Seed word data for Baraye song
export const seedBarayeWords = mutation({
export const seedBarayeWords = internalMutation({
args: {},
handler: async (ctx) => {
// Find Baraye song
Expand Down
12 changes: 6 additions & 6 deletions convex/songs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { query, mutation } from "./_generated/server";
import { query, internalMutation } from "./_generated/server";
import { v } from "convex/values";

export const list = query({
Expand All @@ -15,7 +15,7 @@ export const getById = query({
},
});

export const create = mutation({
export const create = internalMutation({
args: {
title: v.string(),
artist: v.string(),
Expand All @@ -31,7 +31,7 @@ export const create = mutation({
});

// Delete a song and all its lyrics/words
export const deleteSong = mutation({
export const deleteSong = internalMutation({
args: { songId: v.id("songs") },
handler: async (ctx, args) => {
// Delete all lyrics for this song
Expand Down Expand Up @@ -59,7 +59,7 @@ export const deleteSong = mutation({
});

// Set the video URL for a song
export const setVideoUrl = mutation({
export const setVideoUrl = internalMutation({
args: { songId: v.id("songs"), videoUrl: v.string() },
handler: async (ctx, args) => {
await ctx.db.patch(args.songId, { videoUrl: args.videoUrl });
Expand All @@ -68,7 +68,7 @@ export const setVideoUrl = mutation({
});

// Lock timestamps to prevent AI from modifying them without explicit unlock
export const lockTimestamps = mutation({
export const lockTimestamps = internalMutation({
args: { songId: v.id("songs") },
handler: async (ctx, args) => {
await ctx.db.patch(args.songId, { timestampsLocked: true });
Expand All @@ -77,7 +77,7 @@ export const lockTimestamps = mutation({
});

// Unlock timestamps (requires user to explicitly request this)
export const unlockTimestamps = mutation({
export const unlockTimestamps = internalMutation({
args: { songId: v.id("songs") },
handler: async (ctx, args) => {
await ctx.db.patch(args.songId, { timestampsLocked: false });
Expand Down
4 changes: 2 additions & 2 deletions convex/testData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mutation } from "./_generated/server";
import { internalMutation } from "./_generated/server";

// Create test practice data for dashboard testing
export const createTestPracticeData = mutation({
export const createTestPracticeData = internalMutation({
args: {},
handler: async (ctx) => {
const visitorId = "test-visitor-123";
Expand Down
21 changes: 7 additions & 14 deletions convex/transcription.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { internalMutation, internalQuery } from "./_generated/server";

/**
* Transcription job status tracking.
Expand All @@ -20,7 +20,7 @@ export const JOB_STATUS = {
/**
* Create a new transcription job.
*/
export const createJob = mutation({
export const createJob = internalMutation({
args: {
youtubeUrl: v.string(),
language: v.string(),
Expand Down Expand Up @@ -62,9 +62,9 @@ export const createJob = mutation({
});

/**
* Update job status (called by Python pipeline via HTTP action).
* Internal pipeline maintenance; no public HTTP update route is registered.
*/
export const updateJobStatus = mutation({
export const updateJobStatus = internalMutation({
args: {
jobId: v.id("transcriptionJobs"),
status: v.string(),
Expand Down Expand Up @@ -97,7 +97,7 @@ export const updateJobStatus = mutation({
/**
* Get job status for real-time updates.
*/
export const getJobStatus = query({
export const getJobStatus = internalQuery({
args: {
jobId: v.id("transcriptionJobs"),
},
Expand All @@ -109,7 +109,7 @@ export const getJobStatus = query({
/**
* Get all jobs for a visitor.
*/
export const getJobsByVisitor = query({
export const getJobsByVisitor = internalQuery({
args: {
visitorId: v.string(),
},
Expand All @@ -126,7 +126,7 @@ export const getJobsByVisitor = query({
* Import transcription result as a new song.
* Called after pipeline completes successfully.
*/
export const importTranscription = mutation({
export const importTranscription = internalMutation({
args: {
jobId: v.id("transcriptionJobs"),
},
Expand Down Expand Up @@ -205,10 +205,3 @@ export const importTranscription = mutation({
return songId;
},
});

/**
* HTTP endpoint for Python pipeline to update job status.
* Used when running pipeline externally (RunPod, local, etc.)
*/
// Note: HTTP endpoints are defined in http.ts
// This would be called via: POST /api/transcription/update
Loading
Loading