1+ import { AdminForthPlugin , suggestIfTypo , AdminForthFilterOperators } from "adminforth" ;
2+ import type { IAdminForth , IHttpServer , AdminForthResourceColumn , AdminForthComponentDeclaration , AdminForthResource } from "adminforth" ;
3+ import type { PluginOptions } from './types.js' ;
4+
5+ export default class ImportExport extends AdminForthPlugin {
6+ options : PluginOptions ;
7+ emailField : AdminForthResourceColumn ;
8+ authResourceId : string ;
9+ adminforth : IAdminForth ;
10+
11+ constructor ( options : PluginOptions ) {
12+ super ( options , import . meta. url ) ;
13+ this . options = options ;
14+ }
15+
16+ async modifyResourceConfig ( adminforth : IAdminForth , resourceConfig : AdminForthResource ) {
17+ super . modifyResourceConfig ( adminforth , resourceConfig ) ;
18+ if ( ! resourceConfig . options . pageInjections ) {
19+ resourceConfig . options . pageInjections = { } ;
20+ }
21+ if ( ! resourceConfig . options . pageInjections . list ) {
22+ resourceConfig . options . pageInjections . list = { } ;
23+ }
24+ if ( ! resourceConfig . options . pageInjections . list . threeDotsDropdownItems ) {
25+ resourceConfig . options . pageInjections . list . threeDotsDropdownItems = [ ] ;
26+ }
27+ ( resourceConfig . options . pageInjections . list . threeDotsDropdownItems as AdminForthComponentDeclaration [ ] ) . push ( {
28+ file : this . componentPath ( 'ExportCsv.vue' ) ,
29+ meta : { pluginInstanceId : this . pluginInstanceId , select : 'all' }
30+ } , {
31+ file : this . componentPath ( 'ExportCsv.vue' ) ,
32+ meta : { pluginInstanceId : this . pluginInstanceId , select : 'filtered' }
33+ } , {
34+ file : this . componentPath ( 'ImportCsv.vue' ) ,
35+ meta : { pluginInstanceId : this . pluginInstanceId }
36+ } ) ;
37+
38+
39+ // simply modify resourceConfig or adminforth.config. You can get access to plugin options via this.options;
40+ }
41+
42+ validateConfigAfterDiscover ( adminforth : IAdminForth , resourceConfig : AdminForthResource ) {
43+ // optional method where you can safely check field types after database discovery was performed
44+ }
45+
46+ instanceUniqueRepresentation ( pluginOptions : any ) : string {
47+ // optional method to return unique string representation of plugin instance.
48+ // Needed if plugin can have multiple instances on one resource
49+ return `${ this . pluginInstanceId } ` ;
50+ }
51+
52+ setupEndpoints ( server : IHttpServer ) {
53+ server . endpoint ( {
54+ method : 'POST' ,
55+ path : `/plugin/${ this . pluginInstanceId } /export-csv` ,
56+ noAuth : true ,
57+ handler : async ( { body } ) => {
58+ const { filters, sort } = body ;
59+
60+ for ( const filter of ( filters || [ ] ) ) {
61+ if ( ! Object . values ( AdminForthFilterOperators ) . includes ( filter . operator ) ) {
62+ throw new Error ( `Operator '${ filter . operator } ' is not allowed` ) ;
63+ }
64+ if ( ! this . resourceConfig . columns . some ( ( col ) => col . name === filter . field ) ) {
65+ throw new Error ( `Field '${ filter . field } ' is not in resource '${ this . resourceConfig . resourceId } '. Available fields: ${ this . resourceConfig . columns . map ( ( col ) => col . name ) . join ( ', ' ) } ` ) ;
66+ }
67+ if ( filter . operator === AdminForthFilterOperators . IN || filter . operator === AdminForthFilterOperators . NIN ) {
68+ if ( ! Array . isArray ( filter . value ) ) {
69+ throw new Error ( `Value for operator '${ filter . operator } ' should be an array` ) ;
70+ }
71+ }
72+ if ( filter . operator === AdminForthFilterOperators . IN && filter . value . length === 0 ) {
73+ // nonsense
74+ return { data : [ ] , total : 0 } ;
75+ }
76+ }
77+
78+ const data = await this . adminforth . connectors [ this . resourceConfig . dataSource ] . getData ( {
79+ resource : this . resourceConfig ,
80+ limit : 1e6 ,
81+ offset : 0 ,
82+ filters,
83+ sort,
84+ getTotals : true ,
85+ } ) ;
86+
87+ // csv export
88+
89+ const columns = this . resourceConfig . columns . filter ( ( col ) => ! col . virtual ) ;
90+ let csv = data . data . map ( ( row ) => {
91+ return columns . map ( ( col ) => {
92+ return row [ col . name ] ;
93+ } ) . join ( ',' ) ;
94+ } ) . join ( '\n' ) ;
95+
96+ // add headers
97+ const headers = columns . map ( ( col ) => col . name ) . join ( ',' ) ;
98+ csv = `${ headers } \n${ csv } ` ;
99+
100+ return { data : csv , exportedCount : data . total , ok : true } ;
101+
102+ }
103+ } ) ;
104+
105+ server . endpoint ( {
106+ method : 'POST' ,
107+ path : `/plugin/${ this . pluginInstanceId } /import-csv` ,
108+ noAuth : true ,
109+ handler : async ( { body } ) => {
110+ const { data } = body ;
111+ // data is in format {[columnName]: [value1, value2, value3...], [columnName2]: [value1, value2, value3...]}
112+ // we need to convert it to [{columnName: value1, columnName2: value1}, {columnName: value2, columnName2: value2}...]
113+ const rows = [ ] ;
114+ const columns = Object . keys ( data ) ;
115+
116+ // check column names are valid
117+ const errors : string [ ] = [ ] ;
118+ columns . forEach ( ( col ) => {
119+ if ( ! this . resourceConfig . columns . some ( ( c ) => c . name === col ) ) {
120+ const similar = suggestIfTypo ( this . resourceConfig . columns . map ( ( c ) => c . name ) , col ) ;
121+ errors . push ( `Column '${ col } ' defined in CSV not found in resource '${ this . resourceConfig . resourceId } '. ${
122+ similar ? `If you mean '${ similar } ', rename it in CSV` : 'If column is in database but not in resource configuration, add it with showIn:[]' } `
123+ ) ;
124+ }
125+ } ) ;
126+ if ( errors . length > 0 ) {
127+ return { ok : false , errors } ;
128+ }
129+
130+ const columnValues : any [ ] = Object . values ( data ) ;
131+ for ( let i = 0 ; i < columnValues [ 0 ] . length ; i ++ ) {
132+ const row = { } ;
133+ for ( let j = 0 ; j < columns . length ; j ++ ) {
134+ row [ columns [ j ] ] = columnValues [ j ] [ i ] ;
135+ }
136+ rows . push ( row ) ;
137+ }
138+
139+ let importedCount = 0 ;
140+ await Promise . all ( rows . map ( async ( row ) => {
141+ try {
142+ await this . adminforth . resource ( this . resourceConfig . resourceId ) . create ( row ) ;
143+ importedCount ++ ;
144+ } catch ( e ) {
145+ errors . push ( e . message ) ;
146+ }
147+ } ) ) ;
148+
149+
150+ return { ok : true , importedCount, errors} ;
151+
152+ }
153+ } ) ;
154+
155+ }
156+
157+ }
0 commit comments