diff --git a/.gitignore b/.gitignore index e2ed488..cfe9a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -120,8 +120,6 @@ dist .DS_store # build files -/build -/lib /types .adminjs example-app/.adminjs \ No newline at end of file diff --git a/lib/components/ExportComponent.jsx b/lib/components/ExportComponent.jsx new file mode 100644 index 0000000..39d191a --- /dev/null +++ b/lib/components/ExportComponent.jsx @@ -0,0 +1,66 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getExportedFileName = exports.mimeTypes = void 0; +const tslib_1 = require("tslib"); +const react_1 = tslib_1.__importStar(require("react")); +const adminjs_1 = require("adminjs"); +const design_system_1 = require("@adminjs/design-system"); +const file_saver_1 = require("file-saver"); +const exporter_type_1 = require("../exporter.type"); +const format_1 = tslib_1.__importDefault(require("date-fns/format")); +exports.mimeTypes = { + json: 'application/json', + csv: 'text/csv', + xml: 'text/xml', +}; +const getExportedFileName = (extension) => `export-${(0, format_1.default)(Date.now(), 'yyyy-MM-dd_HH-mm')}.${extension}`; +exports.getExportedFileName = getExportedFileName; +const ExportComponent = ({ resource }) => { + const filter = {}; + const query = new URLSearchParams(location.search); + for (const entry of query.entries()) { + const [key, value] = entry; + if (key.match('filters.')) { + filter[key.replace('filters.', '')] = value; + } + } + const [isFetching, setFetching] = (0, react_1.useState)(); + const sendNotice = (0, adminjs_1.useNotice)(); + const exportData = async (type) => { + setFetching(true); + try { + const { data: { exportedData }, } = await new adminjs_1.ApiClient().resourceAction({ + method: 'post', + resourceId: resource.id, + actionName: 'export', + params: { + type, + filter + }, + }); + const blob = new Blob([exportedData], { type: exports.mimeTypes[type] }); + (0, file_saver_1.saveAs)(blob, (0, exports.getExportedFileName)(type)); + sendNotice({ message: 'Exported successfully', type: 'success' }); + } + catch (e) { + sendNotice({ message: e.message, type: 'error' }); + } + setFetching(false); + }; + if (isFetching) { + return ; + } + return ( + + Choose export format: + + + {exporter_type_1.Exporters.map(parserType => ( + exportData(parserType)} disabled={isFetching}> + {parserType.toUpperCase()} + + ))} + + ); +}; +exports.default = ExportComponent; diff --git a/lib/components/ImportComponent.jsx b/lib/components/ImportComponent.jsx new file mode 100644 index 0000000..29bce3f --- /dev/null +++ b/lib/components/ImportComponent.jsx @@ -0,0 +1,49 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const tslib_1 = require("tslib"); +const react_1 = tslib_1.__importStar(require("react")); +const adminjs_1 = require("adminjs"); +const design_system_1 = require("@adminjs/design-system"); +const ImportComponent = ({ resource }) => { + const [file, setFile] = (0, react_1.useState)(null); + const sendNotice = (0, adminjs_1.useNotice)(); + const [isFetching, setFetching] = (0, react_1.useState)(); + const onUpload = (uploadedFile) => { + var _a; + setFile((_a = uploadedFile === null || uploadedFile === void 0 ? void 0 : uploadedFile[0]) !== null && _a !== void 0 ? _a : null); + }; + const onSubmit = async () => { + if (!file) { + return; + } + setFetching(true); + try { + const importData = new FormData(); + importData.append('file', file, file === null || file === void 0 ? void 0 : file.name); + await new adminjs_1.ApiClient().resourceAction({ + method: 'post', + resourceId: resource.id, + actionName: 'import', + data: importData, + }); + sendNotice({ message: 'Imported successfully', type: 'success' }); + } + catch (e) { + sendNotice({ message: e.message, type: 'error' }); + } + setFetching(false); + }; + if (isFetching) { + return ; + } + return ( + + {file && ( setFile(null)}/>)} + + + Upload + + + ); +}; +exports.default = ImportComponent; diff --git a/lib/components/bundleComponents.js b/lib/components/bundleComponents.js new file mode 100644 index 0000000..f4b0e8b --- /dev/null +++ b/lib/components/bundleComponents.js @@ -0,0 +1,11 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.bundleComponents = void 0; +const tslib_1 = require("tslib"); +const adminjs_1 = tslib_1.__importDefault(require("adminjs")); +const bundleComponents = () => { + const EXPORT_COMPONENT = adminjs_1.default.bundle('../../src/components/ExportComponent'); + const IMPORT_COMPONENT = adminjs_1.default.bundle('../../src/components/ImportComponent'); + return { EXPORT_COMPONENT, IMPORT_COMPONENT }; +}; +exports.bundleComponents = bundleComponents; diff --git a/lib/export.handler.js b/lib/export.handler.js new file mode 100644 index 0000000..09356a4 --- /dev/null +++ b/lib/export.handler.js @@ -0,0 +1,15 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.exportHandler = void 0; +const parsers_1 = require("./parsers"); +const utils_1 = require("./utils"); +const exportHandler = async (request, response, context) => { + var _a, _b; + const parser = parsers_1.Parsers[(_b = (_a = request.query) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : 'json'].export; + const records = await (0, utils_1.getRecords)(context, request); + const parsedData = parser(records); + return { + exportedData: parsedData, + }; +}; +exports.exportHandler = exportHandler; diff --git a/lib/exporter.type.js b/lib/exporter.type.js new file mode 100644 index 0000000..fe80003 --- /dev/null +++ b/lib/exporter.type.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Exporters = void 0; +exports.Exporters = ['csv', 'json', 'xml']; diff --git a/lib/import.handler.js b/lib/import.handler.js new file mode 100644 index 0000000..e85e5a0 --- /dev/null +++ b/lib/import.handler.js @@ -0,0 +1,16 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.importHandler = void 0; +const tslib_1 = require("tslib"); +const fs_1 = tslib_1.__importDefault(require("fs")); +const util_1 = tslib_1.__importDefault(require("util")); +const utils_1 = require("./utils"); +const readFile = util_1.default.promisify(fs_1.default.readFile); +const importHandler = async (request, response, context) => { + const file = (0, utils_1.getFileFromRequest)(request); + const importer = (0, utils_1.getImporterByFileName)(file.name); + const fileContent = await readFile(file.path); + await importer(fileContent.toString(), context.resource); + return {}; +}; +exports.importHandler = importHandler; diff --git a/lib/importExportFeature.js b/lib/importExportFeature.js new file mode 100644 index 0000000..61496bd --- /dev/null +++ b/lib/importExportFeature.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const adminjs_1 = require("adminjs"); +const bundleComponents_1 = require("./components/bundleComponents"); +const utils_1 = require("./utils"); +const export_handler_1 = require("./export.handler"); +const import_handler_1 = require("./import.handler"); +const { EXPORT_COMPONENT, IMPORT_COMPONENT } = (0, bundleComponents_1.bundleComponents)(); +const importExportFeature = () => { + return (0, adminjs_1.buildFeature)({ + actions: { + export: { + handler: (0, utils_1.postActionHandler)(export_handler_1.exportHandler), + component: EXPORT_COMPONENT, + actionType: 'resource', + showFilter: true + }, + import: { + handler: (0, utils_1.postActionHandler)(import_handler_1.importHandler), + component: IMPORT_COMPONENT, + actionType: 'resource', + showFilter: true + }, + }, + }); +}; +exports.default = importExportFeature; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..38b6b62 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,11 @@ +"use strict"; +/** + * @module @adminjs/import-export + * @subcategory Features + * @section modules + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const tslib_1 = require("tslib"); +const importExportFeature_1 = tslib_1.__importDefault(require("./importExportFeature")); +tslib_1.__exportStar(require("./components/bundleComponents"), exports); +exports.default = importExportFeature_1.default; diff --git a/lib/modules/csv/csv.exporter.js b/lib/modules/csv/csv.exporter.js new file mode 100644 index 0000000..5a112c1 --- /dev/null +++ b/lib/modules/csv/csv.exporter.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.csvExporter = void 0; +const json2csv_1 = require("json2csv"); +const csvExporter = (records) => { + return (0, json2csv_1.parse)(records.map(r => r.params)); +}; +exports.csvExporter = csvExporter; diff --git a/lib/modules/csv/csv.importer.js b/lib/modules/csv/csv.importer.js new file mode 100644 index 0000000..723a562 --- /dev/null +++ b/lib/modules/csv/csv.importer.js @@ -0,0 +1,11 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.csvImporter = void 0; +const tslib_1 = require("tslib"); +const csvtojson_1 = tslib_1.__importDefault(require("csvtojson")); +const utils_1 = require("../../utils"); +const csvImporter = async (csvString, resource) => { + const records = await (0, csvtojson_1.default)().fromString(csvString); + return (0, utils_1.saveRecords)(records, resource); +}; +exports.csvImporter = csvImporter; diff --git a/lib/modules/json/json.exporter.js b/lib/modules/json/json.exporter.js new file mode 100644 index 0000000..925e4cc --- /dev/null +++ b/lib/modules/json/json.exporter.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.jsonExporter = void 0; +const jsonExporter = (records) => { + return JSON.stringify(records.map(r => r.params)); +}; +exports.jsonExporter = jsonExporter; diff --git a/lib/modules/json/json.importer.js b/lib/modules/json/json.importer.js new file mode 100644 index 0000000..f3353b9 --- /dev/null +++ b/lib/modules/json/json.importer.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.jsonImporter = void 0; +const utils_1 = require("../../utils"); +const jsonImporter = async (jsonString, resource) => { + const records = JSON.parse(jsonString); + return (0, utils_1.saveRecords)(records, resource); +}; +exports.jsonImporter = jsonImporter; diff --git a/lib/modules/xml/xml.exporter.js b/lib/modules/xml/xml.exporter.js new file mode 100644 index 0000000..cbca1d9 --- /dev/null +++ b/lib/modules/xml/xml.exporter.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.xmlExporter = void 0; +const tslib_1 = require("tslib"); +const xml_1 = tslib_1.__importDefault(require("xml")); +const xmlExporter = (records) => { + const data = records.map(record => ({ + record: Object.entries(record.params).map(([key, value]) => ({ + [key]: value, + })), + })); + return (0, xml_1.default)({ records: data }, { + indent: '\t', + declaration: true, + }); +}; +exports.xmlExporter = xmlExporter; diff --git a/lib/modules/xml/xml.importer.js b/lib/modules/xml/xml.importer.js new file mode 100644 index 0000000..f3b6a7e --- /dev/null +++ b/lib/modules/xml/xml.importer.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.xmlImporter = void 0; +const tslib_1 = require("tslib"); +const xml2js_1 = tslib_1.__importDefault(require("xml2js")); +const utils_1 = require("../../utils"); +const xmlImporter = async (xmlString, resource) => { + const parser = new xml2js_1.default.Parser({ explicitArray: false }); + const { records: { record }, } = await parser.parseStringPromise(xmlString); + return (0, utils_1.saveRecords)(record, resource); +}; +exports.xmlImporter = xmlImporter; diff --git a/lib/parsers.js b/lib/parsers.js new file mode 100644 index 0000000..064092b --- /dev/null +++ b/lib/parsers.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Parsers = void 0; +const json_exporter_1 = require("./modules/json/json.exporter"); +const json_importer_1 = require("./modules/json/json.importer"); +const csv_exporter_1 = require("./modules/csv/csv.exporter"); +const xml_exporter_1 = require("./modules/xml/xml.exporter"); +const csv_importer_1 = require("./modules/csv/csv.importer"); +const xml_importer_1 = require("./modules/xml/xml.importer"); +exports.Parsers = { + json: { export: json_exporter_1.jsonExporter, import: json_importer_1.jsonImporter }, + csv: { export: csv_exporter_1.csvExporter, import: csv_importer_1.csvImporter }, + xml: { export: xml_exporter_1.xmlExporter, import: xml_importer_1.xmlImporter }, +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..e8c7972 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,65 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getRecords = exports.getFileFromRequest = exports.postActionHandler = exports.getImporterByFileName = exports.saveRecords = void 0; +const adminjs_1 = require("adminjs"); +const csv_importer_1 = require("./modules/csv/csv.importer"); +const json_importer_1 = require("./modules/json/json.importer"); +const xml_importer_1 = require("./modules/xml/xml.importer"); +const saveRecords = async (records, resource) => { + return Promise.all(records.map(async (record) => { + try { + return await resource.create(record); + } + catch (e) { + console.error(e); + return e; + } + })); +}; +exports.saveRecords = saveRecords; +const getImporterByFileName = (fileName) => { + if (fileName.includes('.json')) { + return json_importer_1.jsonImporter; + } + if (fileName.includes('.csv')) { + return csv_importer_1.csvImporter; + } + if (fileName.includes('.xml')) { + return xml_importer_1.xmlImporter; + } + throw new Error('No parser found'); +}; +exports.getImporterByFileName = getImporterByFileName; +const postActionHandler = (handler) => async (request, response, context) => { + if (request.method !== 'post') { + return {}; + } + return handler(request, response, context); +}; +exports.postActionHandler = postActionHandler; +const getFileFromRequest = (request) => { + var _a; + const file = (_a = request.payload) === null || _a === void 0 ? void 0 : _a.file; + if (!(file === null || file === void 0 ? void 0 : file.path)) { + throw new adminjs_1.ValidationError({ + file: { message: 'No file uploaded' }, + }); + } + return file; +}; +exports.getFileFromRequest = getFileFromRequest; +const getRecords = async (context, request) => { + var _a, _b, _c, _d, _e, _f; + const idProperty = (_b = (_a = context.resource + .properties() + .find(p => p.isId())) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.call(_a); + const titleProperty = (_d = (_c = context.resource.decorate().titleProperty()) === null || _c === void 0 ? void 0 : _c.name) === null || _d === void 0 ? void 0 : _d.call(_c); + return context.resource.find(new adminjs_1.Filter(((_e = request === null || request === void 0 ? void 0 : request.query) === null || _e === void 0 ? void 0 : _e.filter) ? JSON.stringify((_f = request === null || request === void 0 ? void 0 : request.query) === null || _f === void 0 ? void 0 : _f.filter) : {}, context.resource), { + limit: Number.MAX_SAFE_INTEGER, + sort: { + sortBy: idProperty !== null && idProperty !== void 0 ? idProperty : titleProperty, + direction: 'asc', + }, + }); +}; +exports.getRecords = getRecords; diff --git a/package.json b/package.json index f811c95..f2e0423 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ } }, "private": false, - "repository": "git@github.com/MenahemOwlytics/adminjs-import-export.git", + "repository": "git@github.com:MenahemOwlytics/adminjs-import-export.git", "license": "SEE LICENSE IN LICENSE", "scripts": { "release": "semantic-release", diff --git a/src/components/ExportComponent.tsx b/src/components/ExportComponent.tsx index 9cb506a..a41ca21 100644 --- a/src/components/ExportComponent.tsx +++ b/src/components/ExportComponent.tsx @@ -3,8 +3,6 @@ import { ActionProps, ApiClient, useNotice } from 'adminjs'; import { Box, Button, Loader, Text } from '@adminjs/design-system'; import { saveAs } from 'file-saver'; import format from 'date-fns/format'; -import { useSearchParams } from "react-router-dom"; - import { Exporters, ExporterType } from '../exporter.type.js'; @@ -18,12 +16,12 @@ export const getExportedFileName = (extension: string) => `export-${format(Date.now(), 'yyyy-MM-dd_HH-mm')}.${extension}`; const ExportComponent: FC = ({ resource }) => { - const filter: Record = {} - const query = new URLSearchParams(location.search) + const filter: Record = {}; + const query = new URLSearchParams(location.search); for (const entry of query.entries()) { - const [key, value] = entry + const [key, value] = entry; if (key.match('filters.')) { - filter[key.replace('filters.', '')] = value + filter[key.replace('filters.', '')] = value; } } @@ -41,7 +39,7 @@ const ExportComponent: FC = ({ resource }) => { actionName: 'export', params: { type, - filter + filter, }, }); diff --git a/src/export.handler.ts b/src/export.handler.ts index 2652201..322a9e8 100644 --- a/src/export.handler.ts +++ b/src/export.handler.ts @@ -10,7 +10,7 @@ export const exportHandler: ActionHandler = async ( ) => { const parser = Parsers[request.query?.type ?? 'json'].export; - const records = await getRecords(context,request); + const records = await getRecords(context, request); const parsedData = parser(records); return { diff --git a/src/importExportFeature.ts b/src/importExportFeature.ts index 9861315..6c9cb33 100644 --- a/src/importExportFeature.ts +++ b/src/importExportFeature.ts @@ -25,13 +25,13 @@ const importExportFeature = ( handler: postActionHandler(exportHandler), component: exportComponent, actionType: 'resource', - showFilter: true + showFilter: true, }, import: { handler: postActionHandler(importHandler), component: importComponent, actionType: 'resource', - showFilter: true + showFilter: true, }, }, }); diff --git a/src/utils.ts b/src/utils.ts index c74e777..bcc6cf2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -75,11 +75,17 @@ export const getRecords = async ( ?.name?.(); const titleProperty = context.resource.decorate().titleProperty()?.name?.(); - return context.resource.find(new Filter(request?.query?.filter ? JSON.stringify(request?.query?.filter) : {}, context.resource), { - limit: Number.MAX_SAFE_INTEGER, - sort: { - sortBy: idProperty ?? titleProperty, - direction: 'asc', - }, - }); + return context.resource.find( + new Filter( + request?.query?.filter ? JSON.stringify(request?.query?.filter) : {}, + context.resource + ), + { + limit: Number.MAX_SAFE_INTEGER, + sort: { + sortBy: idProperty ?? titleProperty, + direction: 'asc', + }, + } + ); };