@@ -3,11 +3,12 @@ import { PluginOptions } from './types.js';
33import { AdminForthPlugin , AdminForthResourceColumn , AdminForthResource , Filters , IAdminForth , IHttpServer , suggestIfTypo } from "adminforth" ;
44import { Readable } from "stream" ;
55import { RateLimiter } from "adminforth" ;
6+ import { randomUUID } from "crypto" ;
67import { interpretResource } from 'adminforth' ;
78import { ActionCheckSource } from 'adminforth' ;
89
910const ADMINFORTH_NOT_YET_USED_TAG = 'adminforth-candidate-for-cleanup' ;
10-
11+ const jobs = new Map ( ) ;
1112export default class UploadPlugin extends AdminForthPlugin {
1213 options : PluginOptions ;
1314
@@ -28,10 +29,87 @@ export default class UploadPlugin extends AdminForthPlugin {
2829 this . totalCalls = 0 ;
2930 this . totalDuration = 0 ;
3031 if ( this . options . generation ?. rateLimit ?. limit ) {
31- this . rateLimiter = new RateLimiter ( this . options . generation . rateLimit ?. limit )
32+ this . rateLimiter = new RateLimiter ( this . options . generation . rateLimit ?. limit )
3233 }
3334 }
3435
36+ private async generateImages ( jobId : string , prompt : string , recordId : any , adminUser : any , headers : any ) {
37+ if ( this . options . generation . rateLimit ?. limit ) {
38+ // rate limit
39+ // const { error } = RateLimiter.checkRateLimit(
40+ // this.pluginInstanceId,
41+ // this.options.generation.rateLimit?.limit,
42+ // this.adminforth.auth.getClientIp(headers),
43+ // );
44+ if ( ! await this . rateLimiter . consume ( `${ this . pluginInstanceId } -${ this . adminforth . auth . getClientIp ( headers ) } ` ) ) {
45+ jobs . set ( jobId , { status : "failed" , error : this . options . generation . rateLimit . errorMessage } ) ;
46+ return { error : this . options . generation . rateLimit . errorMessage } ;
47+ }
48+ }
49+ let attachmentFiles = [ ] ;
50+ if ( this . options . generation . attachFiles ) {
51+ // TODO - does it require additional allowed action to check this record id has access to get the image?
52+ // or should we mention in docs that user should do validation in method itself
53+ const record = await this . adminforth . resource ( this . resourceConfig . resourceId ) . get (
54+ [ Filters . EQ ( this . resourceConfig . columns . find ( c => c . primaryKey ) ?. name , recordId ) ]
55+ ) ;
56+
57+
58+ if ( ! record ) {
59+ return { error : `Record with id ${ recordId } not found` } ;
60+ }
61+
62+ attachmentFiles = await this . options . generation . attachFiles ( { record, adminUser } ) ;
63+ // if files is not array, make it array
64+ if ( ! Array . isArray ( attachmentFiles ) ) {
65+ attachmentFiles = [ attachmentFiles ] ;
66+ }
67+
68+ }
69+
70+ let error : string | undefined = undefined ;
71+
72+ const STUB_MODE = false ;
73+
74+ const images = await Promise . all (
75+ ( new Array ( this . options . generation . countToGenerate ) ) . fill ( 0 ) . map ( async ( ) => {
76+ if ( STUB_MODE ) {
77+ await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
78+ return `https://picsum.photos/200/300?random=${ Math . floor ( Math . random ( ) * 1000 ) } ` ;
79+ }
80+ const start = + new Date ( ) ;
81+ let resp ;
82+ try {
83+ resp = await this . options . generation . adapter . generate (
84+ {
85+ prompt,
86+ inputFiles : attachmentFiles ,
87+ n : 1 ,
88+ size : this . options . generation . outputSize ,
89+ }
90+ )
91+ } catch ( e : any ) {
92+ error = `No response from image generation provider: ${ e . message } . Please check your prompt or try again later.` ;
93+ return ;
94+ }
95+
96+ if ( resp . error ) {
97+ console . error ( 'Error generating image' , resp . error ) ;
98+ error = resp . error ;
99+ return ;
100+ }
101+
102+ this . totalCalls ++ ;
103+ this . totalDuration += ( + new Date ( ) - start ) / 1000 ;
104+
105+ return resp . imageURLs [ 0 ]
106+
107+ } )
108+ ) ;
109+ jobs . set ( jobId , { status : "completed" , images, error } ) ;
110+ return { ok : true } ;
111+ } ;
112+
35113 instanceUniqueRepresentation ( pluginOptions : any ) : string {
36114 return `${ pluginOptions . pathColumnName } ` ;
37115 }
@@ -315,81 +393,34 @@ export default class UploadPlugin extends AdminForthPlugin {
315393
316394 server . endpoint ( {
317395 method : 'POST' ,
318- path : `/plugin/${ this . pluginInstanceId } /generate_images ` ,
396+ path : `/plugin/${ this . pluginInstanceId } /create-image-generation-job ` ,
319397 handler : async ( { body, adminUser, headers } ) => {
320398 const { prompt, recordId } = body ;
321- if ( this . rateLimiter ) {
322- // rate limit
323- // const { error } = RateLimiter.checkRateLimit(
324- // this.pluginInstanceId,
325- // this.options.generation.rateLimit?.limit,
326- // this.adminforth.auth.getClientIp(headers),
327- // );
328- if ( ! await this . rateLimiter . consume ( `${ this . pluginInstanceId } -${ this . adminforth . auth . getClientIp ( headers ) } ` ) ) {
329- return { error : this . options . generation . rateLimit . errorMessage } ;
330- }
331- }
332- let attachmentFiles = [ ] ;
333- if ( this . options . generation . attachFiles ) {
334- // TODO - does it require additional allowed action to check this record id has access to get the image?
335- // or should we mention in docs that user should do validation in method itself
336- const record = await this . adminforth . resource ( this . resourceConfig . resourceId ) . get (
337- [ Filters . EQ ( this . resourceConfig . columns . find ( ( column : any ) => column . primaryKey ) ?. name , recordId ) ]
338- ) ;
339-
340- if ( ! record ) {
341- return { error : `Record with id ${ recordId } not found` } ;
342- }
343-
344- attachmentFiles = await this . options . generation . attachFiles ( { record, adminUser } ) ;
345- // if files is not array, make it array
346- if ( ! Array . isArray ( attachmentFiles ) ) {
347- attachmentFiles = [ attachmentFiles ] ;
348- }
349-
350- }
351-
352- let error : string | undefined = undefined ;
353-
354- const STUB_MODE = false ;
355-
356- const images = await Promise . all (
357- ( new Array ( this . options . generation . countToGenerate ) ) . fill ( 0 ) . map ( async ( ) => {
358- if ( STUB_MODE ) {
359- await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
360- return `https://picsum.photos/200/300?random=${ Math . floor ( Math . random ( ) * 1000 ) } ` ;
361- }
362- const start = + new Date ( ) ;
363- let resp ;
364- try {
365- resp = await this . options . generation . adapter . generate (
366- {
367- prompt,
368- inputFiles : attachmentFiles ,
369- n : 1 ,
370- size : this . options . generation . outputSize ,
371- }
372- )
373- } catch ( e : any ) {
374- error = `No response from image generation provider: ${ e . message } . Please check your prompt or try again later.` ;
375- return ;
376- }
377399
378- if ( resp . error ) {
379- console . error ( 'Error generating image' , resp . error ) ;
380- error = resp . error ;
381- return ;
382- }
400+ const jobId = randomUUID ( ) ;
401+ jobs . set ( jobId , { status : "in_progress" } ) ;
383402
384- this . totalCalls ++ ;
385- this . totalDuration += ( + new Date ( ) - start ) / 1000 ;
386-
387- return resp . imageURLs [ 0 ]
403+ this . generateImages ( jobId , prompt , recordId , adminUser , headers ) ;
404+ setTimeout ( ( ) => jobs . delete ( jobId ) , 1_800_000 ) ;
405+ setTimeout ( ( ) => { jobs . set ( jobId , { status : "timeout" } ) ; } , 300_000 ) ;
388406
389- } )
390- ) ;
407+ return { ok : true , jobId } ;
408+ }
409+ } ) ;
391410
392- return { error, images } ;
411+ server . endpoint ( {
412+ method : 'POST' ,
413+ path : `/plugin/${ this . pluginInstanceId } /get-image-generation-job-status` ,
414+ handler : async ( { body, adminUser, headers } ) => {
415+ const jobId = body . jobId ;
416+ if ( ! jobId ) {
417+ return { error : "Can't find job id" } ;
418+ }
419+ const job = jobs . get ( jobId ) ;
420+ if ( ! job ) {
421+ return { error : "Job not found" } ;
422+ }
423+ return { ok : true , job } ;
393424 }
394425 } ) ;
395426
@@ -451,5 +482,6 @@ export default class UploadPlugin extends AdminForthPlugin {
451482 } ) ;
452483
453484 }
485+
454486
455487}
0 commit comments