diff --git a/admin/src/components/Federation/CommunityVisualizeItem.vue b/admin/src/components/Federation/CommunityVisualizeItem.vue index c3c8e70cb..5b3d9bc8b 100644 --- a/admin/src/components/Federation/CommunityVisualizeItem.vue +++ b/admin/src/components/Federation/CommunityVisualizeItem.vue @@ -37,8 +37,8 @@ {{ $t('geo-coordinates.format', { - latitude: location.coordinates[1], - longitude: location.coordinates[0], + latitude: location.latitude, + longitude: location.longitude, }) }} @@ -166,7 +166,7 @@ export default { return this.originalGmsApiKey !== this.gmsApiKey }, isValidLocation() { - return this.location && this.location.coordinates.length === 2 + return this.location && this.location.latitude && this.location.longitude }, }, methods: { diff --git a/admin/src/components/input/Coordinates.spec.js b/admin/src/components/input/Coordinates.spec.js index 25e73c554..21d1c3263 100644 --- a/admin/src/components/input/Coordinates.spec.js +++ b/admin/src/components/input/Coordinates.spec.js @@ -6,26 +6,26 @@ import VueI18n from 'vue-i18n' Vue.use(VueI18n) const localVue = global.localVue -const i18n = new VueI18n({ - locale: 'en', - messages: { - en: { - 'geo-coordinates.format': '{latitude}, {longitude}', - }, - }, -}) +const mocks = { + $t: jest.fn((t, v) => { + if (t === 'geo-coordinates.format') { + return `${v.latitude}, ${v.longitude}` + } + return t + }), +} describe('Coordinates', () => { let wrapper const value = { - type: 'Point', - coordinates: [12.34, 56.78], + latitude: 56.78, + longitude: 12.34, } const createWrapper = (propsData) => { return mount(Coordinates, { localVue, - i18n, + mocks, propsData, }) } @@ -49,8 +49,10 @@ describe('Coordinates', () => { await latitudeInput.setValue('34.56') await longitudeInput.setValue('78.90') - expect(wrapper.vm.latitude).toBe('34.56') - expect(wrapper.vm.longitude).toBe('78.90') + expect(wrapper.vm.inputValue).toStrictEqual({ + latitude: 34.56, + longitude: 78.9, + }) }) it('emits input event with updated values', async () => { @@ -60,46 +62,28 @@ describe('Coordinates', () => { await latitudeInput.setValue('34.56') expect(wrapper.emitted().input).toBeTruthy() expect(wrapper.emitted().input[0][0]).toEqual({ - type: 'Point', - coordinates: [12.34, 34.56], + latitude: 34.56, + longitude: 12.34, }) await longitudeInput.setValue('78.90') expect(wrapper.emitted().input).toBeTruthy() expect(wrapper.emitted().input[1][0]).toEqual({ - type: 'Point', - coordinates: [78.9, 34.56], + latitude: 34.56, + longitude: 78.9, }) }) - it('updates latitudeLongitude when latitude or longitude changes', async () => { - const latitudeInput = wrapper.find('#home-community-latitude') - const longitudeInput = wrapper.find('#home-community-longitude') - - await latitudeInput.setValue('34.56') - await longitudeInput.setValue('78.90') - - expect(wrapper.vm.latitudeLongitude).toBe('34.56, 78.90') - }) - it('splits coordinates correctly when entering in latitudeLongitude input', async () => { const latitudeLongitudeInput = wrapper.find('#home-community-latitude-longitude-smart') await latitudeLongitudeInput.setValue('34.56, 78.90') await latitudeLongitudeInput.trigger('input') - expect(wrapper.vm.latitude).toBe(34.56) - expect(wrapper.vm.longitude).toBe(78.9) - }) - - it('sets inputValue to null if coordinates are invalid', async () => { - const latitudeInput = wrapper.find('#home-community-latitude') - const longitudeInput = wrapper.find('#home-community-longitude') - - await latitudeInput.setValue('invalid') - await longitudeInput.setValue('78.90') - - expect(wrapper.vm.inputValue).toBeNull() + expect(wrapper.vm.inputValue).toStrictEqual({ + latitude: 34.56, + longitude: 78.9, + }) }) it('validates coordinates correctly', async () => { diff --git a/admin/src/components/input/Coordinates.vue b/admin/src/components/input/Coordinates.vue index 699ac29ea..b71e5d9d4 100644 --- a/admin/src/components/input/Coordinates.vue +++ b/admin/src/components/input/Coordinates.vue @@ -11,7 +11,7 @@ :description="$t('geo-coordinates.latitude-longitude-smart.describe')" > parseFloat(coord)) - // Remove null and NaN values - .filter((coord) => coord !== null && !isNaN(coord)) - if (this.inputValue.coordinates.length !== 2) { - this.inputValue = null - } + const parseNumber = (value) => { + const number = parseFloat(value) + return isNaN(number) ? '' : number } + return { + latitude: parseNumber(location.latitude), + longitude: parseNumber(location.longitude), + } + }, + getLatitudeLongitudeString({ latitude, longitude } = {}) { + return latitude && longitude ? this.$t('geo-coordinates.format', { latitude, longitude }) : '' + }, + valueUpdated(value) { + this.locationString = this.getLatitudeLongitudeString(this.inputValue) + this.inputValue = this.sanitizeLocation(this.inputValue) + if (this.isValid && this.isChanged) { if (this.$parent.valueChanged) { this.$parent.valueChanged() diff --git a/admin/src/graphql/updateHomeCommunity.js b/admin/src/graphql/updateHomeCommunity.js index f38ec9e1f..a05cb7fd8 100644 --- a/admin/src/graphql/updateHomeCommunity.js +++ b/admin/src/graphql/updateHomeCommunity.js @@ -1,7 +1,7 @@ import gql from 'graphql-tag' export const updateHomeCommunity = gql` - mutation ($uuid: String!, $gmsApiKey: String, $location: Point) { + mutation ($uuid: String!, $gmsApiKey: String, $location: Location) { updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey, location: $location) { id } diff --git a/backend/package.json b/backend/package.json index 7c6128a81..ba45225a2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -33,7 +33,6 @@ "email-templates": "^10.0.1", "express": "^4.17.1", "express-slow-down": "^2.0.1", - "geojson": "^0.5.0", "gradido-database": "file:../database", "graphql": "^15.5.1", "graphql-request": "5.0.0", @@ -58,7 +57,6 @@ "@types/email-templates": "^10.0.1", "@types/express": "^4.17.12", "@types/faker": "^5.5.9", - "@types/geojson": "^7946.0.13", "@types/i18n": "^0.13.4", "@types/jest": "^27.0.2", "@types/lodash.clonedeep": "^4.5.6", diff --git a/backend/src/graphql/input/EditCommunityInput.ts b/backend/src/graphql/input/EditCommunityInput.ts index 560ef3fcd..487221920 100644 --- a/backend/src/graphql/input/EditCommunityInput.ts +++ b/backend/src/graphql/input/EditCommunityInput.ts @@ -1,8 +1,8 @@ import { IsString, IsUUID } from 'class-validator' import { ArgsType, Field, InputType } from 'type-graphql' -import { Point } from '@/graphql/model/Point' -import { isValidPoint } from '@/graphql/validator/Point' +import { Location } from '@/graphql/model/Location' +import { isValidLocation } from '@/graphql/validator/Location' @ArgsType() @InputType() @@ -15,7 +15,7 @@ export class EditCommunityInput { @IsString() gmsApiKey?: string | null - @Field(() => Point, { nullable: true }) - @isValidPoint() - location?: Point | null + @Field(() => Location, { nullable: true }) + @isValidLocation() + location?: Location | null } diff --git a/backend/src/graphql/model/AdminCommunityView.ts b/backend/src/graphql/model/AdminCommunityView.ts index eb9197b73..b4a1664a7 100644 --- a/backend/src/graphql/model/AdminCommunityView.ts +++ b/backend/src/graphql/model/AdminCommunityView.ts @@ -1,9 +1,12 @@ +import { Point } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { ObjectType, Field } from 'type-graphql' +import { Point2Location } from '@/graphql/resolver/util/Location2Point' + import { FederatedCommunity } from './FederatedCommunity' -import { Point } from './Point' +import { Location } from './Location' @ObjectType() export class AdminCommunityView { @@ -37,7 +40,9 @@ export class AdminCommunityView { this.uuid = dbCom.communityUuid this.authenticatedAt = dbCom.authenticatedAt this.gmsApiKey = dbCom.gmsApiKey - this.location = dbCom.location + if (dbCom.location) { + this.location = Point2Location(dbCom.location as Point) + } } @Field(() => Boolean) @@ -64,8 +69,8 @@ export class AdminCommunityView { @Field(() => String, { nullable: true }) gmsApiKey: string | null - @Field(() => Point, { nullable: true }) - location: Point | null + @Field(() => Location, { nullable: true }) + location: Location | null @Field(() => Date, { nullable: true }) creationDate: Date | null diff --git a/backend/src/graphql/model/Point.ts b/backend/src/graphql/model/Point.ts deleted file mode 100644 index 45c4c3a29..000000000 --- a/backend/src/graphql/model/Point.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Position, Point as geojsonPoint } from 'geojson' - -export class Point implements geojsonPoint { - constructor() { - this.coordinates = [] - this.type = 'Point' - } - - type: 'Point' - coordinates: Position -} diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 4299dceb1..8a6d2992b 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -18,6 +18,7 @@ import { getCommunityByUuid, getHomeCommunity, } from './util/communities' +import { Location2Point } from './util/Location2Point' @Resolver() export class CommunityResolver { @@ -90,7 +91,9 @@ export class CommunityResolver { } if (homeCom.gmsApiKey !== gmsApiKey || homeCom.location !== location) { homeCom.gmsApiKey = gmsApiKey ?? null - homeCom.location = location ?? null + if (location) { + homeCom.location = Location2Point(location) + } await DbCommunity.save(homeCom) } return new Community(homeCom) diff --git a/backend/src/graphql/scalar/Point.ts b/backend/src/graphql/scalar/Point.ts deleted file mode 100644 index 5e4b7a53b..000000000 --- a/backend/src/graphql/scalar/Point.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -import { GraphQLScalarType, Kind } from 'graphql' - -import { Point } from '@/graphql/model/Point' - -export const PointScalar = new GraphQLScalarType({ - name: 'Point', - description: - 'The `Point` scalar type to represent longitude and latitude values of a geo location', - - serialize(value: Point) { - // Check type of value - if (value.type !== 'Point') { - throw new Error(`PointScalar can only serialize Geometry type 'Point' values`) - } - return value - }, - - parseValue(value): Point { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (value.type !== 'Point') { - throw new Error(`PointScalar can only deserialize Geometry type 'Point' values`) - } - return value as Point - }, - - parseLiteral(ast) { - if (ast.kind !== Kind.STRING) { - throw new TypeError(`${String(ast)} is not a valid Geometry value.`) - } - - const point = JSON.parse(ast.value) as Point - return point - }, -}) diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts index 4aa5ae2e1..bcb8081a6 100644 --- a/backend/src/graphql/schema.ts +++ b/backend/src/graphql/schema.ts @@ -7,10 +7,8 @@ import { buildSchema } from 'type-graphql' import { Location } from '@model/Location' import { isAuthorized } from './directive/isAuthorized' -import { Point } from './model/Point' import { DecimalScalar } from './scalar/Decimal' import { LocationScalar } from './scalar/Location' -import { PointScalar } from './scalar/Point' export const schema = async (): Promise => { return buildSchema({ @@ -19,7 +17,6 @@ export const schema = async (): Promise => { scalarsMap: [ { type: Decimal, scalar: DecimalScalar }, { type: Location, scalar: LocationScalar }, - { type: Point, scalar: PointScalar }, ], validate: { validationError: { target: false }, diff --git a/backend/src/graphql/validator/Point.ts b/backend/src/graphql/validator/Point.ts deleted file mode 100644 index 923fcf99e..000000000 --- a/backend/src/graphql/validator/Point.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator' - -import { Point } from '@/graphql/model/Point' - -export function isValidPoint(validationOptions?: ValidationOptions) { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isValidPoint', - target: object.constructor, - propertyName, - options: validationOptions, - validator: { - validate(value: Point) { - if (value.type === 'Point') { - if (value.coordinates.length === 2) { - return value.coordinates.every((coord) => typeof coord === 'number') - } - } - return false - }, - defaultMessage(args: ValidationArguments) { - return `${propertyName} must be a valid Point in geoJSON Format, ${args.property}` - }, - }, - }) - } -} diff --git a/backend/yarn.lock b/backend/yarn.lock index caddf50a0..5c33b1ce8 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -994,11 +994,6 @@ dependencies: "@types/node" "*" -"@types/geojson@^7946.0.13": - version "7946.0.14" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" - integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== - "@types/glob@^7.1.3": version "7.1.4" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672" diff --git a/database/entity/0085-add_community_location/Community.ts b/database/entity/0085-add_community_location/Community.ts index 2bd4f0ced..60410c8ce 100644 --- a/database/entity/0085-add_community_location/Community.ts +++ b/database/entity/0085-add_community_location/Community.ts @@ -7,7 +7,7 @@ import { UpdateDateColumn, OneToMany, JoinColumn, - Point, + Geometry, } from 'typeorm' import { FederatedCommunity } from '../FederatedCommunity' import { GeometryTransformer } from '../../src/typeorm/GeometryTransformer' @@ -56,12 +56,12 @@ export class Community extends BaseEntity { @Column({ name: 'location', - type: 'point', + type: 'geometry', default: null, nullable: true, transformer: GeometryTransformer, }) - location: Point | null + location: Geometry | null @CreateDateColumn({ name: 'created_at', diff --git a/database/migrations/0085-add_community_location.ts b/database/migrations/0085-add_community_location.ts index 1a5b233c7..b6f0ea917 100644 --- a/database/migrations/0085-add_community_location.ts +++ b/database/migrations/0085-add_community_location.ts @@ -4,7 +4,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { await queryFn( - 'ALTER TABLE `communities` ADD COLUMN IF NOT EXISTS `location` POINT DEFAULT NULL NULL AFTER `gms_api_key`;', + 'ALTER TABLE `communities` ADD COLUMN IF NOT EXISTS `location` geometry DEFAULT NULL NULL AFTER `gms_api_key`;', ) }