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 a1a3b26..36e5401 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 fb6ca86..5d5c1ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,9 @@ "bcrypt": "^5.0.1", "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", @@ -30,6 +32,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", @@ -2491,6 +2494,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", @@ -2517,6 +2530,15 @@ "integrity": "sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ==", "dev": true }, + "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", @@ -4091,6 +4113,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", @@ -4868,7 +4919,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" }, @@ -5190,7 +5240,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" } @@ -8443,6 +8492,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", @@ -11301,6 +11355,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", @@ -11327,6 +11391,15 @@ "integrity": "sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ==", "dev": true }, + "@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", @@ -12532,6 +12605,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", @@ -13166,7 +13265,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" } @@ -13393,8 +13491,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", @@ -15783,6 +15880,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 c6bc5c7..3c438d3 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,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 @@ "bcrypt": "^5.0.1", "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 ( +
+ ) +} + +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