1+ import type { EphemeralConfig } from "@ai-di/graph-workflow" ;
12import { getErrorMessage } from "@ai-di/shared-logging" ;
23import {
34 DocumentStatus ,
@@ -16,6 +17,43 @@ import {
1617import { PrismaService } from "../database/prisma.service" ;
1718import type { DocumentData } from "./document-db.types" ;
1819
20+ /** A terminal document to purge, with its workflow's ephemeral policy. */
21+ export interface PurgeableEphemeralDocument {
22+ id : string ;
23+ group_id : string ;
24+ workflow_execution_id : string | null ;
25+ ephemeral : EphemeralConfig ;
26+ }
27+
28+ /**
29+ * Extracts `metadata.ephemeral` from a workflow version config (stored as JSON),
30+ * normalizing the object form to explicit booleans. Returns `false` when absent
31+ * or malformed.
32+ */
33+ function extractEphemeralConfig (
34+ config : Prisma . JsonValue | null | undefined ,
35+ ) : EphemeralConfig {
36+ if ( ! config || typeof config !== "object" || Array . isArray ( config ) ) {
37+ return false ;
38+ }
39+ const metadata = ( config as Record < string , unknown > ) . metadata ;
40+ if ( ! metadata || typeof metadata !== "object" || Array . isArray ( metadata ) ) {
41+ return false ;
42+ }
43+ const ephemeral = ( metadata as Record < string , unknown > ) . ephemeral ;
44+ if ( ephemeral === true ) {
45+ return true ;
46+ }
47+ if ( ephemeral && typeof ephemeral === "object" && ! Array . isArray ( ephemeral ) ) {
48+ const obj = ephemeral as Record < string , unknown > ;
49+ return {
50+ files : obj . files === true ,
51+ temporalRecord : obj . temporalRecord === true ,
52+ } ;
53+ }
54+ return false ;
55+ }
56+
1957@Injectable ( )
2058export class DocumentDbService {
2159 constructor (
@@ -34,7 +72,7 @@ export class DocumentDbService {
3472 * @returns The created document record.
3573 */
3674 async createDocument (
37- data : Omit < DocumentData , "created_at" | "updated_at" > ,
75+ data : Omit < DocumentData , "created_at" | "updated_at" | "purged_at" > ,
3876 tx ?: Prisma . TransactionClient ,
3977 ) : Promise < DocumentData > {
4078 const client = tx ?? this . prisma ;
@@ -361,6 +399,76 @@ export class DocumentDbService {
361399 }
362400 }
363401
402+ /**
403+ * Finds documents eligible for ephemeral cleanup: those processed by a
404+ * workflow whose config declares an ephemeral policy that deletes at least
405+ * one target (`metadata.ephemeral` is `true`, or an object with `files` or
406+ * `temporalRecord` set to `true`), in a terminal status, and not yet purged.
407+ * Ephemerality is configured on the workflow — there is no global or
408+ * per-group setting. Each result carries its workflow's ephemeral policy so
409+ * the janitor knows which targets to delete.
410+ *
411+ * @param statuses - Terminal statuses considered safe to purge.
412+ * @param limit - Maximum number of documents to return in one batch.
413+ * @returns Records (id, group, Temporal execution id, ephemeral policy).
414+ */
415+ async findPurgeableEphemeralDocuments (
416+ statuses : DocumentStatus [ ] ,
417+ limit : number ,
418+ ) : Promise < PurgeableEphemeralDocument [ ] > {
419+ const rows = await this . prisma . document . findMany ( {
420+ where : {
421+ status : { in : statuses } ,
422+ purged_at : null ,
423+ workflowVersion : {
424+ is : {
425+ OR : [
426+ { config : { path : [ "metadata" , "ephemeral" ] , equals : true } } ,
427+ {
428+ config : {
429+ path : [ "metadata" , "ephemeral" , "files" ] ,
430+ equals : true ,
431+ } ,
432+ } ,
433+ {
434+ config : {
435+ path : [ "metadata" , "ephemeral" , "temporalRecord" ] ,
436+ equals : true ,
437+ } ,
438+ } ,
439+ ] ,
440+ } ,
441+ } ,
442+ } ,
443+ select : {
444+ id : true ,
445+ group_id : true ,
446+ workflow_execution_id : true ,
447+ workflowVersion : { select : { config : true } } ,
448+ } ,
449+ orderBy : { updated_at : "asc" } ,
450+ take : limit ,
451+ } ) ;
452+ return rows . map ( ( row ) => ( {
453+ id : row . id ,
454+ group_id : row . group_id ,
455+ workflow_execution_id : row . workflow_execution_id ,
456+ ephemeral : extractEphemeralConfig ( row . workflowVersion ?. config ) ,
457+ } ) ) ;
458+ }
459+
460+ /**
461+ * Stamps a document as purged so the cleanup janitor will not reprocess it.
462+ *
463+ * @param id - The document ID to mark purged.
464+ */
465+ async markDocumentPurged ( id : string ) : Promise < void > {
466+ await this . prisma . document . update ( {
467+ where : { id } ,
468+ data : { purged_at : new Date ( ) } ,
469+ } ) ;
470+ }
471+
364472 /**
365473 * Retrieves the most recent OCR result for a document.
366474 *
0 commit comments