From 1f4fadfd5f1d904df4495e05bb30a60207e9da9f Mon Sep 17 00:00:00 2001 From: Dovile Date: Tue, 28 May 2024 22:58:22 +0300 Subject: [PATCH 01/16] location fixes --- .../20240522080439_recentLocationsView.js | 34 ++ services/fishStockings.service.ts | 386 +++++++++++++++--- services/locations.service.ts | 160 ++++---- services/recentLocations.service.ts | 118 ++++++ 4 files changed, 551 insertions(+), 147 deletions(-) create mode 100644 database/migrations/20240522080439_recentLocationsView.js create mode 100644 services/recentLocations.service.ts diff --git a/database/migrations/20240522080439_recentLocationsView.js b/database/migrations/20240522080439_recentLocationsView.js new file mode 100644 index 0000000..385d435 --- /dev/null +++ b/database/migrations/20240522080439_recentLocationsView.js @@ -0,0 +1,34 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +const { commonFields } = require('./20230405144107_setup'); +exports.up = function (knex) { + return knex.schema.dropTable('recentLocations').raw(` + CREATE VIEW recent_locations AS + SELECT DISTINCT ON (location->>'cadastral_id', "created_by", "tenant_id") + location->>'name' AS name, + location->>'cadastral_id' AS cadastral_id, + location->>'municipality' AS municipality, + "tenant_id", + "created_by" AS user_id, + "id" AS "geom", + "event_time" + FROM fish_stockings + ORDER BY location->>'cadastral_id', "user_id", "tenant_id", "event_time" DESC; + `); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.dropView('recentLocations').createTable('recentLocations', (table) => { + table.increments('id'); + table.integer('tenant'); + table.integer('user'); + table.jsonb('recent'); + commonFields(table); + }); +}; diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index c2c0546..a6b1333 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -2,7 +2,6 @@ import { isEmpty, map } from 'lodash'; import moleculer, { Context } from 'moleculer'; -import { DbContextParameters } from 'moleculer-db'; import { Action, Event, Method, Service } from 'moleculer-decorators'; import ApiGateway from 'moleculer-web'; import XLSX from 'xlsx'; @@ -67,7 +66,10 @@ interface Fields extends CommonFields { area: number; cadastral_id: string; name: string; - municipality: string; + municipality: { + id: number; + name: string; + }; }; location: { name: string; @@ -166,11 +168,18 @@ export type FishStocking< fishOriginReservoir: { type: 'object', required: false, + raw: true, properties: { area: 'number', name: 'string', cadastral_id: 'string', - municipality: 'string', + municipality: { + type: 'object', + properties: { + id: 'number', + name: 'string', + }, + }, }, }, location: { @@ -259,17 +268,6 @@ export type FishStocking< reviewLocation: { type: 'any', raw: true, - populate(ctx: any, _values: any, fishStockings: FishStocking[]) { - return Promise.all( - fishStockings.map((fishStocking) => { - return ctx.call('fishStockings.getGeometryJson', { - field: 'reviewLocation', - asField: 'reviewLocation', - id: fishStocking.id, - }); - }), - ); - }, }, reviewTime: 'date', waybillNo: 'string', @@ -398,10 +396,52 @@ export type FishStocking< ...COMMON_FIELDS, }, scopes: { + profile(query: any, ctx: Context, params: any) { + if (ctx.meta) { + // adminai + if ( + !ctx.meta.user && + ctx.meta.authUser && + (ctx.meta.authUser.type === AuthUserRole.ADMIN || + ctx.meta.authUser.type === AuthUserRole.SUPER_ADMIN) + ) { + if (isEmpty(ctx.meta.authUser.municipalities)) { + throw new ApiGateway.Errors.UnAuthorizedError('NO_RIGHTS', { + error: 'NoMunicipalityPermission', + }); + } + return { + ...query, + $raw: { + condition: `("location"::jsonb->'municipality'->'id')::int in (${ctx.meta.authUser.municipalities?.toString()})`, + }, + }; + } + // sesijoj imone + if (ctx.meta.profile && ctx.meta?.user) { + return { + ...query, + $raw: { + condition: `(tenant_id = ${ctx.meta.profile} OR stocking_customer_id = ${ctx.meta.profile})`, + }, + }; + } + + // sesijoj freelancer + if (!ctx.meta.profile && ctx.meta?.user) { + return { + ...query, + createdBy: ctx.meta.user.id, + tenant: { $exists: false }, + }; + } + } + return query; + }, ...COMMON_SCOPES, }, - defaultScopes: [...COMMON_DEFAULT_SCOPES], - defaultPopulates: ['batches', 'status'], + defaultScopes: [...COMMON_DEFAULT_SCOPES, 'profile'], + defaultPopulates: ['batches', 'status', 'mandatory'], }, hooks: { before: { @@ -442,7 +482,13 @@ export default class FishStockingsService extends moleculer.Service { area: 'number', name: 'string', cadastral_id: 'string', - municipality: 'string', + municipality: { + type: 'object', + properties: { + id: 'number', + name: 'string', + }, + }, }, }, location: 'object|optional', @@ -670,10 +716,16 @@ export default class FishStockingsService extends moleculer.Service { type: 'object', optional: true, properties: { - area: 'number', name: 'string', cadastral_id: 'string', - municipality: 'string', + municipality: { + type: 'object', + properties: { + id: 'number', + name: 'string', + }, + }, + area: 'number|optional', }, }, tenant: 'number|integer|optional|optional', @@ -702,7 +754,9 @@ export default class FishStockingsService extends moleculer.Service { // Assign tenant if necessary ctx.params.tenant = ctx.meta.profile; - const fishStocking: FishStocking = await this.createEntity(ctx); + console.log('REGISTRATION', ctx.params); + + const fishStocking: FishStocking = await this.createEntity(ctx, ctx.params); try { await ctx.call('fishBatches.createBatches', { @@ -761,7 +815,14 @@ export default class FishStockingsService extends moleculer.Service { properties: { cadastral_id: 'string', name: 'string', - municipality: 'object', + municipality: { + type: 'object', + properties: { + id: 'number', + name: 'number', + }, + }, + area: 'number|optional', }, }, batches: { @@ -788,7 +849,13 @@ export default class FishStockingsService extends moleculer.Service { area: 'number', name: 'string', cadastral_id: 'string', - municipality: 'string', + municipality: { + type: 'object', + properties: { + id: 'number', + name: 'string', + }, + }, }, }, tenant: 'number|optional', @@ -950,42 +1017,11 @@ export default class FishStockingsService extends moleculer.Service { rest: 'GET /recentLocations', auth: RestrictionType.USER, }) - async getRecentLocations(ctx: Context) { - const { profile, user } = ctx.meta; - const adapter = await this.getAdapter(ctx); - const knex = adapter.client; - let response; - if (profile) { - response = await knex.raw( - `select distinct on ("location"::jsonb->'cadastral_id') "location", "id" from "fish_stockings" where "tenant_id" = ${profile} limit 5`, - ); - } else if (user) { - response = await knex.raw( - `select distinct on ("location"::jsonb->'cadastral_id') "location", "id" from "fish_stockings" where "created_by" = ${user.id} limit 5`, - ); - } - const data = []; - for (const row of response.rows) { - const id = row.id; - const geom = await ctx.call('fishStockings.getGeometryJson', { - id, - }); - data.push({ - ...row.location, - geom, - }); - } - return data; + async getRecentLocations(ctx: Context) { + const recentLocations = await ctx.call('recentLocations.list'); + return recentLocations; } - @Action() - async getLocations() { - const adapter = await this.getAdapter(); - const knex = adapter.client; - return knex.raw( - `select distinct on ("location"::jsonb->'cadastral_id') "location" from "fish_stockings"`, - ); - } @Action() async getLocationsCount(ctx: Context) { const adapter = await this.getAdapter(ctx); @@ -1137,10 +1173,8 @@ export default class FishStockingsService extends moleculer.Service { @Method async beforeSelect(ctx: Context) { - const profilesQuery = await this.handleProfile(ctx.params.query || {}, ctx); let query = { ...ctx.params.query, - ...profilesQuery, }; let filters; @@ -1281,6 +1315,242 @@ export default class FishStockingsService extends moleculer.Service { return q; } + @Method + async seedDB() { + if (process.env.NODE_ENV === 'local') { + await this.broker.waitForServices([ + 'users', + 'tenants', + 'tenantUsers', + 'fishBatches', + 'mandatoryLocations', + ]); + + const user: User[] = await this.broker.call('users.find', { + query: { + email: 'vadovas@imone.lt', + }, + }); + + const reviewData = { + waybillNo: '1', + veterinaryApprovalNo: '1', + veterinaryApprovalOrderNo: '1', + containerWaterTemp: '17', + waterTemp: '16', + }; + + const data: any[] = [ + { + ...reviewData, + eventTime: '2021-06-06T17:09:40.164Z', + reviewTime: '2021-06-06T18:05:40.164Z', + createdBy: user?.[0]?.id, + assignedTo: user?.[0]?.id, + reviewedBy: user?.[0]?.id, + phone: '861111111', + batches: [ + { + amount: 100, + reviewAmount: 100, + fishType: 1, + fishAge: 2, + }, + { + amount: 150, + reviewAmount: 150, + fishType: 5, + fishAge: 4, + }, + ], + fishTypes: { + 1: 100, + 5: 150, + }, + geom: '0101000020120D0000000000004CA51E4100000080F3325741', + fishOrigin: 'GROWN', + fishOriginCompanyName: 'Test', + location: { + name: 'Nemunas', + cadastral_id: '10010001', + municipality: { + id: 52, + name: 'Kauno r. sav.', + }, + }, + comment: 'komentaras', + }, + { + ...reviewData, + eventTime: '2024-04-06T17:09:40.164Z', + reviewTime: '2021-04-07T12:05:40.164Z', + createdBy: user?.[0]?.id, + assignedTo: user?.[0]?.id, + reviewedBy: user?.[0]?.id, + phone: '861111111', + batches: [ + { + amount: 10, + reviewAmount: 10, + fishType: 2, + fishAge: 3, + }, + { + amount: 20, + reviewAmount: 20, + fishType: 4, + fishAge: 1, + }, + ], + fishTypes: { + 2: 10, + 4: 20, + }, + geom: '0101000020120D000000000000309E2141000000405B155741', + fishOrigin: 'GROWN', + fishOriginCompanyName: 'Test', + location: { + name: 'Baluošas', + cadastral_id: '10010002', + municipality: { + id: 7, + name: 'Švenčionių r. sav.', + }, + }, + comment: 'komentaras', + }, + { + ...reviewData, + eventTime: '2024-05-06T17:09:40.164Z', + reviewTime: '2024-04-07T12:05:40.164Z', + createdBy: user?.[0]?.id, + assignedTo: user?.[0]?.id, + reviewedBy: user?.[0]?.id, + phone: '861111111', + batches: [ + { + amount: 50, + fishType: 6, + fishAge: 1, + }, + { + amount: 50, + fishType: 3, + fishAge: 3, + }, + ], + fishTypes: { + 6: 50, + 3: 50, + }, + geom: '0101000020120D000000000000000B1F410000004092315741', + fishOrigin: 'GROWN', + fishOriginCompanyName: 'Test', + location: { + name: 'Dubrius', + cadastral_id: '10011443', + municipality: { + id: 52, + name: 'Kauno r. sav.', + }, + }, + comment: 'komentaras', + }, + { + ...reviewData, + eventTime: '2022-05-06T17:09:40.164Z', + reviewTime: '2022-05-07T12:05:40.164Z', + createdBy: user?.[0]?.id, + assignedTo: user?.[0]?.id, + reviewedBy: user?.[0]?.id, + phone: '861111111', + batches: [ + { + amount: 70, + reviewAmount: 70, + fishType: 9, + fishAge: 1, + }, + { + amount: 70, + reviewAmount: 70, + fishType: 1, + fishAge: 6, + }, + ], + fishTypes: { + 9: 70, + 1: 70, + }, + geom: '0101000020120D00000000000080351F41000000C0A0345741', + fishOrigin: 'GROWN', + fishOriginCompanyName: 'Test', + location: { + area: '5.89', + name: 'Paežeris', + cadastral_id: '10031211', + municipality: { + id: 49, + name: 'Kaišiadorių r. sav.', + }, + }, + comment: 'komentaras', + }, + { + ...reviewData, + eventTime: '2023-05-06T17:09:40.164Z', + reviewTime: '2023-05-07T12:05:40.164Z', + createdBy: user?.[0]?.id, + assignedTo: user?.[0]?.id, + reviewedBy: user?.[0]?.id, + phone: '861111111', + batches: [ + { + amount: 300, + reviewAmount: 300, + fishType: 1, + fishAge: 1, + }, + { + amount: 300, + reviewAmount: 300, + fishType: 10, + fishAge: 1, + }, + ], + fishTypes: { + 1: 300, + 10: 300, + }, + geom: '0101000020120D00008C58F93F86DF1B418E299256749F5741', + fishOrigin: 'GROWN', + fishOriginCompanyName: 'Test', + location: { + cadastral_id: '41040012', + name: 'Rėkyva', + municipality: { + id: 29, + name: 'Šiaulių m. sav.', + }, + area: '1196.84', + }, + comment: 'komentaras', + }, + ]; + + for (const item of data) { + const fishStocking = await this.createEntity(null, item, { permissive: true }); + if (fishStocking?.id) { + const batches = item.batches.map((batch: FishBatch) => ({ + ...batch, + fishStocking: fishStocking.id, + })); + await this.broker.call('fishBatches.createMany', batches); + } + } + } + } + @Event() async 'fishBatches.*'(ctx: Context>) { //Generates an object with amounts of fish stocked and stores in the database. diff --git a/services/locations.service.ts b/services/locations.service.ts index e80bfa6..bfde473 100644 --- a/services/locations.service.ts +++ b/services/locations.service.ts @@ -1,9 +1,9 @@ 'use strict'; -import { find, isEmpty, map } from 'lodash'; +import { find } from 'lodash'; import moleculer, { Context } from 'moleculer'; import { Action, Method, Service } from 'moleculer-decorators'; -import { GeomFeatureCollection, coordinatesToGeometry } from '../modules/geometry'; +import { GeomFeatureCollection } from '../modules/geometry'; import { CommonFields, CommonPopulates, RestrictionType, Table } from '../types'; import { UserAuthMeta } from './api.service'; const getBox = (geom: GeomFeatureCollection, tolerance: number = 0.001) => { @@ -22,7 +22,10 @@ const getBox = (geom: GeomFeatureCollection, tolerance: number = 0.001) => { interface Fields extends CommonFields { cadastral_id: string; name: string; - municipality: string; + municipality: { + id: number; + name: string; + }; } interface Populates extends CommonPopulates {} @@ -37,71 +40,80 @@ export type Location< }) export default class LocationsService extends moleculer.Service { @Action({ - rest: 'GET /', + rest: 'GET /uetk', auth: RestrictionType.PUBLIC, params: { - geom: 'string|optional', - search: 'any|optional', - withGeom: 'any|optional', + search: { + type: 'string', + optional: true, + }, }, cache: false, }) - async search( + async uetkSearch( ctx: Context< { search?: string; - geom?: string; - withGeom?: any; + query: any; }, UserAuthMeta >, ) { - const { geom, search, withGeom, ...rest } = ctx.params; - if (geom) { - const geomJson: GeomFeatureCollection = JSON.parse(geom); - const riverOrLake = await this.getRiverOrLakeFromPoint(geomJson); - return riverOrLake; - } else if (search) { - const url = - `${process.env.INTERNAL_API}/uetk/search?` + new URLSearchParams({ search, ...rest }); + const targetUrl = `${process.env.UETK_URL}/objects/search`; + const params: any = ctx.params; + const searchParams = new URLSearchParams(params); + const query = { + category: { + $in: [ + 'RIVER', + 'CANAL', + 'INTERMEDIATE_WATER_BODY', + 'TERRITORIAL_WATER_BODY', + 'NATURAL_LAKE', + 'PONDED_LAKE', + 'POND', + 'ISOLATED_WATER_BODY', + ], + }, + ...params.query, + }; + searchParams.set('query', JSON.stringify(query)); + const queryString = searchParams.toString(); + const url = `${targetUrl}?${queryString}`; + try { const response = await fetch(url); - const data = await response.json(); - return map(data.rows, (item) => { - const location: any = { - cadastral_id: item.properties.cadastral_id, - name: item.properties.name, - municipality: item.properties.municipality, - area: item.properties.area, - }; - if (withGeom === 'true') { - location['geom'] = coordinatesToGeometry({ - x: item.properties.lon, - y: item.properties.lat, - }); - } - return location; - }); + const municipalities = await this.actions.getMunicipalities(null, { parentCtx: ctx }); + const rows = data?.rows?.map((item: any) => ({ + name: item.name, + cadastral_id: item.cadastralId, + municipality: municipalities?.rows?.find((m: any) => m.name === item.municipality), + area: item.area, + })); + + return { + ...data, + rows, + }; + } catch (error) { + throw new Error(`Failed to fetch: ${error.message}`); } } - @Action() - async getLocationsByCadastralIds(ctx: Context<{ locations: string[] }>) { - const promises = map(ctx.params.locations, (location) => - ctx.call('locations.search', { search: location }), - ); - - const result: any = await Promise.all(promises); - - const data: Location[] = []; - for (const item of result) { - if (!isEmpty(item)) { - data.push(item[0]); - } - } - return data; + @Action({ + rest: 'GET /municipalities/search', + params: { + geom: 'string|optional', + }, + cache: { + ttl: 24 * 60 * 60, + }, + }) + async searchMunicipalities(ctx: Context<{ geom?: string }>) { + const geom: GeomFeatureCollection = JSON.parse(ctx.params.geom); + return this.getMunicipalityFromPoint(geom); } @Action({ @@ -153,46 +165,16 @@ export default class LocationsService extends moleculer.Service { return find(municipalities?.rows, { id: ctx.params.id }); } - @Method - async getRiverOrLakeFromPoint(geom: GeomFeatureCollection) { - if (geom?.features?.length) { - try { - const box = getBox(geom, 200); - const rivers = `${process.env.GEO_SERVER}/qgisserver/uetk_zuvinimas?SERVICE=WFS&REQUEST=GetFeature&TYPENAME=rivers&OUTPUTFORMAT=application/json&GEOMETRYNAME=centroid&BBOX=${box}`; - const riversData = await fetch(rivers, { - headers: { - 'Content-Type': 'application/json', - }, - }); - const riversResult = await riversData.json(); - const municipality = await this.getMunicipalityFromPoint(geom); - const lakes = `${process.env.GEO_SERVER}/qgisserver/uetk_zuvinimas?SERVICE=WFS&REQUEST=GetFeature&TYPENAME=lakes_ponds&OUTPUTFORMAT=application/json&GEOMETRYNAME=centroid&BBOX=${box}`; - const lakesData = await fetch(lakes, { - headers: { - 'Content-Type': 'application/json', - }, - }); - const lakesResult = await lakesData.json(); - const list = [...riversResult.features, ...lakesResult.features]; - - const mappedList = map(list, (item) => { - return { - cadastral_id: item.properties.kadastro_id, - name: item.properties.pavadinimas, - municipality: municipality, - area: item.properties.st_area - ? (item.properties.st_area / 10000).toFixed(2) - : undefined, - }; - }); - - return mappedList; - } catch (err) { - throw new moleculer.Errors.ValidationError(err.message); - } - } else { - throw new moleculer.Errors.ValidationError('Invalid geometry'); - } + @Action({ + params: { + name: 'string', + }, + }) + async searchMunicipality(ctx: Context<{ name: string }>) { + const municipalities = await this.actions.getMunicipalities(null, { + parentCtx: ctx, + }); + return find(municipalities?.rows, { name: ctx.params.name }); } @Method diff --git a/services/recentLocations.service.ts b/services/recentLocations.service.ts new file mode 100644 index 0000000..5776166 --- /dev/null +++ b/services/recentLocations.service.ts @@ -0,0 +1,118 @@ +'use strict'; + +import moleculer, { Context } from 'moleculer'; +import { Method, Service } from 'moleculer-decorators'; +import DbConnection from '../mixins/database.mixin'; +import { RestrictionType } from '../types'; +import { UserAuthMeta } from './api.service'; +import { FishStocking } from './fishStockings.service'; + +const mapItem = (data: RecentLocation) => { + const { cadastralId, ...rest } = data; + return { ...rest, cadastral_id: cadastralId }; +}; + +export interface RecentLocation { + name: string; + cadastralId: string; + municipality: { + id: number; + name: string; + }; + geom: any; +} + +@Service({ + name: 'recentLocations', + mixins: [ + DbConnection({ + createActions: { + create: false, + update: false, + remove: false, + createMany: false, + }, + }), + ], + settings: { + auth: RestrictionType.USER, + fields: { + name: 'string', + cadastralId: 'string', + municipality: { + type: 'object', + properties: { + id: 'number|integer|positive', + name: 'string', + }, + }, + geom: { + type: 'any', + populate: async (ctx: Context, _values: any, entities: RecentLocation[]) => { + const fishStockingIds = entities.map((entity) => entity.geom); + const fishStockings: FishStocking[] = await ctx.call('fishStockings.find', { + query: { + id: { $in: fishStockingIds }, + }, + scope: false, + populate: ['geom'], + }); + return entities.map((entity) => fishStockings.find((f) => f.id === entity.geom)?.geom); + }, + }, + tenant: { + type: 'number', + columnType: 'integer', + columnName: 'tenantId', + required: true, + immutable: true, + hidden: 'byDefault', + }, + user: { + type: 'number', + columnType: 'integer', + columnName: 'userId', + required: true, + immutable: true, + hidden: 'byDefault', + }, + }, + scopes: { + profile(query: any, ctx: Context, params: any) { + const { user, profile } = ctx.meta; + if (!user?.id) return query; + query.user = user.id; + query.tenant = profile ? profile : { $exists: false }; + return query; + }, + }, + defaultScopes: ['profile'], + defaultPopulates: ['geom'], + }, + hooks: { + after: { + list: 'afterSelect', + find: 'afterSelect', + get: 'afterSelect', + }, + }, +}) +export default class RecentLocationsService extends moleculer.Service { + @Method + async afterSelect(ctx: any, data: any) { + if (Array.isArray(data)) { + return data.map((item: RecentLocation) => { + return mapItem(item); + }); + } else if (data?.rows) { + return { + ...data, + rows: data.rows.map((item: RecentLocation) => { + return mapItem(item); + }), + }; + } else if (data?.cadastralId) { + return mapItem(data); + } + } +} From 8367f178ba62f7dc4841b26eaf7541e1b372943c Mon Sep 17 00:00:00 2001 From: Dovile Date: Wed, 29 May 2024 09:40:35 +0300 Subject: [PATCH 02/16] municipality type fix --- services/fishStockings.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index a6b1333..c10c8d8 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -819,7 +819,7 @@ export default class FishStockingsService extends moleculer.Service { type: 'object', properties: { id: 'number', - name: 'number', + name: 'string', }, }, area: 'number|optional', From c2faa50c5469104299ce06852787419f727eab48 Mon Sep 17 00:00:00 2001 From: Dovile Date: Wed, 29 May 2024 12:02:56 +0300 Subject: [PATCH 03/16] municipality quick fix --- services/locations.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/locations.service.ts b/services/locations.service.ts index bfde473..e03f928 100644 --- a/services/locations.service.ts +++ b/services/locations.service.ts @@ -189,7 +189,7 @@ export default class LocationsService extends moleculer.Service { const { features } = await data.json(); return { id: Number(features[0]?.properties?.kodas), - name: features[0]?.properties?.pavadinimas, + name: features[0]?.properties?.pavadinimas || '', }; } } From f00a76ef40113e3b248112dba6d07add0fe33512 Mon Sep 17 00:00:00 2001 From: Dovile Date: Wed, 29 May 2024 13:36:15 +0300 Subject: [PATCH 04/16] byAge --- services/public.service.ts | 44 +++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/services/public.service.ts b/services/public.service.ts index 39f9887..c7653a3 100644 --- a/services/public.service.ts +++ b/services/public.service.ts @@ -27,14 +27,24 @@ const uetkStatisticsParams = { year: 'number|convert|optional', cadastralId: 'string|optional', }; +type StatsByFishAge = { + [id: string]: { + count: number; + fishAge: { + id: number; + label: string; + }; + }; +}; -type StatsById = { +type StatsByFish = { [id: string]: { count: number; fishType: { id: number; label: string; }; + byAge: StatsByFishAge; }; }; @@ -44,7 +54,7 @@ type StatsByYear = { type StatsByCadastralId = { count: number; - byFish?: StatsById; + byFish?: StatsByFish; byYear?: StatsByYear; }; @@ -58,15 +68,39 @@ type BatchesById = { [id: string]: Batches; }; +const getByAge = (value: CompletedFishBatch) => { + const age = value.fish_age; + return { + count: value.count, + fishAge: { + id: age.id, + label: age.label, + }, + }; +}; + const getByFish = (batches: Batches) => { return batches?.reduce( - (aggregate, value) => { + (aggregate: StatsByCadastralId, value: CompletedFishBatch) => { aggregate.count += value.count; let fishTypeData = aggregate.byFish[value.fish_type.id]; + const age = value.fish_age; if (fishTypeData) { fishTypeData.count += value.count; + const fishTypeDataAge = fishTypeData.byAge[age.id]; + if (fishTypeDataAge) { + fishTypeData.byAge[age.id].count += value.count; + } else { + fishTypeData.byAge[age.id] = getByAge(value); + } } else { - fishTypeData = { count: value.count, fishType: value.fish_type }; + fishTypeData = { + count: value.count, + fishType: value.fish_type, + byAge: { + [age.id]: getByAge(value), + }, + }; } aggregate.byFish[value.fish_type.id] = fishTypeData; return aggregate; @@ -99,7 +133,7 @@ const getCount = (batches: Batches) => { @Service({ name: 'public', }) -export default class FishAgesService extends moleculer.Service { +export default class PublicService extends moleculer.Service { @Action({ rest: 'GET /fishStockings', auth: RestrictionType.PUBLIC, From e2c6f76828266df1153973bae06d9b9c0d352bfa Mon Sep 17 00:00:00 2001 From: Dovile Date: Wed, 29 May 2024 13:38:31 +0300 Subject: [PATCH 05/16] removed console log --- services/fishStockings.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index c10c8d8..abd948f 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -754,8 +754,6 @@ export default class FishStockingsService extends moleculer.Service { // Assign tenant if necessary ctx.params.tenant = ctx.meta.profile; - console.log('REGISTRATION', ctx.params); - const fishStocking: FishStocking = await this.createEntity(ctx, ctx.params); try { From 0459c14558a592cf263c39ccf24da8472e0c96ba Mon Sep 17 00:00:00 2001 From: Dovile Date: Mon, 10 Jun 2024 22:09:01 +0300 Subject: [PATCH 06/16] Keep old location search action, in case it is somewhere used. --- services/fishStockings.service.ts | 11 ++++ services/locations.service.ts | 96 ++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index abd948f..f37460c 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -268,6 +268,17 @@ export type FishStocking< reviewLocation: { type: 'any', raw: true, + populate(ctx: any, _values: any, fishStockings: FishStocking[]) { + return Promise.all( + fishStockings.map((fishStocking) => { + return ctx.call('fishStockings.getGeometryJson', { + field: 'reviewLocation', + asField: 'reviewLocation', + id: fishStocking.id, + }); + }), + ); + }, }, reviewTime: 'date', waybillNo: 'string', diff --git a/services/locations.service.ts b/services/locations.service.ts index e03f928..92abb0a 100644 --- a/services/locations.service.ts +++ b/services/locations.service.ts @@ -1,9 +1,9 @@ 'use strict'; -import { find } from 'lodash'; +import { find, map } from 'lodash'; import moleculer, { Context } from 'moleculer'; import { Action, Method, Service } from 'moleculer-decorators'; -import { GeomFeatureCollection } from '../modules/geometry'; +import { GeomFeatureCollection, coordinatesToGeometry } from '../modules/geometry'; import { CommonFields, CommonPopulates, RestrictionType, Table } from '../types'; import { UserAuthMeta } from './api.service'; const getBox = (geom: GeomFeatureCollection, tolerance: number = 0.001) => { @@ -102,6 +102,98 @@ export default class LocationsService extends moleculer.Service { } } + @Action({ + rest: 'GET /', + auth: RestrictionType.PUBLIC, + params: { + geom: 'string|optional', + search: 'any|optional', + withGeom: 'any|optional', + }, + cache: false, + }) + async search( + ctx: Context< + { + search?: string; + geom?: string; + withGeom?: any; + }, + UserAuthMeta + >, + ) { + const { geom, search, withGeom, ...rest } = ctx.params; + if (geom) { + const geomJson: GeomFeatureCollection = JSON.parse(geom); + const riverOrLake = await this.getRiverOrLakeFromPoint(geomJson); + return riverOrLake; + } else if (search) { + const url = + `${process.env.INTERNAL_API}/uetk/search?` + new URLSearchParams({ search, ...rest }); + const response = await fetch(url); + + const data = await response.json(); + + return map(data.rows, (item) => { + const location: any = { + cadastral_id: item.properties.cadastral_id, + name: item.properties.name, + municipality: item.properties.municipality, + area: item.properties.area, + }; + if (withGeom === 'true') { + location['geom'] = coordinatesToGeometry({ + x: item.properties.lon, + y: item.properties.lat, + }); + } + return location; + }); + } + } + + @Method + async getRiverOrLakeFromPoint(geom: GeomFeatureCollection) { + if (geom?.features?.length) { + try { + const box = getBox(geom, 200); + const rivers = `${process.env.GEO_SERVER}/qgisserver/uetk_zuvinimas?SERVICE=WFS&REQUEST=GetFeature&TYPENAME=rivers&OUTPUTFORMAT=application/json&GEOMETRYNAME=centroid&BBOX=${box}`; + const riversData = await fetch(rivers, { + headers: { + 'Content-Type': 'application/json', + }, + }); + const riversResult = await riversData.json(); + const municipality = await this.getMunicipalityFromPoint(geom); + const lakes = `${process.env.GEO_SERVER}/qgisserver/uetk_zuvinimas?SERVICE=WFS&REQUEST=GetFeature&TYPENAME=lakes_ponds&OUTPUTFORMAT=application/json&GEOMETRYNAME=centroid&BBOX=${box}`; + const lakesData = await fetch(lakes, { + headers: { + 'Content-Type': 'application/json', + }, + }); + const lakesResult = await lakesData.json(); + const list = [...riversResult.features, ...lakesResult.features]; + + const mappedList = map(list, (item) => { + return { + cadastral_id: item.properties.kadastro_id, + name: item.properties.pavadinimas, + municipality: municipality, + area: item.properties.st_area + ? (item.properties.st_area / 10000).toFixed(2) + : undefined, + }; + }); + + return mappedList; + } catch (err) { + throw new moleculer.Errors.ValidationError(err.message); + } + } else { + throw new moleculer.Errors.ValidationError('Invalid geometry'); + } + } + @Action({ rest: 'GET /municipalities/search', params: { From be141551d70717ddf2e619d603506f57fdcfac62 Mon Sep 17 00:00:00 2001 From: Dovile Date: Tue, 11 Jun 2024 09:47:29 +0300 Subject: [PATCH 07/16] review location fix --- services/fishStockings.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index f37460c..13af076 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -1155,7 +1155,7 @@ export default class FishStockingsService extends moleculer.Service { async parseReviewLocationField( ctx: Context<{ id?: number; - reviewLocation?: any; + reviewLocation?: { lat: number; lng: number }; }>, ) { const { reviewLocation } = ctx.params; @@ -1165,7 +1165,10 @@ export default class FishStockingsService extends moleculer.Service { return ctx; } - const reviewLocationGeom: any = coordinatesToGeometry(reviewLocation); + const reviewLocationGeom: any = coordinatesToGeometry({ + x: reviewLocation.lng, + y: reviewLocation.lat, + }); if (reviewLocationGeom?.features?.length) { const adapter = await this.getAdapter(ctx); const table = adapter.getTable(); From 6c19bef951998e641e43c26a819772fc8e69f822 Mon Sep 17 00:00:00 2001 From: Dovile Date: Tue, 18 Jun 2024 12:05:13 +0300 Subject: [PATCH 08/16] fixes --- .../20240522080439_recentLocationsView.js | 2 +- services/fishStockings.service.ts | 18 +++++------ services/locations.service.ts | 6 ++-- services/recentLocations.service.ts | 32 ++++++++++++------- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/database/migrations/20240522080439_recentLocationsView.js b/database/migrations/20240522080439_recentLocationsView.js index 385d435..cea08d2 100644 --- a/database/migrations/20240522080439_recentLocationsView.js +++ b/database/migrations/20240522080439_recentLocationsView.js @@ -12,7 +12,7 @@ exports.up = function (knex) { location->>'municipality' AS municipality, "tenant_id", "created_by" AS user_id, - "id" AS "geom", + "id" AS "fishStockingId", "event_time" FROM fish_stockings ORDER BY location->>'cadastral_id', "user_id", "tenant_id", "event_time" DESC; diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index a213497..b366c21 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -223,6 +223,7 @@ export type FishStocking< virtual: true, default: () => [], async populate(ctx: Context, _values: any, fishStockings: FishStocking[]) { + if (!ctx) return; const fishBatches: FishBatch[] = await ctx.call('fishBatches.find', { query: { fishStocking: { @@ -364,6 +365,7 @@ export type FishStocking< virtual: true, default: () => [], async populate(ctx: Context, _values: any, fishStockings: FishStocking[]) { + if (!ctx) return; const fishBatches: FishBatch[] = await ctx.call('fishBatches.find', { query: { fishStocking: { @@ -389,6 +391,7 @@ export type FishStocking< //TODO: mandatory flag could be part of location object virtual: true, get: async ({ entity, ctx }: FieldHookCallback) => { + if (!ctx) return; const area = entity.location.area; if (area && area > 50) { return true; @@ -423,18 +426,14 @@ export type FishStocking< } return { ...query, - $raw: { - condition: `("location"::jsonb->'municipality'->'id')::int in (${ctx.meta.authUser.municipalities?.toString()})`, - }, + $raw: `("location"::jsonb->'municipality'->'id')::int in (${ctx.meta.authUser.municipalities?.toString()})`, }; } // sesijoj imone if (ctx.meta.profile && ctx.meta?.user) { return { ...query, - $raw: { - condition: `(tenant_id = ${ctx.meta.profile} OR stocking_customer_id = ${ctx.meta.profile})`, - }, + $raw: `(tenant_id = ${ctx.meta.profile} OR stocking_customer_id = ${ctx.meta.profile})`, }; } @@ -452,7 +451,7 @@ export type FishStocking< ...COMMON_SCOPES, }, defaultScopes: [...COMMON_DEFAULT_SCOPES, 'profile'], - defaultPopulates: ['batches', 'status', 'mandatory'], + defaultPopulates: ['batches', 'status'], }, hooks: { before: { @@ -1029,8 +1028,7 @@ export default class FishStockingsService extends moleculer.Service { auth: RestrictionType.USER, }) async getRecentLocations(ctx: Context) { - const recentLocations = await ctx.call('recentLocations.list'); - return recentLocations; + return await ctx.call('recentLocations.list'); } @Action() @@ -1553,7 +1551,7 @@ export default class FishStockingsService extends moleculer.Service { ]; for (const item of data) { - const fishStocking = await this.createEntity(null, item, { permissive: true }); + const fishStocking = await this.createEntity(null, item); if (fishStocking?.id) { const batches = item.batches.map((batch: FishBatch) => ({ ...batch, diff --git a/services/locations.service.ts b/services/locations.service.ts index 92abb0a..797d28c 100644 --- a/services/locations.service.ts +++ b/services/locations.service.ts @@ -75,16 +75,14 @@ export default class LocationsService extends moleculer.Service { 'ISOLATED_WATER_BODY', ], }, - ...params.query, + ...(params.query || {}), }; searchParams.set('query', JSON.stringify(query)); const queryString = searchParams.toString(); const url = `${targetUrl}?${queryString}`; try { - const response = await fetch(url); - const data = await response.json(); - + const data = await fetch(url).then((r) => r.json()); const municipalities = await this.actions.getMunicipalities(null, { parentCtx: ctx }); const rows = data?.rows?.map((item: any) => ({ name: item.name, diff --git a/services/recentLocations.service.ts b/services/recentLocations.service.ts index 5776166..ff2aa3f 100644 --- a/services/recentLocations.service.ts +++ b/services/recentLocations.service.ts @@ -5,7 +5,6 @@ import { Method, Service } from 'moleculer-decorators'; import DbConnection from '../mixins/database.mixin'; import { RestrictionType } from '../types'; import { UserAuthMeta } from './api.service'; -import { FishStocking } from './fishStockings.service'; const mapItem = (data: RecentLocation) => { const { cadastralId, ...rest } = data; @@ -19,6 +18,7 @@ export interface RecentLocation { id: number; name: string; }; + fishStockingId: number; geom: any; } @@ -46,18 +46,28 @@ export interface RecentLocation { name: 'string', }, }, + fishStockingId: { + type: 'number', + columnType: 'integer', + columnName: 'fishStockingId', + required: true, + immutable: true, + hidden: 'byDefault', + }, geom: { type: 'any', - populate: async (ctx: Context, _values: any, entities: RecentLocation[]) => { - const fishStockingIds = entities.map((entity) => entity.geom); - const fishStockings: FishStocking[] = await ctx.call('fishStockings.find', { - query: { - id: { $in: fishStockingIds }, - }, - scope: false, - populate: ['geom'], - }); - return entities.map((entity) => fishStockings.find((f) => f.id === entity.geom)?.geom); + raw: true, + virtual: true, + async populate(ctx: any, _values: any, recentLocations: RecentLocation[]) { + return Promise.all( + recentLocations.map(async (recentLocation) => { + return ctx.call('fishStockings.getGeometryJson', { + field: 'geom', + asField: 'geom', + id: recentLocation.fishStockingId, + }); + }), + ); }, }, tenant: { From d77072029f441215c2292c7806d90fda0fbcd0ee Mon Sep 17 00:00:00 2001 From: Dovile Date: Wed, 25 Sep 2024 11:12:01 +0300 Subject: [PATCH 09/16] new fields in location object --- services/fishStockings.service.ts | 13 ++++++++++++- services/recentLocations.service.ts | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index b366c21..9df1acc 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -74,6 +74,8 @@ interface Fields extends CommonFields { location: { name: string; area: number; + length: number; + category: string; cadastral_id: string; municipality: { id: number; @@ -190,6 +192,9 @@ export type FishStocking< cadastral_id: 'string', name: 'string', municipality: 'object', + area: 'number|optional', + length: 'number|optional', + category: 'string', }, }, geom: { @@ -705,6 +710,9 @@ export default class FishStockingsService extends moleculer.Service { cadastral_id: 'string', name: 'string', municipality: 'object', + area: 'number|optional|convert', + length: 'number|optional|convert', + category: 'string', }, }, geom: 'any', @@ -745,6 +753,7 @@ export default class FishStockingsService extends moleculer.Service { }, }) async register(ctx: Context) { + console.log('register location', ctx.params.location); // Validate eventTime const timeBeforeReview = await isTimeBeforeReview(ctx, new Date(ctx.params.eventTime)); if (!timeBeforeReview) { @@ -832,7 +841,9 @@ export default class FishStockingsService extends moleculer.Service { name: 'string', }, }, - area: 'number|optional', + area: 'number|optional|convert', + length: 'number|optional|convert', + category: 'string', }, }, batches: { diff --git a/services/recentLocations.service.ts b/services/recentLocations.service.ts index ff2aa3f..053280e 100644 --- a/services/recentLocations.service.ts +++ b/services/recentLocations.service.ts @@ -46,6 +46,9 @@ export interface RecentLocation { name: 'string', }, }, + area: 'number', + length: 'number', + category: 'string', fishStockingId: { type: 'number', columnType: 'integer', From bc33eda39ef73cbb3b85e7b9cffd7d0f87d03efc Mon Sep 17 00:00:00 2001 From: Dovile Date: Wed, 25 Sep 2024 12:20:15 +0300 Subject: [PATCH 10/16] category field added to export --- services/fishStockings.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index 9df1acc..fa29bbc 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -1081,6 +1081,7 @@ export default class FishStockingsService extends moleculer.Service { : fishStocking?.fishOriginReservoir; const date = fishStocking?.eventTime || '-'; const municipality = fishStocking.location.municipality?.name || '-'; + const category = fishStocking.location.category || '-'; const waterBodyName = fishStocking.location?.name || '-'; const waterBodyCode = fishStocking.location.cadastral_id || '-'; const waybillNo = fishStocking.waybillNo || '-'; @@ -1094,6 +1095,7 @@ export default class FishStockingsService extends moleculer.Service { Rajonas: municipality, 'Vandens telkinio pavadinimas': waterBodyName, 'Telkinio kodas': waterBodyCode, + 'Telkinio kategorija': category, 'Žuvų, vėžių rūšis': batch.fishType?.label, Amžius: batch.fishAge?.label, 'Planuojamas kiekis, vnt': batch.amount || 0, From c24a95e5dc194f1f782eaa32323bfde420204d85 Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 26 Sep 2024 10:18:15 +0300 Subject: [PATCH 11/16] search fixes --- ...075036_recentLocationsView_add_category.js | 44 +++++++++++++++++++ services/locations.service.ts | 28 +++++++----- 2 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 database/migrations/20240925075036_recentLocationsView_add_category.js diff --git a/database/migrations/20240925075036_recentLocationsView_add_category.js b/database/migrations/20240925075036_recentLocationsView_add_category.js new file mode 100644 index 0000000..1264bb1 --- /dev/null +++ b/database/migrations/20240925075036_recentLocationsView_add_category.js @@ -0,0 +1,44 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.raw(` + DROP VIEW IF EXISTS recent_locations; + CREATE VIEW recent_locations AS + SELECT DISTINCT ON (location->>'cadastral_id', "created_by", "tenant_id") + location->>'name' AS name, + location->>'cadastral_id' AS cadastral_id, + location->>'municipality' AS municipality, + location->>'area' AS area, + location->>'length' AS length, + location->>'category' AS category, + "tenant_id", + "created_by" AS user_id, + "id" AS "fishStockingId", + "event_time" + FROM fish_stockings + ORDER BY location->>'cadastral_id', "user_id", "tenant_id", "event_time" DESC; + `); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.raw(` + DROP VIEW IF EXISTS recent_locations; + CREATE VIEW recent_locations AS + SELECT DISTINCT ON (location->>'cadastral_id', "created_by", "tenant_id") + location->>'name' AS name, + location->>'cadastral_id' AS cadastral_id, + location->>'municipality' AS municipality, + "tenant_id", + "created_by" AS user_id, + "id" AS "fishStockingId", + "event_time" + FROM fish_stockings + ORDER BY location->>'cadastral_id', "user_id", "tenant_id", "event_time" DESC; + `); +}; diff --git a/services/locations.service.ts b/services/locations.service.ts index 797d28c..440fb1e 100644 --- a/services/locations.service.ts +++ b/services/locations.service.ts @@ -83,14 +83,7 @@ export default class LocationsService extends moleculer.Service { const url = `${targetUrl}?${queryString}`; try { const data = await fetch(url).then((r) => r.json()); - const municipalities = await this.actions.getMunicipalities(null, { parentCtx: ctx }); - const rows = data?.rows?.map((item: any) => ({ - name: item.name, - cadastral_id: item.cadastralId, - municipality: municipalities?.rows?.find((m: any) => m.name === item.municipality), - area: item.area, - })); - + const rows = await Promise.all(data?.rows?.map((item: any) => this.mapUETKObject(ctx, item))); return { ...data, rows, @@ -126,6 +119,7 @@ export default class LocationsService extends moleculer.Service { const riverOrLake = await this.getRiverOrLakeFromPoint(geomJson); return riverOrLake; } else if (search) { + //TODO: after releaseing fronten this part can be deleted const url = `${process.env.INTERNAL_API}/uetk/search?` + new URLSearchParams({ search, ...rest }); const response = await fetch(url); @@ -180,9 +174,11 @@ export default class LocationsService extends moleculer.Service { area: item.properties.st_area ? (item.properties.st_area / 10000).toFixed(2) : undefined, + length: item.properties.ilgis_uetk, + category: item.properties.kategorija, + //TODO: category is a number, needs to be converted to string in lithuanian language }; }); - return mappedList; } catch (err) { throw new moleculer.Errors.ValidationError(err.message); @@ -251,7 +247,6 @@ export default class LocationsService extends moleculer.Service { const municipalities = await this.actions.getMunicipalities(null, { parentCtx: ctx, }); - return find(municipalities?.rows, { id: ctx.params.id }); } @@ -267,6 +262,19 @@ export default class LocationsService extends moleculer.Service { return find(municipalities?.rows, { name: ctx.params.name }); } + @Method + async mapUETKObject(ctx: Context, item: any) { + const municipalities = await this.actions.getMunicipalities(null, { parentCtx: ctx }); + return { + name: item.name, + cadastral_id: item.cadastralId, + municipality: municipalities?.rows?.find((m: any) => m.name === item.municipality), + area: item.area, + length: item.length, + category: item.category, + }; + } + @Method async getMunicipalityFromPoint(geom: GeomFeatureCollection) { const box = getBox(geom); From d782a0368187a4cc74900d52c5abdbfc9ff491cc Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 26 Sep 2024 11:29:01 +0300 Subject: [PATCH 12/16] same result for serch by geom and uetk --- services/locations.service.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/services/locations.service.ts b/services/locations.service.ts index 440fb1e..a72c839 100644 --- a/services/locations.service.ts +++ b/services/locations.service.ts @@ -6,6 +6,17 @@ import { Action, Method, Service } from 'moleculer-decorators'; import { GeomFeatureCollection, coordinatesToGeometry } from '../modules/geometry'; import { CommonFields, CommonPopulates, RestrictionType, Table } from '../types'; import { UserAuthMeta } from './api.service'; + +const CategoryTranslates: any = { + 1: 'Upė', + 2: 'Kanalas', + 3: 'Natūralus ežeras', + 4: 'Patvenktas ežeras', + 5: 'Tvenkinys', + 6: 'Nepratekamas dirbtinis paviršinis vandens telkinys', + 7: 'Tarpinis vandens telkinys', +}; + const getBox = (geom: GeomFeatureCollection, tolerance: number = 0.001) => { const coordinates: any = geom.features[0].geometry.coordinates; const topLeft = { @@ -172,11 +183,10 @@ export default class LocationsService extends moleculer.Service { name: item.properties.pavadinimas, municipality: municipality, area: item.properties.st_area - ? (item.properties.st_area / 10000).toFixed(2) - : undefined, + ? Math.round(item.properties.st_area / 100) / 100 + : undefined, //ha length: item.properties.ilgis_uetk, - category: item.properties.kategorija, - //TODO: category is a number, needs to be converted to string in lithuanian language + category: CategoryTranslates[item.properties.kategorija], }; }); return mappedList; @@ -269,9 +279,9 @@ export default class LocationsService extends moleculer.Service { name: item.name, cadastral_id: item.cadastralId, municipality: municipalities?.rows?.find((m: any) => m.name === item.municipality), - area: item.area, + area: item.area ? Math.round(item.area / 100) / 100 : undefined, //ha length: item.length, - category: item.category, + category: item.categoryTranslate, }; } From 9545686415bbdff4fd2f723d2cd9ca4968bb788b Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 26 Sep 2024 12:02:43 +0300 Subject: [PATCH 13/16] search fixes --- services/fishStockings.service.ts | 8 ++++++ services/locations.service.ts | 42 ++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index fa29bbc..f20ec0c 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -1626,4 +1626,12 @@ export default class FishStockingsService extends moleculer.Service { ); } } + + //TODO: delete after release + async started() { + // const fishStockings = await this.getEntities(null); + // for (const fishStocking of fishStockings) { + // const cadastralId = fishStockings.location.cadastral_id; + // } + } } diff --git a/services/locations.service.ts b/services/locations.service.ts index a72c839..eccf638 100644 --- a/services/locations.service.ts +++ b/services/locations.service.ts @@ -104,6 +104,42 @@ export default class LocationsService extends moleculer.Service { } } + @Action({ + rest: 'GET /uetk/:cadastralId', + auth: RestrictionType.PUBLIC, + params: { + cadastralId: { + type: 'string', + optional: true, + }, + }, + }) + async uetkSearchByCadastralId( + ctx: Context< + { + cadastralId: string; + }, + UserAuthMeta + >, + ) { + const targetUrl = `${process.env.UETK_URL}/objects/search`; + const params: any = ctx.params; + const searchParams = new URLSearchParams(params); + const query = { + cadastralId: ctx.params.cadastralId, + }; + searchParams.set('query', JSON.stringify(query)); + const queryString = searchParams.toString(); + + const url = `${targetUrl}?${queryString}`; + try { + const data = await fetch(url).then((r) => r.json()); + return this.mapUETKObject(ctx, data?.rows?.[0]); + } catch (error) { + throw new Error(`Failed to fetch: ${error.message}`); + } + } + @Action({ rest: 'GET /', auth: RestrictionType.PUBLIC, @@ -130,7 +166,7 @@ export default class LocationsService extends moleculer.Service { const riverOrLake = await this.getRiverOrLakeFromPoint(geomJson); return riverOrLake; } else if (search) { - //TODO: after releaseing fronten this part can be deleted + //TODO: after releaseing frontend this part can be deleted const url = `${process.env.INTERNAL_API}/uetk/search?` + new URLSearchParams({ search, ...rest }); const response = await fetch(url); @@ -185,7 +221,7 @@ export default class LocationsService extends moleculer.Service { area: item.properties.st_area ? Math.round(item.properties.st_area / 100) / 100 : undefined, //ha - length: item.properties.ilgis_uetk, + length: item.properties.ilgis_uetk, //km category: CategoryTranslates[item.properties.kategorija], }; }); @@ -280,7 +316,7 @@ export default class LocationsService extends moleculer.Service { cadastral_id: item.cadastralId, municipality: municipalities?.rows?.find((m: any) => m.name === item.municipality), area: item.area ? Math.round(item.area / 100) / 100 : undefined, //ha - length: item.length, + length: item.length ? Math.round(item.length / 10) / 100 : undefined, //km category: item.categoryTranslate, }; } From 67dc994f9e1fa2a7b99d37cb53ac689a2590a331 Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 26 Sep 2024 13:52:28 +0300 Subject: [PATCH 14/16] fix locations in database --- services/fishStockings.service.ts | 284 ++++-------------------------- services/locations.service.ts | 17 +- 2 files changed, 38 insertions(+), 263 deletions(-) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index f20ec0c..d50d2ff 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -36,6 +36,7 @@ import { AuthUserRole, UserAuthMeta } from './api.service'; import { FishBatch } from './fishBatches.service'; import { FishStockingPhoto } from './fishStockingPhotos.service'; import { FishType } from './fishTypes.service'; +import { Location } from './locations.service'; import { Setting } from './settings.service'; import { Tenant } from './tenants.service'; import { User } from './users.service'; @@ -71,17 +72,7 @@ interface Fields extends CommonFields { name: string; }; }; - location: { - name: string; - area: number; - length: number; - category: string; - cadastral_id: string; - municipality: { - id: number; - name: string; - }; - }; + location: Location; geom: any; batches: Array; assignedTo: User['id']; @@ -131,7 +122,6 @@ export type FishStocking< mixins: [ DbConnection({ createActions: { - update: false, create: false, }, }), @@ -461,7 +451,6 @@ export type FishStocking< hooks: { before: { create: ['parseGeomField', 'parseReviewLocationField'], - updateFishStocking: ['parseGeomField'], updateRegistration: ['parseGeomField'], register: ['parseGeomField'], review: ['parseReviewLocationField'], @@ -477,6 +466,9 @@ export type FishStocking< remove: { auth: RestrictionType.ADMIN, }, + update: { + rest: null, + }, }, }) export default class FishStockingsService extends moleculer.Service { @@ -1340,242 +1332,6 @@ export default class FishStockingsService extends moleculer.Service { return q; } - @Method - async seedDB() { - if (process.env.NODE_ENV === 'local') { - await this.broker.waitForServices([ - 'users', - 'tenants', - 'tenantUsers', - 'fishBatches', - 'mandatoryLocations', - ]); - - const user: User[] = await this.broker.call('users.find', { - query: { - email: 'vadovas@imone.lt', - }, - }); - - const reviewData = { - waybillNo: '1', - veterinaryApprovalNo: '1', - veterinaryApprovalOrderNo: '1', - containerWaterTemp: '17', - waterTemp: '16', - }; - - const data: any[] = [ - { - ...reviewData, - eventTime: '2021-06-06T17:09:40.164Z', - reviewTime: '2021-06-06T18:05:40.164Z', - createdBy: user?.[0]?.id, - assignedTo: user?.[0]?.id, - reviewedBy: user?.[0]?.id, - phone: '861111111', - batches: [ - { - amount: 100, - reviewAmount: 100, - fishType: 1, - fishAge: 2, - }, - { - amount: 150, - reviewAmount: 150, - fishType: 5, - fishAge: 4, - }, - ], - fishTypes: { - 1: 100, - 5: 150, - }, - geom: '0101000020120D0000000000004CA51E4100000080F3325741', - fishOrigin: 'GROWN', - fishOriginCompanyName: 'Test', - location: { - name: 'Nemunas', - cadastral_id: '10010001', - municipality: { - id: 52, - name: 'Kauno r. sav.', - }, - }, - comment: 'komentaras', - }, - { - ...reviewData, - eventTime: '2024-04-06T17:09:40.164Z', - reviewTime: '2021-04-07T12:05:40.164Z', - createdBy: user?.[0]?.id, - assignedTo: user?.[0]?.id, - reviewedBy: user?.[0]?.id, - phone: '861111111', - batches: [ - { - amount: 10, - reviewAmount: 10, - fishType: 2, - fishAge: 3, - }, - { - amount: 20, - reviewAmount: 20, - fishType: 4, - fishAge: 1, - }, - ], - fishTypes: { - 2: 10, - 4: 20, - }, - geom: '0101000020120D000000000000309E2141000000405B155741', - fishOrigin: 'GROWN', - fishOriginCompanyName: 'Test', - location: { - name: 'Baluošas', - cadastral_id: '10010002', - municipality: { - id: 7, - name: 'Švenčionių r. sav.', - }, - }, - comment: 'komentaras', - }, - { - ...reviewData, - eventTime: '2024-05-06T17:09:40.164Z', - reviewTime: '2024-04-07T12:05:40.164Z', - createdBy: user?.[0]?.id, - assignedTo: user?.[0]?.id, - reviewedBy: user?.[0]?.id, - phone: '861111111', - batches: [ - { - amount: 50, - fishType: 6, - fishAge: 1, - }, - { - amount: 50, - fishType: 3, - fishAge: 3, - }, - ], - fishTypes: { - 6: 50, - 3: 50, - }, - geom: '0101000020120D000000000000000B1F410000004092315741', - fishOrigin: 'GROWN', - fishOriginCompanyName: 'Test', - location: { - name: 'Dubrius', - cadastral_id: '10011443', - municipality: { - id: 52, - name: 'Kauno r. sav.', - }, - }, - comment: 'komentaras', - }, - { - ...reviewData, - eventTime: '2022-05-06T17:09:40.164Z', - reviewTime: '2022-05-07T12:05:40.164Z', - createdBy: user?.[0]?.id, - assignedTo: user?.[0]?.id, - reviewedBy: user?.[0]?.id, - phone: '861111111', - batches: [ - { - amount: 70, - reviewAmount: 70, - fishType: 9, - fishAge: 1, - }, - { - amount: 70, - reviewAmount: 70, - fishType: 1, - fishAge: 6, - }, - ], - fishTypes: { - 9: 70, - 1: 70, - }, - geom: '0101000020120D00000000000080351F41000000C0A0345741', - fishOrigin: 'GROWN', - fishOriginCompanyName: 'Test', - location: { - area: '5.89', - name: 'Paežeris', - cadastral_id: '10031211', - municipality: { - id: 49, - name: 'Kaišiadorių r. sav.', - }, - }, - comment: 'komentaras', - }, - { - ...reviewData, - eventTime: '2023-05-06T17:09:40.164Z', - reviewTime: '2023-05-07T12:05:40.164Z', - createdBy: user?.[0]?.id, - assignedTo: user?.[0]?.id, - reviewedBy: user?.[0]?.id, - phone: '861111111', - batches: [ - { - amount: 300, - reviewAmount: 300, - fishType: 1, - fishAge: 1, - }, - { - amount: 300, - reviewAmount: 300, - fishType: 10, - fishAge: 1, - }, - ], - fishTypes: { - 1: 300, - 10: 300, - }, - geom: '0101000020120D00008C58F93F86DF1B418E299256749F5741', - fishOrigin: 'GROWN', - fishOriginCompanyName: 'Test', - location: { - cadastral_id: '41040012', - name: 'Rėkyva', - municipality: { - id: 29, - name: 'Šiaulių m. sav.', - }, - area: '1196.84', - }, - comment: 'komentaras', - }, - ]; - - for (const item of data) { - const fishStocking = await this.createEntity(null, item); - if (fishStocking?.id) { - const batches = item.batches.map((batch: FishBatch) => ({ - ...batch, - fishStocking: fishStocking.id, - })); - await this.broker.call('fishBatches.createMany', batches); - } - } - } - } - @Event() async 'fishBatches.*'(ctx: Context>) { //Generates an object with amounts of fish stocked and stores in the database. @@ -1629,9 +1385,31 @@ export default class FishStockingsService extends moleculer.Service { //TODO: delete after release async started() { - // const fishStockings = await this.getEntities(null); - // for (const fishStocking of fishStockings) { - // const cadastralId = fishStockings.location.cadastral_id; - // } + await this.broker.waitForServices(['locations']); + const fishStockings: FishStocking[] = await this.actions.find({ + query: { + $raw: `location->>'category' IS NULL`, + }, + }); + for (const fishStocking of fishStockings) { + try { + const cadastralId = fishStocking.location?.cadastral_id; + if (!cadastralId) continue; + const uetkObject: Location = await this.broker.call('locations.uetkSearchByCadastralId', { + cadastralId, + }); + if (!uetkObject) continue; + const updatedLocation = { + ...uetkObject, + ...fishStocking.location, + }; + await this.actions.update({ + id: fishStocking.id, + location: updatedLocation, + }); + } catch (e) { + continue; + } + } } } diff --git a/services/locations.service.ts b/services/locations.service.ts index eccf638..d12eaa1 100644 --- a/services/locations.service.ts +++ b/services/locations.service.ts @@ -4,7 +4,7 @@ import { find, map } from 'lodash'; import moleculer, { Context } from 'moleculer'; import { Action, Method, Service } from 'moleculer-decorators'; import { GeomFeatureCollection, coordinatesToGeometry } from '../modules/geometry'; -import { CommonFields, CommonPopulates, RestrictionType, Table } from '../types'; +import { RestrictionType } from '../types'; import { UserAuthMeta } from './api.service'; const CategoryTranslates: any = { @@ -30,22 +30,18 @@ const getBox = (geom: GeomFeatureCollection, tolerance: number = 0.001) => { return `${topLeft.lng},${bottomRight.lat},${bottomRight.lng},${topLeft.lat}`; }; -interface Fields extends CommonFields { - cadastral_id: string; +export interface Location { name: string; + area: number; + length: number; + category: string; + cadastral_id: string; municipality: { id: number; name: string; }; } -interface Populates extends CommonPopulates {} - -export type Location< - P extends keyof Populates = never, - F extends keyof (Fields & Populates) = keyof Fields, -> = Table; - @Service({ name: 'locations', }) @@ -134,6 +130,7 @@ export default class LocationsService extends moleculer.Service { const url = `${targetUrl}?${queryString}`; try { const data = await fetch(url).then((r) => r.json()); + if (!data?.rows?.[0]) return; return this.mapUETKObject(ctx, data?.rows?.[0]); } catch (error) { throw new Error(`Failed to fetch: ${error.message}`); From 2e447e48a03dbab35d4dab8eadf8726a10b2b90c Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 26 Sep 2024 15:00:58 +0300 Subject: [PATCH 15/16] remove console.log --- services/fishStockings.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index d50d2ff..d9cf8aa 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -745,7 +745,6 @@ export default class FishStockingsService extends moleculer.Service { }, }) async register(ctx: Context) { - console.log('register location', ctx.params.location); // Validate eventTime const timeBeforeReview = await isTimeBeforeReview(ctx, new Date(ctx.params.eventTime)); if (!timeBeforeReview) { From 484204068d4a59204897fe520f447cb75affd4fc Mon Sep 17 00:00:00 2001 From: Dovile Date: Fri, 27 Sep 2024 14:57:44 +0300 Subject: [PATCH 16/16] municipality fix --- services/locations.service.ts | 102 +++++++++++++++------------------- 1 file changed, 46 insertions(+), 56 deletions(-) diff --git a/services/locations.service.ts b/services/locations.service.ts index d12eaa1..5cd9789 100644 --- a/services/locations.service.ts +++ b/services/locations.service.ts @@ -188,49 +188,6 @@ export default class LocationsService extends moleculer.Service { } } - @Method - async getRiverOrLakeFromPoint(geom: GeomFeatureCollection) { - if (geom?.features?.length) { - try { - const box = getBox(geom, 200); - const rivers = `${process.env.GEO_SERVER}/qgisserver/uetk_zuvinimas?SERVICE=WFS&REQUEST=GetFeature&TYPENAME=rivers&OUTPUTFORMAT=application/json&GEOMETRYNAME=centroid&BBOX=${box}`; - const riversData = await fetch(rivers, { - headers: { - 'Content-Type': 'application/json', - }, - }); - const riversResult = await riversData.json(); - const municipality = await this.getMunicipalityFromPoint(geom); - const lakes = `${process.env.GEO_SERVER}/qgisserver/uetk_zuvinimas?SERVICE=WFS&REQUEST=GetFeature&TYPENAME=lakes_ponds&OUTPUTFORMAT=application/json&GEOMETRYNAME=centroid&BBOX=${box}`; - const lakesData = await fetch(lakes, { - headers: { - 'Content-Type': 'application/json', - }, - }); - const lakesResult = await lakesData.json(); - const list = [...riversResult.features, ...lakesResult.features]; - - const mappedList = map(list, (item) => { - return { - cadastral_id: item.properties.kadastro_id, - name: item.properties.pavadinimas, - municipality: municipality, - area: item.properties.st_area - ? Math.round(item.properties.st_area / 100) / 100 - : undefined, //ha - length: item.properties.ilgis_uetk, //km - category: CategoryTranslates[item.properties.kategorija], - }; - }); - return mappedList; - } catch (err) { - throw new moleculer.Errors.ValidationError(err.message); - } - } else { - throw new moleculer.Errors.ValidationError('Invalid geometry'); - } - } - @Action({ rest: 'GET /municipalities/search', params: { @@ -240,7 +197,7 @@ export default class LocationsService extends moleculer.Service { ttl: 24 * 60 * 60, }, }) - async searchMunicipalities(ctx: Context<{ geom?: string }>) { + async searchMunicipalitiesByGeom(ctx: Context<{ geom?: string }>) { const geom: GeomFeatureCollection = JSON.parse(ctx.params.geom); return this.getMunicipalityFromPoint(geom); } @@ -293,25 +250,58 @@ export default class LocationsService extends moleculer.Service { return find(municipalities?.rows, { id: ctx.params.id }); } - @Action({ - params: { - name: 'string', - }, - }) - async searchMunicipality(ctx: Context<{ name: string }>) { - const municipalities = await this.actions.getMunicipalities(null, { - parentCtx: ctx, - }); - return find(municipalities?.rows, { name: ctx.params.name }); + @Method + async getRiverOrLakeFromPoint(geom: GeomFeatureCollection) { + if (geom?.features?.length) { + try { + const box = getBox(geom, 200); + const rivers = `${process.env.GEO_SERVER}/qgisserver/uetk_zuvinimas?SERVICE=WFS&REQUEST=GetFeature&TYPENAME=rivers&OUTPUTFORMAT=application/json&GEOMETRYNAME=centroid&BBOX=${box}`; + const riversData = await fetch(rivers, { + headers: { + 'Content-Type': 'application/json', + }, + }); + const riversResult = await riversData.json(); + const municipality = await this.getMunicipalityFromPoint(geom); + const lakes = `${process.env.GEO_SERVER}/qgisserver/uetk_zuvinimas?SERVICE=WFS&REQUEST=GetFeature&TYPENAME=lakes_ponds&OUTPUTFORMAT=application/json&GEOMETRYNAME=centroid&BBOX=${box}`; + const lakesData = await fetch(lakes, { + headers: { + 'Content-Type': 'application/json', + }, + }); + const lakesResult = await lakesData.json(); + const list = [...riversResult.features, ...lakesResult.features]; + + const mappedList = map(list, (item) => { + return { + cadastral_id: item.properties.kadastro_id, + name: item.properties.pavadinimas, + municipality: municipality, + area: item.properties.st_area + ? Math.round(item.properties.st_area / 100) / 100 + : undefined, //ha + length: item.properties.ilgis_uetk, //km + category: CategoryTranslates[item.properties.kategorija], + }; + }); + return mappedList; + } catch (err) { + throw new moleculer.Errors.ValidationError(err.message); + } + } else { + throw new moleculer.Errors.ValidationError('Invalid geometry'); + } } @Method async mapUETKObject(ctx: Context, item: any) { - const municipalities = await this.actions.getMunicipalities(null, { parentCtx: ctx }); return { name: item.name, cadastral_id: item.cadastralId, - municipality: municipalities?.rows?.find((m: any) => m.name === item.municipality), + municipality: { + name: item.municipality, + id: item.municipalityCode, + }, area: item.area ? Math.round(item.area / 100) / 100 : undefined, //ha length: item.length ? Math.round(item.length / 10) / 100 : undefined, //km category: item.categoryTranslate,