diff --git a/.adminjs/.entry.js b/.adminjs/.entry.js index c9edeb9..8af1cf1 100644 --- a/.adminjs/.entry.js +++ b/.adminjs/.entry.js @@ -1 +1,3 @@ AdminJS.UserComponents = {} +import Component1 from '../src/admin/components/import-action-component' +AdminJS.UserComponents.Component1 = Component1 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 90acf54..95a6695 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: - 5432 volumes: - ./dbs/postgres-data:/var/lib/postgresql + env_file: + - .env backend: container_name: soroka-backend diff --git a/package-lock.json b/package-lock.json index 6002679..ca0c195 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@types/bcryptjs": "^2.4.2", "@types/cors": "^2.8.12", "@types/express": "^4.17.13", + "@types/express-formidable": "^1.2.0", "@types/express-session": "^1.17.4", "@types/flat": "^5.0.2", "@types/node": "^17.0.8", @@ -2444,6 +2445,16 @@ "@types/serve-static": "*" } }, + "node_modules/@types/express-formidable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/express-formidable/-/express-formidable-1.2.0.tgz", + "integrity": "sha512-nmqjkWllce71jVB0dWilJHyv676GmYESDtY12gcxgUwwl6J55FaOEAfn9bcShnydxj4f+AWIN1gEF4ItXyubUA==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/formidable": "*" + } + }, "node_modules/@types/express-serve-static-core": { "version": "4.17.28", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", @@ -2467,6 +2478,15 @@ "resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.2.tgz", "integrity": "sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ==" }, + "node_modules/@types/formidable": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.5.tgz", + "integrity": "sha512-uvMcdn/KK3maPOaVUAc3HEYbCEhjaGFwww4EsX6IJfWIJ1tzHtDHczuImH3GKdusPnAAmzB07St90uabZeCKPA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/history": { "version": "4.7.11", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", @@ -3951,6 +3971,35 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" }, + "node_modules/csv": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.1.3.tgz", + "integrity": "sha512-AO1ZWeHGHZ4rZvB0h2U4iQQwZmDHKuvNooFCz/74/CsYj7NK0H8HLsepEDRQDGjmeSTlx9JX+KVQjyWFFO0FwA==", + "dependencies": { + "csv-generate": "^4.1.0", + "csv-parse": "^5.2.0", + "csv-stringify": "^6.1.3", + "stream-transform": "^3.1.0" + }, + "engines": { + "node": ">= 0.1.90" + } + }, + "node_modules/csv-generate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.1.0.tgz", + "integrity": "sha512-Z17wI0xmDfpwzB7lShyK7INBt0YMyh5kV7svWTwsBSOa30T6Lq1fHHasmSCtf2rRTI7GnTk52HQRYBk0ToAXQQ==" + }, + "node_modules/csv-parse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.2.0.tgz", + "integrity": "sha512-ZuLjTp3Qx2gycoB7FKS9q11KgDL3f0wQszTlNOajS3fHa0jypN/zgjmkam+rczX5dXw5z7+KrDW2hWkM4542Ug==" + }, + "node_modules/csv-stringify": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.1.3.tgz", + "integrity": "sha512-jK2oj6VQafoke0uBXYqYhlwLg4d5qNYjikld+oMgdkOiSiFJygDfQJsiK67igX13Vb/DFnCJexnlhiSErjKR4g==" + }, "node_modules/d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", @@ -4692,7 +4741,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/express-formidable/-/express-formidable-1.2.0.tgz", "integrity": "sha512-w1vXjF3gb50UKTNkFaW8/4rqY4dUrKfZ1sAZzwAF9YxCAgj/29QZsycf71di0GkskrZOAkubk9pvGYfxyAMYiw==", - "peer": true, "dependencies": { "formidable": "^1.0.17" }, @@ -5004,7 +5052,6 @@ "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", - "peer": true, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } @@ -8112,6 +8159,11 @@ "node": ">= 0.8" } }, + "node_modules/stream-transform": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.1.0.tgz", + "integrity": "sha512-ncT/rST/M2N2zGLacJmeB7TNq/XS8Ck0xqvhYX6bgLwSPTSZLVFCSvNnwTU+RYhk9fAfwzCkYH9I5kdgcDMvfw==" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -10876,6 +10928,16 @@ "@types/serve-static": "*" } }, + "@types/express-formidable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/express-formidable/-/express-formidable-1.2.0.tgz", + "integrity": "sha512-nmqjkWllce71jVB0dWilJHyv676GmYESDtY12gcxgUwwl6J55FaOEAfn9bcShnydxj4f+AWIN1gEF4ItXyubUA==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/formidable": "*" + } + }, "@types/express-serve-static-core": { "version": "4.17.28", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", @@ -10899,6 +10961,15 @@ "resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.2.tgz", "integrity": "sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ==" }, + "@types/formidable": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.5.tgz", + "integrity": "sha512-uvMcdn/KK3maPOaVUAc3HEYbCEhjaGFwww4EsX6IJfWIJ1tzHtDHczuImH3GKdusPnAAmzB07St90uabZeCKPA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/history": { "version": "4.7.11", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", @@ -12030,6 +12101,32 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" }, + "csv": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.1.3.tgz", + "integrity": "sha512-AO1ZWeHGHZ4rZvB0h2U4iQQwZmDHKuvNooFCz/74/CsYj7NK0H8HLsepEDRQDGjmeSTlx9JX+KVQjyWFFO0FwA==", + "requires": { + "csv-generate": "^4.1.0", + "csv-parse": "^5.2.0", + "csv-stringify": "^6.1.3", + "stream-transform": "^3.1.0" + } + }, + "csv-generate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.1.0.tgz", + "integrity": "sha512-Z17wI0xmDfpwzB7lShyK7INBt0YMyh5kV7svWTwsBSOa30T6Lq1fHHasmSCtf2rRTI7GnTk52HQRYBk0ToAXQQ==" + }, + "csv-parse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.2.0.tgz", + "integrity": "sha512-ZuLjTp3Qx2gycoB7FKS9q11KgDL3f0wQszTlNOajS3fHa0jypN/zgjmkam+rczX5dXw5z7+KrDW2hWkM4542Ug==" + }, + "csv-stringify": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.1.3.tgz", + "integrity": "sha512-jK2oj6VQafoke0uBXYqYhlwLg4d5qNYjikld+oMgdkOiSiFJygDfQJsiK67igX13Vb/DFnCJexnlhiSErjKR4g==" + }, "d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", @@ -12631,7 +12728,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/express-formidable/-/express-formidable-1.2.0.tgz", "integrity": "sha512-w1vXjF3gb50UKTNkFaW8/4rqY4dUrKfZ1sAZzwAF9YxCAgj/29QZsycf71di0GkskrZOAkubk9pvGYfxyAMYiw==", - "peer": true, "requires": { "formidable": "^1.0.17" } @@ -12848,8 +12944,7 @@ "formidable": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", - "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", - "peer": true + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==" }, "forwarded": { "version": "0.2.0", @@ -15125,6 +15220,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "stream-transform": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.1.0.tgz", + "integrity": "sha512-ncT/rST/M2N2zGLacJmeB7TNq/XS8Ck0xqvhYX6bgLwSPTSZLVFCSvNnwTU+RYhk9fAfwzCkYH9I5kdgcDMvfw==" + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/package.json b/package.json index 031edb5..e41d62b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@types/bcrypt": "^5.0.0", "@types/cors": "^2.8.12", "@types/express": "^4.17.13", + "@types/express-formidable": "^1.2.0", "@types/express-session": "^1.17.4", "@types/flat": "^5.0.2", "@types/node": "^17.0.8", @@ -47,7 +48,9 @@ "bcryptjs": "^2.4.3", "body-parser": "^1.19.1", "cors": "^2.8.5", + "csv": "^6.1.3", "express": "^4.17.2", + "express-formidable": "^1.2.0", "passport": "^0.5.2", "passport-jwt": "^4.0.0", "pg": "^8.7.3", diff --git a/src/admin/components/import-action-component.jsx b/src/admin/components/import-action-component.jsx new file mode 100644 index 0000000..ba752e3 --- /dev/null +++ b/src/admin/components/import-action-component.jsx @@ -0,0 +1,44 @@ +import React from 'react' + +const ImportAction = () => { + async function submitForm(form) { + const formData = new FormData(form) + + const response = await fetch( + '/v1/admin/import-csv', + { + method: 'POST', + body: formData + } + ) + + if (response.status === 200) { + form.reset() + + alert('Импорт завершён') + } + } + + return ( +
{ + event.preventDefault() + submitForm(event.target) + }} + > + + + +
+ ) +} + +export default ImportAction diff --git a/src/controllers/cards/Card.ts b/src/controllers/cards/Card.ts index 83ba076..8477146 100644 --- a/src/controllers/cards/Card.ts +++ b/src/controllers/cards/Card.ts @@ -7,9 +7,13 @@ class CardController implements ICardController { } getAll = async (request: Request, response: Response) => { - const cardsList = await this.cardService.getAll(request.user) + const cardsResponse = await this.cardService.getAll( + request.user, + Number(request.query.limit) || null, + Number(request.query.offset) || null + ) - return response.send(cardsList) + return response.send(cardsResponse) } create = async (request: Request, response: Response) => { diff --git a/src/controllers/users/User.ts b/src/controllers/users/User.ts index 8733651..61b8e47 100644 --- a/src/controllers/users/User.ts +++ b/src/controllers/users/User.ts @@ -75,13 +75,13 @@ class UserController implements IUserController { if (tokens) { const { accessToken, refreshToken } = tokens - response.send({ accessToken, refreshToken }) + return response.send({ accessToken, refreshToken }) } } - response.status(401).send({ error: 'Login or password is incorrect!' }) + return response.status(401).send({ error: 'Login or password is incorrect!' }) } catch (e: any) { - response.status(500).send({ error: e }) + return response.status(500).send({ error: 'Internal server error' }) } } @@ -104,13 +104,13 @@ class UserController implements IUserController { if (tokens) { const { accessToken, refreshToken } = tokens - response.send({ accessToken, refreshToken }) + return response.send({ accessToken, refreshToken }) } } - response.status(401).send({ error: 'Invalid credentials' }) + return response.status(401).send({ error: 'Invalid credentials' }) } catch (e) { - response.status(500).send({ error: e }) + return response.status(500).send({ error: 'Internal server error' }) } } } diff --git a/src/core/swagger.json b/src/core/swagger.json index b05831a..2a0d2ca 100644 --- a/src/core/swagger.json +++ b/src/core/swagger.json @@ -366,13 +366,29 @@ "get": { "description": "Список карточек", "tags": ["cards"], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": true, + "type": "number", + "example": 2 + }, + { + "name": "offset", + "in": "query", + "required": true, + "type": "number", + "example": 20 + } + ], "responses": { "200": { "name": "obj", "in": "body", "description": "Список карточек", "schema": { - "$ref": "#/definitions/CardsResponse" + "$ref": "#/definitions/PaginatedCardsResponse" } } } @@ -938,6 +954,25 @@ } } }, + "PaginatedCardsResponse": { + "type": "object", + "properties": { + "total": { + "type": "number", + "example": 2200 + }, + "hasNextPage": { + "type": "boolean", + "example": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/CardsResponse" + } + } + } + }, "CardData": { "type": "object", "properties": { diff --git a/src/migrations/20220620145536-filled-property-delete-field-name.js b/src/migrations/20220620145536-filled-property-delete-field-name.js new file mode 100644 index 0000000..9ca0854 --- /dev/null +++ b/src/migrations/20220620145536-filled-property-delete-field-name.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.removeColumn('FilledProperties', 'name') + }, + + async down (queryInterface, Sequelize) { + await queryInterface.addColumn('FilledProperties', 'name', { + type: Sequelize.STRING, + allowNull: false + }) + } +}; diff --git a/src/migrations/20220620145537-filled-property-alter-field-data.js b/src/migrations/20220620145537-filled-property-alter-field-data.js new file mode 100644 index 0000000..af51e73 --- /dev/null +++ b/src/migrations/20220620145537-filled-property-alter-field-data.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.changeColumn('FilledProperties', 'data', { + type: Sequelize.STRING(100000), + }) + }, + + async down (queryInterface, Sequelize) { + await queryInterface.changeColumn('FilledProperties', 'data', { + type: Sequelize.STRING(255) + }) + } +}; diff --git a/src/models/cards/DataType.ts b/src/models/cards/DataType.ts index 62b568f..f376d06 100644 --- a/src/models/cards/DataType.ts +++ b/src/models/cards/DataType.ts @@ -1,10 +1,19 @@ -import { Table, Column, Model, AllowNull } from 'sequelize-typescript' +import { Table, Column, Model, AllowNull, HasMany, Scopes } from 'sequelize-typescript' +import Property from './Property' +@Scopes(() => ({ + short: { + attributes: ['name'] + } +})) @Table class DataType extends Model { @AllowNull(false) @Column name: string + + @HasMany(() => Property) + properties: Property[] } export default DataType diff --git a/src/models/cards/FilledProperty.ts b/src/models/cards/FilledProperty.ts index 4bb4b10..74c9474 100644 --- a/src/models/cards/FilledProperty.ts +++ b/src/models/cards/FilledProperty.ts @@ -8,7 +8,9 @@ import { DataType as DT, BeforeUpdate, HasMany, - DefaultScope + DefaultScope, + Length, + BelongsTo } from 'sequelize-typescript' import Card, { FilledPropertyCard } from './Card' import DataType from './DataType' @@ -17,19 +19,17 @@ import Property from './Property' import GeoProperty from './GeoProperty' @DefaultScope(() => ({ - include: [GeoProperty] + include: [GeoProperty, Property.scope('dataType')], + attributes: { exclude: ['FilledPropertyCard', 'createdAt', 'updatedAt'] } })) @Table class FilledProperty extends Model { - @AllowNull(false) - @Column - name: string - @AllowNull(false) @ForeignKey(() => Property) @Column propertyId: number + @Length({ max: 100000 }) @Column(DT.JSON) data: string @@ -39,6 +39,9 @@ class FilledProperty extends Model { @HasMany(() => GeoProperty) geoProperty: GeoProperty + @BelongsTo(() => Property) + property: Property + @BeforeUpdate static async onJulianDateChanged(instance: FilledProperty) { // FIXME: добавить BelongsTo во всех связанных моделях @@ -57,6 +60,10 @@ class FilledProperty extends Model { const dateStart = data[0].jd const dateEnd = data[0].jd + if (!dateStart) { + return + } + const filledPropertyId = instance.id await DateCatalog.create({ dateStart, dateEnd, filledPropertyId }) diff --git a/src/models/cards/Property.ts b/src/models/cards/Property.ts index 3fee242..ba9d031 100644 --- a/src/models/cards/Property.ts +++ b/src/models/cards/Property.ts @@ -1,6 +1,20 @@ -import { Table, Column, Model, AllowNull, ForeignKey } from 'sequelize-typescript' +import { Table, Column, Model, AllowNull, ForeignKey, HasMany, Scopes, BelongsTo } from 'sequelize-typescript' import DataType from './DataType' +import FilledProperty from './FilledProperty' +@Scopes(() => ({ + dataType: { + include: [DataType.scope('short')], + attributes: ['id', 'name'] + }, + detail: { + include: [DataType.scope('short')], + attributes: [ + ['id', 'propertyId'], + 'name', 'isLink' + ] + } +})) @Table class Property extends Model { @AllowNull(false) @@ -15,6 +29,13 @@ class Property extends Model { @AllowNull(false) @Column isLink: boolean + + @HasMany(() => FilledProperty) + filledProperties: FilledProperty[] + + @BelongsTo(() => DataType) + dataType: DataType } export default Property + diff --git a/src/routes/admin/admin.ts b/src/routes/admin/admin.ts index 1e2a7eb..7e6f615 100644 --- a/src/routes/admin/admin.ts +++ b/src/routes/admin/admin.ts @@ -25,8 +25,22 @@ const adminJs = new AdminJS({ User, UserRole, Organization, AuthorizationLink, RefreshToken, DataType, Property, FilledProperty, - CardTemplate, Card, FilledPropertyCard, - DateCatalog, Calendar, GeoProperty + CardTemplate, FilledPropertyCard, + DateCatalog, Calendar, GeoProperty, + { + resource: Card, + options: { + actions: { + CSVImport: { + actionType: 'resource', + icon: 'Upload', + isVisible: true, + handler: async () => { console.log() }, + component: AdminJS.bundle('../../admin/components/import-action-component.jsx'), + }, + }, + }, + } ], branding: { companyName: 'AdminJS', diff --git a/src/routes/v1/cards/CsvImport.ts b/src/routes/v1/cards/CsvImport.ts new file mode 100644 index 0000000..4f3d42e --- /dev/null +++ b/src/routes/v1/cards/CsvImport.ts @@ -0,0 +1,140 @@ +import express from "express" +import formidable from "express-formidable" +import { parse } from "csv-parse/sync" +import fs from "fs" +import Card, { FilledPropertyCard } from "../../../models/cards/Card" +import FilledProperty from "../../../models/cards/FilledProperty" +import GeoProperty from "../../../models/cards/GeoProperty" +import Property from "../../../models/cards/Property" + +class JulianDate { + date: Date + + // julian constants + private JULIAN_1970 = 2440588 + private DAY_IN_MS = 1000 * 60 * 60 * 24 + + constructor(date: Date) { + this.date = date + } + + getJulianDate = () => { + return this.date.valueOf() / this.DAY_IN_MS - 0.5 + this.JULIAN_1970 + } +} + +class Controller { + import = async (request: any, response: any): Promise => { + const csvFile = request.files.csvFile + + const csvFileData = fs.readFileSync(csvFile.path) + + const cards = parse(csvFileData, { + columns: true, + skip_empty_lines: true, + delimiter: ';' + }) + + const geoPointProp = await Property.findOne({ where: { name: 'geoPoint' } }) + const cyteProp = await Property.findOne({ where: { name: 'quote' } }) + const julianDateProp = await Property.findOne({ where: { name: 'julianDate' } }) + const sourceProp = await Property.findOne({ where: { name: 'sources' } }) + const tagsProp = await Property.findOne({ where: { name: 'tags' } }) + const annotationProp = await Property.findOne({ where: { name: 'annotation' } }) + + for (const card of cards) { + const cardData = { + name: card.description, + organizationId: 1, + userId: 1, + preventDelete: false + } + + const geoPropertyData = { + propertyId: geoPointProp?.id + } + + const geoJsonData = { + name: card.place, + location: { + type: "Point", + coordinates: card.coords.split(',').map( + (coord: string) => parseFloat(coord) + ) + }, + filledPropertyId: null + } + + const julianDate = new JulianDate( + new Date(`${card.year}-${card.month}-${card.day}`) + ) + + const datePropertyData = { + propertyId: julianDateProp?.id, + data: [{ jd: julianDate.getJulianDate() }] + } + + const cytePropertyData = { + data: card.cyte, + propertyId: cyteProp?.id + } + + const sourcePropertyData = { + data: card.source, + propertyId: sourceProp?.id + } + + const tagsPropertyData = { + data: card.tags, + propertyId: tagsProp?.id + } + + const annotationPropertyData = { + data: card.annotation, + propertyId: annotationProp?.id + } + + // создадим гео-свойство + const createdGeoProperty = await FilledProperty.create(geoPropertyData) + + geoJsonData.filledPropertyId = createdGeoProperty.id + + await GeoProperty.create(geoJsonData) + + // создадим одним запросом все остальные свойства + const createdFilledProps = await FilledProperty.bulkCreate( + [ + datePropertyData, cytePropertyData, + sourcePropertyData, tagsPropertyData, + annotationPropertyData + ] + ) + + // создадим карточку + const createdCard = await Card.create(cardData) + + // запишем все свойства в карточку + const filledProps = createdFilledProps.map( + (prop) => { return { filledPropertyId: prop.id, cardId: createdCard.id } } + ) + + filledProps.push( + { filledPropertyId: createdGeoProperty.id, cardId: createdCard.id } + ) + + await FilledPropertyCard.bulkCreate(filledProps) + } + + + return response.send({ ok: true }) + } +} + +const controller = new Controller() + +const router: express.Router = express.Router() + +router.route('/import-csv') + .post(formidable(), controller.import) + +export default router diff --git a/src/routes/v1/index.ts b/src/routes/v1/index.ts index 5dda103..ee81a64 100644 --- a/src/routes/v1/index.ts +++ b/src/routes/v1/index.ts @@ -9,11 +9,13 @@ import cardTemplateRoutes from "./cards/CardTemplate" import organizationRoutes from "./organizations/Organization" import dateRoutes from "./dates/DateCatalog" import geoPropertyRoutes from "./cards/GeoProperty" +import adminRoutes from "./cards/CsvImport" import { ControllerContainer } from "../../types" function getRouter(container: ControllerContainer) { const router: express.Router = express.Router() + router.use('/admin', adminRoutes) router.use('/users', userRoutes(container.userController)) router.use('/cards', cardRoutes(container.cardController)) router.use('/cards/templates', cardTemplateRoutes(container.cardTemplateController)) diff --git a/src/services/cards/Card.ts b/src/services/cards/Card.ts index 6676f43..a898c93 100644 --- a/src/services/cards/Card.ts +++ b/src/services/cards/Card.ts @@ -2,9 +2,10 @@ import { ICardService } from "../../interfaces" import Card from "../../models/cards/Card" import FilledProperty from "../../models/cards/FilledProperty" import UserRole from "../../models/users/UserRole" +import paginate from "../../utils/paginate" class CardService implements ICardService { - async getAll (user: any): Promise { + async getAll (user: any, limit?: number, offset?: number): Promise { const ALLOWED_ROLES = ['ADMIN', 'EDITOR'] if (!user) { @@ -17,19 +18,19 @@ class CardService implements ICardService { const filters = hasPermission ? {} : { organizationId: user.organization } - const cards: any = await Card.findAll({ where: {...filters} }) + const cards: any = await paginate(Card, filters, limit, offset) const cardsList = [] - for (const card of cards) { + for (const card of cards.results) { const cardObj = card.toJSON() let props = await card.getProperties() props = props.map((prop: FilledProperty) => { - const { id, name, propertyId, data } = prop + const { id, propertyId, data } = prop - return { id, name, propertyId, data } + return { id, propertyId, data } }) cardObj.propertiesList = props @@ -41,7 +42,11 @@ class CardService implements ICardService { cardsList.push(cardObj) } - return cardsList + return { + total: cards.total, + results: cardsList, + hasNextPage: cards.hasNextPage + } } async create (user: any, cardData: any): Promise { diff --git a/src/services/cards/Property.ts b/src/services/cards/Property.ts index 83b4b35..da9336b 100644 --- a/src/services/cards/Property.ts +++ b/src/services/cards/Property.ts @@ -3,10 +3,10 @@ import Property from '../../models/cards/Property' class PropertyService implements IPropertyService { async getAll(): Promise { - const properties = await Property.findAll() + const properties = await Property.scope('detail').findAll() return { - detail: properties.map((property: Property) => property.toJSON()), + detail: properties, status: 200 } } diff --git a/src/utils/paginate.ts b/src/utils/paginate.ts new file mode 100644 index 0000000..d9d70e3 --- /dev/null +++ b/src/utils/paginate.ts @@ -0,0 +1,23 @@ +const paginate = async (model: any, filters = {}, limit?: number, offset = 0) => { + const queryParams: any = { where: filters, offset } + + if (limit) { + queryParams.limit = limit + } + + const results = await model.findAndCountAll(queryParams) + + let hasNextPage = false + + if (limit) { + hasNextPage = (results.count - offset) > limit + } + + return { + total: results.count, + results: results.rows, + hasNextPage + } +} + +export default paginate