1
- import { AdminForthPlugin , AdminForthResource , IAdminForth } from "adminforth" ;
1
+ import { AdminForthPlugin , AdminForthResource , IAdminForth , Filters , AdminUser } from "adminforth" ;
2
2
import { PluginOptions } from "./types.js" ;
3
3
4
4
export default class MarkdownPlugin extends AdminForthPlugin {
5
5
options : PluginOptions ;
6
6
resourceConfig ! : AdminForthResource ;
7
7
adminforth ! : IAdminForth ;
8
+ uploadPlugin : AdminForthPlugin ;
9
+ attachmentResource : AdminForthResource = undefined ;
8
10
9
11
constructor ( options : PluginOptions ) {
10
12
super ( options , import . meta. url ) ;
@@ -41,6 +43,31 @@ export default class MarkdownPlugin extends AdminForthPlugin {
41
43
if ( ! column . components ) {
42
44
column . components = { } ;
43
45
}
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
+ }
44
71
45
72
column . components . show = {
46
73
file : this . componentPath ( "MarkdownRenderer.vue" ) ,
@@ -64,6 +91,7 @@ export default class MarkdownPlugin extends AdminForthPlugin {
64
91
pluginInstanceId : this . pluginInstanceId ,
65
92
columnName : fieldName ,
66
93
pluginType : 'crepe' ,
94
+ uploadPluginInstanceId : this . uploadPlugin ?. pluginInstanceId ,
67
95
} ,
68
96
} ;
69
97
@@ -73,7 +101,138 @@ export default class MarkdownPlugin extends AdminForthPlugin {
73
101
pluginInstanceId : this . pluginInstanceId ,
74
102
columnName : fieldName ,
75
103
pluginType : 'crepe' ,
104
+ uploadPluginInstanceId : this . uploadPlugin ?. pluginInstanceId ,
76
105
} ,
77
106
} ;
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
+ }
78
237
}
79
238
}
0 commit comments