@@ -48,7 +48,7 @@ export const getBySlug = query({
4848 . query ( 'skills' )
4949 . withIndex ( 'by_slug' , ( q ) => q . eq ( 'slug' , args . slug ) )
5050 . unique ( )
51- if ( ! skill ) return null
51+ if ( ! skill || skill . softDeletedAt ) return null
5252 const latestVersion = skill . latestVersionId ? await ctx . db . get ( skill . latestVersionId ) : null
5353 const owner = await ctx . db . get ( skill . ownerUserId )
5454 return { skill, latestVersion, owner }
@@ -74,21 +74,27 @@ export const list = query({
7474 handler : async ( ctx , args ) => {
7575 const limit = args . limit ?? 24
7676 if ( args . batch ) {
77- return ctx . db
77+ const entries = await ctx . db
7878 . query ( 'skills' )
7979 . withIndex ( 'by_batch' , ( q ) => q . eq ( 'batch' , args . batch ) )
8080 . order ( 'desc' )
81- . take ( limit )
81+ . take ( limit * 5 )
82+ return entries . filter ( ( skill ) => ! skill . softDeletedAt ) . slice ( 0 , limit )
8283 }
8384 const ownerUserId = args . ownerUserId
8485 if ( ownerUserId ) {
85- return ctx . db
86+ const entries = await ctx . db
8687 . query ( 'skills' )
8788 . withIndex ( 'by_owner' , ( q ) => q . eq ( 'ownerUserId' , ownerUserId ) )
8889 . order ( 'desc' )
89- . take ( limit )
90+ . take ( limit * 5 )
91+ return entries . filter ( ( skill ) => ! skill . softDeletedAt ) . slice ( 0 , limit )
9092 }
91- return ctx . db . query ( 'skills' ) . order ( 'desc' ) . take ( limit )
93+ const entries = await ctx . db
94+ . query ( 'skills' )
95+ . order ( 'desc' )
96+ . take ( limit * 5 )
97+ return entries . filter ( ( skill ) => ! skill . softDeletedAt ) . slice ( 0 , limit )
9298 } ,
9399} )
94100
@@ -164,11 +170,7 @@ export async function publishVersionForUser(
164170 if ( ! semver . valid ( version ) ) {
165171 throw new ConvexError ( 'Version must be valid semver' )
166172 }
167- const existingSkill = await ctx . runQuery ( internal . skills . getSkillBySlugInternal , { slug } )
168173 const changelogText = args . changelog . trim ( )
169- if ( existingSkill && ! changelogText ) {
170- throw new ConvexError ( 'Changelog is required for updates' )
171- }
172174
173175 const sanitizedFiles = args . files . map ( ( file ) => ( {
174176 ...file ,
@@ -418,6 +420,7 @@ export const insertVersion = internalMutation({
418420 ownerUserId : userId ,
419421 latestVersionId : undefined ,
420422 tags : { } ,
423+ softDeletedAt : undefined ,
421424 badges : { redactionApproved : undefined } ,
422425 stats : { downloads : 0 , stars : 0 , versions : 0 , comments : 0 } ,
423426 createdAt : now ,
@@ -461,6 +464,7 @@ export const insertVersion = internalMutation({
461464 latestVersionId : versionId ,
462465 tags : nextTags ,
463466 stats : { ...skill . stats , versions : skill . stats . versions + 1 } ,
467+ softDeletedAt : undefined ,
464468 updatedAt : now ,
465469 } )
466470
@@ -493,6 +497,61 @@ export const insertVersion = internalMutation({
493497 } ,
494498} )
495499
500+ export const setSkillSoftDeletedInternal = internalMutation ( {
501+ args : {
502+ userId : v . id ( 'users' ) ,
503+ slug : v . string ( ) ,
504+ deleted : v . boolean ( ) ,
505+ } ,
506+ handler : async ( ctx , args ) => {
507+ const user = await ctx . db . get ( args . userId )
508+ if ( ! user || user . deletedAt ) throw new Error ( 'User not found' )
509+
510+ const slug = args . slug . trim ( ) . toLowerCase ( )
511+ if ( ! slug ) throw new Error ( 'Slug required' )
512+
513+ const skill = await ctx . db
514+ . query ( 'skills' )
515+ . withIndex ( 'by_slug' , ( q ) => q . eq ( 'slug' , slug ) )
516+ . unique ( )
517+ if ( ! skill ) throw new Error ( 'Skill not found' )
518+
519+ if ( skill . ownerUserId !== args . userId ) {
520+ assertRole ( user , [ 'admin' , 'moderator' ] )
521+ }
522+
523+ const now = Date . now ( )
524+ await ctx . db . patch ( skill . _id , {
525+ softDeletedAt : args . deleted ? now : undefined ,
526+ updatedAt : now ,
527+ } )
528+
529+ const embeddings = await ctx . db
530+ . query ( 'skillEmbeddings' )
531+ . withIndex ( 'by_skill' , ( q ) => q . eq ( 'skillId' , skill . _id ) )
532+ . collect ( )
533+ for ( const embedding of embeddings ) {
534+ await ctx . db . patch ( embedding . _id , {
535+ visibility : args . deleted
536+ ? 'deleted'
537+ : visibilityFor ( embedding . isLatest , embedding . isApproved ) ,
538+ updatedAt : now ,
539+ } )
540+ }
541+
542+ await ctx . db . insert ( 'auditLogs' , {
543+ actorUserId : args . userId ,
544+ action : args . deleted ? 'skill.delete' : 'skill.undelete' ,
545+ targetType : 'skill' ,
546+ targetId : skill . _id ,
547+ metadata : { slug, softDeletedAt : args . deleted ? now : null } ,
548+ createdAt : now ,
549+ } )
550+
551+ return { ok : true as const }
552+ } ,
553+ } )
554+
496555async function fetchText (
497556 ctx : { storage : { get : ( id : Id < '_storage' > ) => Promise < Blob | null > } } ,
498557 storageId : Id < '_storage' > ,
0 commit comments