1- import { AdminForthPlugin , AdminForthResource , IAdminForth } from "adminforth" ;
1+ import { AdminForthPlugin , AdminForthResource , IAdminForth , Filters , AdminUser } from "adminforth" ;
22import { PluginOptions } from "./types.js" ;
33
44export default class MarkdownPlugin extends AdminForthPlugin {
55 options : PluginOptions ;
66 resourceConfig ! : AdminForthResource ;
77 adminforth ! : IAdminForth ;
8+ uploadPlugin : AdminForthPlugin ;
9+ attachmentResource : AdminForthResource = undefined ;
810
911 constructor ( options : PluginOptions ) {
1012 super ( options , import . meta. url ) ;
@@ -41,6 +43,31 @@ export default class MarkdownPlugin extends AdminForthPlugin {
4143 if ( ! column . components ) {
4244 column . components = { } ;
4345 }
46+ if ( this . options . attachments ) {
47+ const resource = await adminforth . config . resources . find ( r => r . resourceId === this . options . attachments ! . attachmentResource ) ;
48+ if ( ! resource ) {
49+ throw new Error ( `Resource '${ this . options . attachments ! . attachmentResource } ' not found` ) ;
50+ }
51+ this . attachmentResource = resource ;
52+ const field = await resource . columns . find ( c => c . name === this . options . attachments ! . attachmentFieldName ) ;
53+ if ( ! field ) {
54+ throw new Error ( `Field '${ this . options . attachments ! . attachmentFieldName } ' not found in resource '${ this . options . attachments ! . attachmentResource } '` ) ;
55+ }
56+
57+ const plugin = await adminforth . activatedPlugins . find ( p =>
58+ p . resourceConfig ! . resourceId === this . options . attachments ! . attachmentResource &&
59+ p . pluginOptions . pathColumnName === this . options . attachments ! . attachmentFieldName
60+ ) ;
61+ if ( ! plugin ) {
62+ throw new Error ( `${ plugin } Plugin for attachment field '${ this . options . attachments ! . attachmentFieldName } ' not found in resource '${ this . options . attachments ! . attachmentResource } ', please check if Upload Plugin is installed on the field ${ this . options . attachments ! . attachmentFieldName } ` ) ;
63+ }
64+
65+ if ( plugin . pluginOptions . s3ACL !== 'public-read' ) {
66+ throw new Error ( `Upload Plugin for attachment field '${ this . options . attachments ! . attachmentFieldName } ' in resource '${ this . options . attachments ! . attachmentResource } '
67+ should have s3ACL set to 'public-read' (in vast majority of cases signed urls inside of HTML text is not desired behavior, so we did not implement it)` ) ;
68+ }
69+ this . uploadPlugin = plugin ;
70+ }
4471
4572 column . components . show = {
4673 file : this . componentPath ( "MarkdownRenderer.vue" ) ,
@@ -64,6 +91,7 @@ export default class MarkdownPlugin extends AdminForthPlugin {
6491 pluginInstanceId : this . pluginInstanceId ,
6592 columnName : fieldName ,
6693 pluginType : 'crepe' ,
94+ uploadPluginInstanceId : this . uploadPlugin ?. pluginInstanceId ,
6795 } ,
6896 } ;
6997
@@ -73,7 +101,138 @@ export default class MarkdownPlugin extends AdminForthPlugin {
73101 pluginInstanceId : this . pluginInstanceId ,
74102 columnName : fieldName ,
75103 pluginType : 'crepe' ,
104+ uploadPluginInstanceId : this . uploadPlugin ?. pluginInstanceId ,
76105 } ,
77106 } ;
107+ const editorRecordPkField = resourceConfig . columns . find ( c => c . primaryKey ) ;
108+ if ( this . options . attachments ) {
109+
110+ function getAttachmentPathes ( markdown : string ) : string [ ] {
111+ if ( ! markdown ) {
112+ return [ ] ;
113+ }
114+
115+ const s3PathRegex = / ! \[ .* ?\] \( ( h t t p s : \/ \/ .* ?\/ .* ?) ( \? .* ) ? \) / g;
116+
117+ const matches = [ ...markdown . matchAll ( s3PathRegex ) ] ;
118+
119+ return matches
120+ . map ( match => match [ 1 ] )
121+ . filter ( src => src . includes ( "s3" ) || src . includes ( "amazonaws" ) ) ;
122+ }
123+
124+ const createAttachmentRecords = async (
125+ adminforth : IAdminForth , options : PluginOptions , recordId : any , s3Paths : string [ ] , adminUser : AdminUser
126+ ) => {
127+ const extractKey = ( s3Paths : string ) => s3Paths . replace ( / ^ h t t p s : \/ \/ [ ^ \/ ] + \/ + / , '' ) ;
128+ process . env . HEAVY_DEBUG && console . log ( '📸 Creating attachment records' , JSON . stringify ( recordId ) )
129+ try {
130+ await Promise . all ( s3Paths . map ( async ( s3Path ) => {
131+ console . log ( 'Processing path:' , s3Path ) ;
132+ try {
133+ await adminforth . createResourceRecord (
134+ {
135+ resource : this . attachmentResource ,
136+ record : {
137+ [ options . attachments . attachmentFieldName ] : extractKey ( s3Path ) ,
138+ [ options . attachments . attachmentRecordIdFieldName ] : recordId ,
139+ [ options . attachments . attachmentResourceIdFieldName ] : resourceConfig . resourceId ,
140+ } ,
141+ adminUser
142+ }
143+ ) ;
144+ console . log ( 'Successfully created record for:' , s3Path ) ;
145+ } catch ( err ) {
146+ console . error ( 'Error creating record for' , s3Path , err ) ;
147+ }
148+ } ) ) ;
149+ } catch ( err ) {
150+ console . error ( 'Error in Promise.all' , err ) ;
151+ }
152+ }
153+
154+ const deleteAttachmentRecords = async (
155+ adminforth : IAdminForth , options : PluginOptions , s3Paths : string [ ] , adminUser : AdminUser
156+ ) => {
157+ if ( ! s3Paths . length ) {
158+ return ;
159+ }
160+ const attachmentPrimaryKeyField = this . attachmentResource . columns . find ( c => c . primaryKey ) ;
161+ const attachments = await adminforth . resource ( options . attachments . attachmentResource ) . list (
162+ Filters . IN ( options . attachments . attachmentFieldName , s3Paths )
163+ ) ;
164+ await Promise . all ( attachments . map ( async ( a : any ) => {
165+ await adminforth . deleteResourceRecord (
166+ {
167+ resource : this . attachmentResource ,
168+ recordId : a [ attachmentPrimaryKeyField . name ] ,
169+ adminUser,
170+ record : a ,
171+ }
172+ )
173+ } ) )
174+ }
175+
176+ ( resourceConfig . hooks . create . afterSave ) . push ( async ( { record, adminUser } : { record : any , adminUser : AdminUser } ) => {
177+ // find all s3Paths in the html
178+ const s3Paths = getAttachmentPathes ( record [ this . options . fieldName ] )
179+
180+ process . env . HEAVY_DEBUG && console . log ( '📸 Found s3Paths' , s3Paths ) ;
181+ // create attachment records
182+ await createAttachmentRecords (
183+ adminforth , this . options , record [ editorRecordPkField . name ] , s3Paths , adminUser ) ;
184+
185+ return { ok : true } ;
186+ } ) ;
187+
188+ // after edit we need to delete attachments that are not in the html anymore
189+ // and add new ones
190+ ( resourceConfig . hooks . edit . afterSave ) . push (
191+ async ( { recordId, record, adminUser } : { recordId : any , record : any , adminUser : AdminUser } ) => {
192+ process . env . HEAVY_DEBUG && console . log ( '⚓ Cought hook' , recordId , 'rec' , record ) ;
193+ if ( record [ this . options . fieldName ] === undefined ) {
194+ console . log ( '⚓ Cought hook' , recordId , 'rec' , record ) ;
195+ // field was not changed, do nothing
196+ return { ok : true } ;
197+ }
198+ const existingAparts = await adminforth . resource ( this . options . attachments . attachmentResource ) . list ( [
199+ Filters . EQ ( this . options . attachments . attachmentRecordIdFieldName , recordId ) ,
200+ Filters . EQ ( this . options . attachments . attachmentResourceIdFieldName , resourceConfig . resourceId )
201+ ] ) ;
202+ const existingS3Paths = existingAparts . map ( ( a : any ) => a [ this . options . attachments . attachmentFieldName ] ) ;
203+ const newS3Paths = getAttachmentPathes ( record [ this . options . fieldName ] ) ;
204+ process . env . HEAVY_DEBUG && console . log ( '📸 Existing s3Paths (from db)' , existingS3Paths )
205+ process . env . HEAVY_DEBUG && console . log ( '📸 Found new s3Paths (from text)' , newS3Paths ) ;
206+ const toDelete = existingS3Paths . filter ( s3Path => ! newS3Paths . includes ( s3Path ) ) ;
207+ const toAdd = newS3Paths . filter ( s3Path => ! existingS3Paths . includes ( s3Path ) ) ;
208+ process . env . HEAVY_DEBUG && console . log ( '📸 Found s3Paths to delete' , toDelete )
209+ process . env . HEAVY_DEBUG && console . log ( '📸 Found s3Paths to add' , toAdd ) ;
210+ await Promise . all ( [
211+ deleteAttachmentRecords ( adminforth , this . options , toDelete , adminUser ) ,
212+ createAttachmentRecords ( adminforth , this . options , recordId , toAdd , adminUser )
213+ ] ) ;
214+
215+ return { ok : true } ;
216+
217+ }
218+ ) ;
219+
220+ // after delete we need to delete all attachments
221+ ( resourceConfig . hooks . delete . afterSave ) . push (
222+ async ( { record, adminUser } : { record : any , adminUser : AdminUser } ) => {
223+ const existingAparts = await adminforth . resource ( this . options . attachments . attachmentResource ) . list (
224+ [
225+ Filters . EQ ( this . options . attachments . attachmentRecordIdFieldName , record [ editorRecordPkField . name ] ) ,
226+ Filters . EQ ( this . options . attachments . attachmentResourceIdFieldName , resourceConfig . resourceId )
227+ ]
228+ ) ;
229+ const existingS3Paths = existingAparts . map ( ( a : any ) => a [ this . options . attachments . attachmentFieldName ] ) ;
230+ process . env . HEAVY_DEBUG && console . log ( '📸 Found s3Paths to delete' , existingS3Paths ) ;
231+ await deleteAttachmentRecords ( adminforth , this . options , existingS3Paths , adminUser ) ;
232+
233+ return { ok : true } ;
234+ }
235+ ) ;
236+ }
78237 }
79238}
0 commit comments