From 316afb182b26c606c8f67a4566a3a7a9e9da3e61 Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 4 Apr 2024 10:40:46 +0300 Subject: [PATCH 01/12] fishStockings completed --- ...240404063150_completedFishstockingsView.js | 57 +++++ services/fishStockingsCompleted.service.ts | 82 +++++++ services/public.service.ts | 216 +++++++++++++++++- 3 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 database/migrations/20240404063150_completedFishstockingsView.js create mode 100644 services/fishStockingsCompleted.service.ts diff --git a/database/migrations/20240404063150_completedFishstockingsView.js b/database/migrations/20240404063150_completedFishstockingsView.js new file mode 100644 index 0000000..fea9c89 --- /dev/null +++ b/database/migrations/20240404063150_completedFishstockingsView.js @@ -0,0 +1,57 @@ +exports.up = function (knex) { + return knex.schema.createViewOrReplace('fish_stockings_completed', function (view) { + view.as( + knex.raw(` + WITH fb AS ( + SELECT + fb.fish_stocking_id, + json_agg( + json_build_object( + 'fish_type', + json_build_object('id', ft.id, 'label', ft.label), + 'fish_age', + json_build_object('id', fa.id, 'label', fa.label), + 'count', + fb.amount, + 'weight', + fb.weight + ) + ) AS fish_batches + FROM + public.fish_batches fb + LEFT JOIN public.fish_types ft ON ft.id = fb.fish_type_id + LEFT JOIN public.fish_ages fa ON fa.id = fb.fish_age_id + GROUP BY + fb.fish_stocking_id + ) + SELECT + s.id, + s.event_time, + s.review_time, + s.geom, + s.location::json, + fb.fish_batches + FROM + public.fish_stockings s + LEFT JOIN fb ON fb.fish_stocking_id = s.id + WHERE + EXISTS ( + SELECT + 1 + FROM + public.fish_batches fb + WHERE + fb.fish_stocking_id = s.id + AND fb.review_amount IS NOT NULL + AND fb.deleted_at IS NULL + ) + AND s.review_time IS NOT NULL + AND s.deleted_at IS NULL + `), + ); + }); +}; + +exports.down = function (knex) { + return knex.schema.dropViewIfExists('fish_stockings_completed'); +}; diff --git a/services/fishStockingsCompleted.service.ts b/services/fishStockingsCompleted.service.ts new file mode 100644 index 0000000..76ffe70 --- /dev/null +++ b/services/fishStockingsCompleted.service.ts @@ -0,0 +1,82 @@ +'use strict'; + +import moleculer from 'moleculer'; +import { Service } from 'moleculer-decorators'; +import PostgisMixin from 'moleculer-postgis'; +import DbConnection from '../mixins/database.mixin'; +import { FishAge } from './fishAges.service'; +import { FishType } from './fishTypes.service'; + +export interface FishStockingsCompleted { + id: number; + eventTime: Date; + reviewTime: Date; + geom: any; + location: { + name: string; + area: number; + cadastral_id: string; + municipality: { + id: number; + name: string; + }; + }; + fishBatches: { + fish_type: FishType; + fish_age: FishAge; + count: number; + weight: number; + }; +} + +//TODO: might be unnecessary if fishBatches refactored +@Service({ + name: 'fishStockingsCompleted', + mixins: [ + DbConnection({ + collection: 'fishStockingsCompleted', + rest: false, + createActions: { + create: false, + update: false, + remove: false, + get: false, + createMany: false, + removeAllEntities: false, + }, + }), + PostgisMixin({ + srid: 3346, + }), + ], + settings: { + fields: { + id: { + type: 'number', + primaryKey: true, + secure: true, + }, + date: { + type: 'date', + columnType: 'datetime', + }, + geom: { + type: 'any', + geom: { + type: 'geom', + }, + }, + location: { + type: 'object', + columnType: 'json', + }, + fishes: { + type: 'array', + columnType: 'json', + items: { type: 'object' }, + }, + }, + defaultPopulates: ['geom'], + }, +}) +export default class PublishingFishStockingsService extends moleculer.Service {} diff --git a/services/public.service.ts b/services/public.service.ts index 804973d..434facc 100644 --- a/services/public.service.ts +++ b/services/public.service.ts @@ -1,16 +1,50 @@ 'use strict'; -import moleculer, { Context } from 'moleculer'; +import moleculer, { Context, RestSchema } from 'moleculer'; import { Action, Service } from 'moleculer-decorators'; import { CommonFields, CommonPopulates, RestrictionType, Table } from '../types'; +import { endOfYear, startOfYear } from 'date-fns/fp'; import { GeomFeature } from '../modules/geometry'; import { FishBatch } from './fishBatches.service'; import { FishStockingPhoto } from './fishStockingPhotos.service'; import { FishStocking } from './fishStockings.service'; +import { FishStockingsCompleted } from './fishStockingsCompleted.service'; import { Tenant } from './tenants.service'; import { User } from './users.service'; +interface KeyValue { + [key: string]: any; +} + +type FishBatchesStats = { + [cadastralId: string]: { + count: number; + cadastralId: string; + [key: string]: + | any + | { + count: number; + fishType: { id: number; label: string }; + }; + }; +}; + +type StatsByCadastralIdAndFish = { + [cadastralId: string]: { + count: number; + byFish: { + [fishId: string]: { + count: number; + fishType: { + id: number; + label: string; + }; + }; + }; + }; +}; + interface Fields extends CommonFields { id: number; eventTime: Date; @@ -57,7 +91,6 @@ export type FishAge< name: 'public', }) export default class FishAgesService extends moleculer.Service { - @Action({ rest: 'GET /fishStockings', auth: RestrictionType.PUBLIC, @@ -103,4 +136,183 @@ export default class FishAgesService extends moleculer.Service { fish_count: fishCount, }; } + + @Action({ + rest: { + method: 'GET', + path: '/uetk/statistics', + }, + params: { + date: [ + { + type: 'string', + optional: true, + }, + { + type: 'object', + optional: true, + }, + ], + fishType: { + type: 'number', + convert: true, + optional: true, + }, + year: 'number|convert|optional', + cadastralId: 'string|optional', + }, + auth: RestrictionType.PUBLIC, + }) + async getStatisticsForUETK( + ctx: Context<{ date: any; fishType: number; year: number; cadastralId: string }>, + ) { + const { fishType, date, year, cadastralId } = ctx.params; + const query: any = { reviewAmount: { $exists: true } }; + + if (fishType) { + query.fishType = fishType; + } + + if (date) { + query.createdAt = date; + try { + query.createdAt = JSON.parse(date); + } catch (err) {} + } + + // if (cadastralId) { + // query.$raw = { + // condition: `?? @> ?::jsonb`, + // bindings: ['location', { cadastral_id: [cadastralId] }], + // }; + // } + + if (year) { + console.log('year!!!', year); + const yearDate = new Date(year); + const startTime = startOfYear(yearDate); + const endTime = endOfYear(yearDate); + query.createdAt = { + $gte: startTime.toDateString(), + $lt: endTime.toDateString(), + }; + query.$raw = { + condition: ``, + }; + } + + const fishBatches: FishBatch<'fishStocking' | 'fishType'>[] = await ctx.call( + 'fishBatches.find', + { + query, + populate: ['fishStocking', 'fishType'], + }, + ); + + const stats = fishBatches.reduce((groupedFishBatch, fishBatch) => { + const cadastralId = fishBatch?.fishStocking?.location?.cadastral_id; + const fishTypeId = fishBatch?.fishType?.id; + + if (!cadastralId) return groupedFishBatch; + + groupedFishBatch[cadastralId] = groupedFishBatch[cadastralId] || { + count: 0, + cadastralId: cadastralId, + }; + + groupedFishBatch[cadastralId].count += fishBatch.reviewAmount; + + groupedFishBatch[cadastralId][fishTypeId] = groupedFishBatch[cadastralId][fishTypeId] || { + count: 0, + fishType: { id: fishTypeId, label: fishBatch?.fishType?.label }, + }; + + groupedFishBatch[cadastralId][fishTypeId].count += fishBatch.reviewAmount; + + return groupedFishBatch; + }, {} as FishBatchesStats); + + return Object.values(stats).reduce( + (groupedFishBatch: KeyValue, currentGroupedFishBatch: KeyValue) => { + const { cadastralId, count, ...rest } = currentGroupedFishBatch; + groupedFishBatch[cadastralId] = { + count, + byFishes: Object.values(rest), + }; + return groupedFishBatch; + }, + {} as StatsByCadastralIdAndFish, + ); + } + + @Action({ + rest: { + method: 'GET', + path: '/uetk/statistics2', + }, + params: { + date: [ + { + type: 'string', + optional: true, + }, + { + type: 'object', + optional: true, + }, + ], + fishType: { + type: 'number', + convert: true, + optional: true, + }, + year: 'number|convert|optional', + cadastralId: 'string|optional', + }, + auth: RestrictionType.PUBLIC, + }) + async getStatisticsForUETK2( + ctx: Context<{ date: any; fishType: number; year: number; cadastralId: string }>, + ) { + const { fishType, date, year, cadastralId } = ctx.params; + const query: any = { review_time: { $exists: true } }; + + if (fishType) { + query.fishType = fishType; + } + + if (date) { + query.reviewTime = date; + try { + query.reviewTime = JSON.parse(date); + } catch (err) {} + } + + if (cadastralId) { + query.$raw = { + condition: `?? @> ?::jsonb`, + bindings: ['location', { cadastral_id: [cadastralId] }], + }; + } + + if (year) { + const yearDate = new Date().setFullYear(year); + const startTime = startOfYear(yearDate); + const endTime = endOfYear(yearDate); + console.log('year', yearDate, startTime, endTime); + query.reviewTime = { + $gte: startTime.toDateString(), + $lt: endTime.toDateString(), + }; + } + + const completedFishStockings: FishStockingsCompleted[] = await ctx.call( + 'fishStockingsCompleted.find', + { + query, + }, + ); + + return completedFishStockings; + } } From de5d43833f7e38653029720e099f68e4ab880ff4 Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 4 Apr 2024 12:57:40 +0300 Subject: [PATCH 02/12] updated public endpoint --- ...240404063150_completedFishstockingsView.js | 4 +- services/fishStockings.service.ts | 329 +++++++----------- services/fishStockingsCompleted.service.ts | 16 +- services/public.service.ts | 214 ++++-------- 4 files changed, 216 insertions(+), 347 deletions(-) diff --git a/database/migrations/20240404063150_completedFishstockingsView.js b/database/migrations/20240404063150_completedFishstockingsView.js index fea9c89..c966556 100644 --- a/database/migrations/20240404063150_completedFishstockingsView.js +++ b/database/migrations/20240404063150_completedFishstockingsView.js @@ -12,9 +12,9 @@ exports.up = function (knex) { 'fish_age', json_build_object('id', fa.id, 'label', fa.label), 'count', - fb.amount, + fb.review_amount, 'weight', - fb.weight + fb.review_weight ) ) AS fish_batches FROM diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index 3b8f0e3..18b3334 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -1,6 +1,13 @@ 'use strict'; +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'; +import DbConnection from '../mixins/database.mixin'; +import GeometriesMixin from '../mixins/geometries.mixin'; import { GeomFeatureCollection, coordinatesToGeometry, geometryToGeom } from '../modules/geometry'; import { COMMON_DEFAULT_SCOPES, @@ -9,32 +16,30 @@ import { CommonFields, CommonPopulates, EntityChangedParams, - FieldHookCallback, FishOrigin, FishStockingErrorMessages, FishStockingStatus, - RestrictionType, StatusLabels, + FieldHookCallback, + FishOrigin, + FishStockingErrorMessages, + FishStockingStatus, + RestrictionType, + StatusLabels, Table, } from '../types'; +import { + canProfileModifyFishStocking, + getStatus, + isTimeBeforeReview, + validateAssignedTo, + validateFishData, + validateFishOrigin, + validateStockingCustomer, +} from '../utils/functions'; import { AuthUserRole, UserAuthMeta } from './api.service'; -import { isEmpty, map } from 'lodash'; -import moleculer, { Context, RestSchema } from 'moleculer'; -import { DbContextParameters } from 'moleculer-db'; -import ApiGateway from 'moleculer-web'; -import XLSX from 'xlsx'; -import DbConnection from '../mixins/database.mixin'; -import GeometriesMixin from '../mixins/geometries.mixin'; import { FishBatch } from './fishBatches.service'; import { FishStockingPhoto } from './fishStockingPhotos.service'; import { FishType } from './fishTypes.service'; import { Setting } from './settings.service'; import { Tenant } from './tenants.service'; import { User } from './users.service'; -import { - canProfileModifyFishStocking, getStatus, - isTimeBeforeReview, - validateAssignedTo, - validateFishData, - validateFishOrigin, - validateStockingCustomer -} from "../utils/functions"; const Readable = require('stream').Readable; @@ -98,7 +103,7 @@ interface Fields extends CommonFields { coordinates?: any; oldId: number; fishTypes: any; - status: FishStockingStatus + status: FishStockingStatus; } interface Populates extends CommonPopulates { @@ -156,17 +161,17 @@ export type FishStocking< action: 'tenants.resolve', }, }, - fishOrigin: { type: "enum", values: Object.values(FishOrigin), required: true}, + fishOrigin: { type: 'enum', values: Object.values(FishOrigin), required: true }, fishOriginCompanyName: 'string', fishOriginReservoir: { type: 'object', required: false, properties: { area: 'number', - name: "string", - cadastral_id: "string", - municipality: "string" - } + name: 'string', + cadastral_id: 'string', + municipality: 'string', + }, }, location: { type: 'object', @@ -201,7 +206,8 @@ export type FishStocking< }); }, }, - batches: { // TODO: could be actual jsonb field instead of batches table. This would make selection and updates much easier. + batches: { + // TODO: could be actual jsonb field instead of batches table. This would make selection and updates much easier. type: 'array', readonly: false, required: true, @@ -239,7 +245,7 @@ export type FishStocking< phone: { type: 'string', required: false, - pattern: /^(86|\+3706)\d{7}$/ + pattern: /^(86|\+3706)\d{7}$/, }, reviewedBy: { type: 'number', @@ -337,7 +343,7 @@ export type FishStocking< phone: { type: 'string', required: true, - pattern: /^(86|\+3706)\d{7}$/ + pattern: /^(86|\+3706)\d{7}$/, }, organization: 'string', }, @@ -370,7 +376,8 @@ export type FishStocking< ); }, }, - mandatory: { //TODO: mandatory flag could be part of location object + mandatory: { + //TODO: mandatory flag could be part of location object virtual: true, get: async ({ entity, ctx }: FieldHookCallback) => { const area = entity.location.area; @@ -414,11 +421,10 @@ export type FishStocking< actions: { remove: { auth: RestrictionType.ADMIN, - } - } + }, + }, }) export default class FishStockingsService extends moleculer.Service { - @Action({ rest: 'PATCH /:id', auth: RestrictionType.ADMIN, @@ -427,17 +433,17 @@ export default class FishStockingsService extends moleculer.Service { comment: 'string|optional', tenant: 'number|optional', stockingCustomer: 'number|optional', - fishOrigin: { type: "enum", values: Object.values(FishOrigin), optional: true }, + fishOrigin: { type: 'enum', values: Object.values(FishOrigin), optional: true }, fishOriginCompanyName: 'string|optional', fishOriginReservoir: { type: 'object', optional: true, properties: { area: 'number', - name: "string", - cadastral_id: "string", - municipality: "string" - } + name: 'string', + cadastral_id: 'string', + municipality: 'string', + }, }, location: 'object|optional', geom: 'any|optional', @@ -454,16 +460,16 @@ export default class FishStockingsService extends moleculer.Service { amount: 'number|integer|positive|convert', weight: 'number|optional|convert', reviewAmount: 'number|integer|positive|optional', - reviewWeight: 'number|optional' - } - } + reviewWeight: 'number|optional', + }, + }, }, assignedTo: 'number|optional', phone: { // TODO: freelancer might not have phone number and currently it is not required for freelancer to enter phone number in FishStocking registration form. type: 'string', optional: true, - pattern: /^(86|\+3706)\d{7}$/ + pattern: /^(86|\+3706)\d{7}$/, }, waybillNo: 'string|optional', veterinaryApprovalNo: 'string|optional', @@ -477,62 +483,68 @@ export default class FishStockingsService extends moleculer.Service { type: 'object', properties: { signedBy: 'string', - signature: 'string|base64' - } - } + signature: 'string|base64', + }, + }, }, inspector: 'number|optional', canceledAt: 'string|optional', }, }) async updateFishStocking(ctx: Context) { - const existingFishStocking: FishStocking = await this.resolveEntities(ctx, {id: ctx.params.id, populate: 'status'}); + const existingFishStocking: FishStocking = await this.resolveEntities(ctx, { + id: ctx.params.id, + populate: 'status', + }); // Validate tenant - if(ctx.params.tenant) { - await ctx.call('tenants.resolve', {id: ctx.params.tenant, throwIfNotExist: true}) + if (ctx.params.tenant) { + await ctx.call('tenants.resolve', { id: ctx.params.tenant, throwIfNotExist: true }); } // Validate stockingCustomer - if(ctx.params.stockingCustomer) { - const stockingCustomer = await ctx.call('tenants.get', {id: ctx.params.stockingCustomer}); - if(!stockingCustomer) { + if (ctx.params.stockingCustomer) { + const stockingCustomer = await ctx.call('tenants.get', { id: ctx.params.stockingCustomer }); + if (!stockingCustomer) { throw new moleculer.Errors.ValidationError('Invalid stocking customer'); } } // Validate fishType & fishAge - if(ctx.params.batches) { + if (ctx.params.batches) { await validateFishData(ctx); } // Validate assignedTo - if(ctx.params.assignedTo) { + if (ctx.params.assignedTo) { const tenant = ctx.params.tenant || existingFishStocking.tenant; - if (tenant) { // Tenant fish stocking + if (tenant) { + // Tenant fish stocking const tenantUser = await ctx.call('tenantUsers.findOne', { query: { tenant, user: ctx.params.assignedTo, - } + }, }); - if(!tenantUser) { + if (!tenantUser) { throw new moleculer.Errors.ValidationError('Invalid "assignedTo" id'); } - } else { // Freelancers fish stocking + } else { + // Freelancers fish stocking const user: User = await ctx.call('users.get', { id: ctx.params.assignedTo, }); //if user does not exist or is not freelancer - if(!user || !user.isFreelancer) { + if (!user || !user.isFreelancer) { throw new moleculer.Errors.ValidationError('Invalid "assignedTo" id'); } } } // Validate canceledAt time - if(ctx.params.canceledAt) { - const eventTime: Date = ctx.params.eventTime && new Date(ctx.params.eventTime) || existingFishStocking.eventTime; + if (ctx.params.canceledAt) { + const eventTime: Date = + (ctx.params.eventTime && new Date(ctx.params.eventTime)) || existingFishStocking.eventTime; const canceledAtTime = new Date(ctx.params.canceledAt); - if(eventTime.getTime() - canceledAtTime.getTime() <= 0) { + if (eventTime.getTime() - canceledAtTime.getTime() <= 0) { throw new moleculer.Errors.ValidationError('Invalid "canceledAt" time'); } } @@ -546,7 +558,7 @@ export default class FishStockingsService extends moleculer.Service { id: ctx.params.inspector, }); // Validate inspector - if(!inspector) { + if (!inspector) { throw new moleculer.Errors.ValidationError('Invalid inspector id'); } const fishStocking = await this.updateEntity(ctx, { @@ -585,9 +597,12 @@ export default class FishStockingsService extends moleculer.Service { auth: RestrictionType.USER, }) async cancel(ctx: Context) { - const fishStocking = await this.resolveEntities(ctx, { id: ctx.params.id, populate: ['status'] }); + const fishStocking = await this.resolveEntities(ctx, { + id: ctx.params.id, + populate: ['status'], + }); - if(!fishStocking) { + if (!fishStocking) { throw new moleculer.Errors.ValidationError(FishStockingErrorMessages.INVALID_ID); } @@ -602,8 +617,8 @@ export default class FishStockingsService extends moleculer.Service { } //if fish stocking is still in upcoming state, then it can be deleted. - if(fishStocking.status === FishStockingStatus.UPCOMING) { - return this.removeEntity(ctx, {id: fishStocking.id}); + if (fishStocking.status === FishStockingStatus.UPCOMING) { + return this.removeEntity(ctx, { id: fishStocking.id }); } //else it should be canceled return this.updateEntity(ctx, { @@ -622,7 +637,7 @@ export default class FishStockingsService extends moleculer.Service { // TODO: freelancer might not have phone number and currently it is not required for freelancer to enter phone number in FishStocking registration form. type: 'string', optional: true, - pattern: /^(86|\+3706)\d{7}$/ + pattern: /^(86|\+3706)\d{7}$/, }, assignedTo: 'number|integer|convert', location: { @@ -644,31 +659,30 @@ export default class FishStockingsService extends moleculer.Service { fishType: 'number|integer|positive|convert', fishAge: 'number|integer|positive|convert', amount: 'number|integer|positive|convert', - weight: 'number|positive|optional|convert' + weight: 'number|positive|optional|convert', }, - } + }, }, - fishOrigin: { type: "enum", values: Object.values(FishOrigin) }, + fishOrigin: { type: 'enum', values: Object.values(FishOrigin) }, fishOriginCompanyName: 'string|optional', fishOriginReservoir: { type: 'object', optional: true, properties: { area: 'number', - name: "string", - cadastral_id: "string", - municipality: "string" - } + name: 'string', + cadastral_id: 'string', + municipality: 'string', + }, }, tenant: 'number|integer|optional|optional', stockingCustomer: 'number|integer|optional|convert', }, }) async register(ctx: Context) { - // Validate eventTime const timeBeforeReview = await isTimeBeforeReview(ctx, new Date(ctx.params.eventTime)); - if(!timeBeforeReview) { + if (!timeBeforeReview) { throw new moleculer.Errors.ValidationError(FishStockingErrorMessages.INVALID_EVENT_TIME); } @@ -690,16 +704,13 @@ export default class FishStockingsService extends moleculer.Service { const fishStocking: FishStocking = await this.createEntity(ctx); try { - await ctx.call( - 'fishBatches.createBatches', - { - batches: ctx.params.batches, - fishStocking: fishStocking.id, - } - ); + await ctx.call('fishBatches.createBatches', { + batches: ctx.params.batches, + fishStocking: fishStocking.id, + }); } catch (e) { await this.removeEntity(ctx, { id: fishStocking.id }); - throw e; + throw e; } // Send email to notify about new fish stocking @@ -731,7 +742,7 @@ export default class FishStockingsService extends moleculer.Service { // TODO: freelancer might not have phone number and currently it is not required for freelancer to enter phone number in FishStocking registration form. type: 'string', optional: true, - pattern: /^(86|\+3706)\d{7}$/ + pattern: /^(86|\+3706)\d{7}$/, }, assignedTo: { type: 'number', @@ -763,9 +774,9 @@ export default class FishStockingsService extends moleculer.Service { fishType: 'number|convert', fishAge: 'number|convert', amount: 'number|convert', - weight: 'number|optional' - } - } + weight: 'number|optional', + }, + }, }, fishOrigin: 'string', fishOriginCompanyName: 'string|optional', @@ -774,50 +785,58 @@ export default class FishStockingsService extends moleculer.Service { optional: true, properties: { area: 'number', - name: "string", - cadastral_id: "string", - municipality: "string" - } + name: 'string', + cadastral_id: 'string', + municipality: 'string', + }, }, tenant: 'number|optional', stockingCustomer: 'number|optional', }, }) async updateRegistration(ctx: Context) { - const existingFishStocking: FishStocking = await this.resolveEntities(ctx, {id: ctx.params.id, populate: 'status'});; - if(!existingFishStocking) { + const existingFishStocking: FishStocking = await this.resolveEntities(ctx, { + id: ctx.params.id, + populate: 'status', + }); + if (!existingFishStocking) { throw new moleculer.Errors.ValidationError(FishStockingErrorMessages.INVALID_ID); } // Validate fish stocking status - if(![FishStockingStatus.UPCOMING, FishStockingStatus.ONGOING].some(status => status ===existingFishStocking.status)) { + if ( + ![FishStockingStatus.UPCOMING, FishStockingStatus.ONGOING].some( + (status) => status === existingFishStocking.status, + ) + ) { throw new moleculer.Errors.ValidationError(FishStockingErrorMessages.INVALID_STATUS); } //Validate if user can edit fishStocking canProfileModifyFishStocking(ctx, existingFishStocking); // Validate assignedTo - const assignedToChanged = !!ctx.params.assignedTo && ctx.params.assignedTo !== existingFishStocking.assignedTo; + const assignedToChanged = + !!ctx.params.assignedTo && ctx.params.assignedTo !== existingFishStocking.assignedTo; await validateAssignedTo(ctx); // If existing fish stocking time is within the time interval indicating that it is time to review fish stocking, // then most of the data cannot be edited except assignedTo. - if(existingFishStocking.status === FishStockingStatus.ONGOING) { - if(assignedToChanged) { - try{ - return this.updateEntity(ctx, {assignedTo: ctx.params.assignedTo}); + if (existingFishStocking.status === FishStockingStatus.ONGOING) { + if (assignedToChanged) { + try { + return this.updateEntity(ctx, { assignedTo: ctx.params.assignedTo }); } catch (e) { throw new moleculer.Errors.ValidationError('Could not update fishStocking'); } } } - if(existingFishStocking.status === FishStockingStatus.UPCOMING) { + if (existingFishStocking.status === FishStockingStatus.UPCOMING) { // Validate event time - if(ctx.params.eventTime ) { + if (ctx.params.eventTime) { const timeBeforeReview = await isTimeBeforeReview(ctx, new Date(ctx.params.eventTime)); - if(!timeBeforeReview) { + if (!timeBeforeReview) { throw new moleculer.Errors.ValidationError(FishStockingErrorMessages.INVALID_EVENT_TIME); } } // Validate fishType & fishAge - if(ctx.params.batches) { + if (ctx.params.batches) { await validateFishData(ctx); } // Validate stocking customer @@ -858,8 +877,8 @@ export default class FishStockingsService extends moleculer.Service { id: 'number|integer|positive|convert', reviewAmount: 'number|integer|positive|convert', reviewWeight: 'number|optional|convert', - } - } + }, + }, }, signatures: 'any|optional', comment: 'string|optional', @@ -891,10 +910,12 @@ export default class FishStockingsService extends moleculer.Service { UserAuthMeta >, ) { + const existingFishStocking: FishStocking = await this.resolveEntities(ctx, { + id: ctx.params.id, + populate: 'status', + }); - const existingFishStocking: FishStocking = await this.resolveEntities(ctx, {id: ctx.params.id, populate: 'status'}); - - if(!existingFishStocking) { + if (!existingFishStocking) { throw new moleculer.Errors.ValidationError(FishStockingErrorMessages.INVALID_ID); } @@ -902,7 +923,7 @@ export default class FishStockingsService extends moleculer.Service { canProfileModifyFishStocking(ctx, existingFishStocking); // Validate if fishStocking status, it must be ONGOING. - if(existingFishStocking.status !== FishStockingStatus.ONGOING) { + if (existingFishStocking.status !== FishStockingStatus.ONGOING) { throw new moleculer.Errors.ValidationError(FishStockingErrorMessages.INVALID_STATUS); } @@ -961,7 +982,7 @@ export default class FishStockingsService extends moleculer.Service { const adapter = await this.getAdapter(); const knex = adapter.client; return knex.raw( - `select distinct on ("location"::jsonb->'cadastral_id') "location" from "fish_stockings"`, + `select distinct on ("location"::jsonb->'cadastral_id') "location" from "fish_stockings"`, ); } @Action() @@ -986,89 +1007,6 @@ export default class FishStockingsService extends moleculer.Service { return Number(response.rows[0].sum); } - @Action({ - rest: { - method: 'GET', - basePath: '/public', - path: '/uetk/statistics', - }, - params: { - date: [ - { - type: 'string', - optional: true, - }, - { - type: 'object', - optional: true, - }, - ], - fishType: { - type: 'number', - convert: true, - optional: true, - }, - }, - auth: RestrictionType.PUBLIC, - }) - async getStatisticsForUETK(ctx: Context<{ date: any; fishType: number }>) { - const { fishType, date } = ctx.params; - const query: any = { reviewAmount: { $exists: true } }; - - if (fishType) { - query.fishType = fishType; - } - - if (date) { - query.createdAt = date; - try { - query.createdAt = JSON.parse(date); - } catch (err) {} - } - - const fishBatches: FishBatch<'fishStocking' | 'fishType'>[] = await ctx.call( - 'fishBatches.find', - { - query, - populate: ['fishStocking', 'fishType'], - }, - ); - - return Object.values( - fishBatches.reduce((groupedFishBatch, fishBatch) => { - const cadastralId = fishBatch?.fishStocking?.location?.cadastral_id; - const fishTypeId = fishBatch?.fishType?.id; - if (!cadastralId) return groupedFishBatch; - - groupedFishBatch[cadastralId] = groupedFishBatch[cadastralId] || { - count: 0, - cadastralId: cadastralId, - }; - - groupedFishBatch[cadastralId].count += fishBatch.reviewAmount; - - groupedFishBatch[cadastralId][fishTypeId] = groupedFishBatch[cadastralId][fishTypeId] || { - count: 0, - fishType: { id: fishTypeId, label: fishBatch?.fishType?.label }, - }; - - groupedFishBatch[cadastralId][fishTypeId].count += fishBatch.reviewAmount; - - return groupedFishBatch; - }, {} as any), - ).reduce( - ( - groupedFishBatch: { [key: string]: any }, - currentGroupedFishBatch: { [key: string]: any }, - ) => { - const { cadastralId, count, ...rest } = currentGroupedFishBatch; - groupedFishBatch[cadastralId] = { count, byFishes: Object.values(rest) }; - return groupedFishBatch; - }, - {}, - ); - } - @Action({ rest: 'GET /export', }) @@ -1079,7 +1017,7 @@ export default class FishStockingsService extends moleculer.Service { populate: ['assignedTo', 'reviewedBy', 'batches', 'status'], }); const mappedData: any[] = []; - data.map((fishStocking: FishStocking<'reviewedBy'|'assignedTo' |'batches'>) => { + data.map((fishStocking: FishStocking<'reviewedBy' | 'assignedTo' | 'batches'>) => { const fishOrigin = fishStocking.fishOrigin === 'GROWN' ? fishStocking?.fishOriginCompanyName @@ -1103,13 +1041,13 @@ export default class FishStockingsService extends moleculer.Service { Amžius: batch.fishAge?.label, 'Planuojamas kiekis, vnt': batch.amount || 0, 'Kiekis, vnt.': batch.reviewAmount || 0, - 'Planuojamas svoris, kg' : batch.weight || 0, + 'Planuojamas svoris, kg': batch.weight || 0, 'Svoris, kg': batch.reviewWeight || 0, 'Žuvys išaugintos': fishOrigin, 'Važtaraščio nr.': waybillNo || '', 'Atsakingas asmuo': assignedTo || '', 'Veterinarinio pažymėjimo Nr.': veterinaryApprovalNo || '', - 'Būsena': StatusLabels[status], + Būsena: StatusLabels[status], }); } }); @@ -1144,7 +1082,6 @@ export default class FishStockingsService extends moleculer.Service { geom?: GeomFeatureCollection; }>, ) { - const { geom, id } = ctx.params; if (geom?.features?.length) { @@ -1261,7 +1198,6 @@ export default class FishStockingsService extends moleculer.Service { } } if (filters.status) { - const settings: Setting = await ctx.call('settings.getSettings'); const statusQueries: any = getStatusQueries(settings.maxTimeForRegistration); @@ -1344,7 +1280,6 @@ export default class FishStockingsService extends moleculer.Service { return q; } - @Event() async 'fishBatches.*'(ctx: Context>) { //Generates an object with amounts of fish stocked and stores in the database. diff --git a/services/fishStockingsCompleted.service.ts b/services/fishStockingsCompleted.service.ts index 76ffe70..6f0ff9d 100644 --- a/services/fishStockingsCompleted.service.ts +++ b/services/fishStockingsCompleted.service.ts @@ -7,6 +7,12 @@ import DbConnection from '../mixins/database.mixin'; import { FishAge } from './fishAges.service'; import { FishType } from './fishTypes.service'; +export interface CompletedFishBatch { + fish_type: FishType; + fish_age: FishAge; + count: number; + weight: number; +} export interface FishStockingsCompleted { id: number; eventTime: Date; @@ -26,7 +32,7 @@ export interface FishStockingsCompleted { fish_age: FishAge; count: number; weight: number; - }; + }[]; } //TODO: might be unnecessary if fishBatches refactored @@ -56,7 +62,11 @@ export interface FishStockingsCompleted { primaryKey: true, secure: true, }, - date: { + eventTime: { + type: 'date', + columnType: 'datetime', + }, + reviewTime: { type: 'date', columnType: 'datetime', }, @@ -70,7 +80,7 @@ export interface FishStockingsCompleted { type: 'object', columnType: 'json', }, - fishes: { + fishBatches: { type: 'array', columnType: 'json', items: { type: 'object' }, diff --git a/services/public.service.ts b/services/public.service.ts index 434facc..a28b947 100644 --- a/services/public.service.ts +++ b/services/public.service.ts @@ -9,42 +9,10 @@ import { GeomFeature } from '../modules/geometry'; import { FishBatch } from './fishBatches.service'; import { FishStockingPhoto } from './fishStockingPhotos.service'; import { FishStocking } from './fishStockings.service'; -import { FishStockingsCompleted } from './fishStockingsCompleted.service'; +import { CompletedFishBatch, FishStockingsCompleted } from './fishStockingsCompleted.service'; import { Tenant } from './tenants.service'; import { User } from './users.service'; -interface KeyValue { - [key: string]: any; -} - -type FishBatchesStats = { - [cadastralId: string]: { - count: number; - cadastralId: string; - [key: string]: - | any - | { - count: number; - fishType: { id: number; label: string }; - }; - }; -}; - -type StatsByCadastralIdAndFish = { - [cadastralId: string]: { - count: number; - byFish: { - [fishId: string]: { - count: number; - fishType: { - id: number; - label: string; - }; - }; - }; - }; -}; - interface Fields extends CommonFields { id: number; eventTime: Date; @@ -167,120 +135,15 @@ export default class FishAgesService extends moleculer.Service { ctx: Context<{ date: any; fishType: number; year: number; cadastralId: string }>, ) { const { fishType, date, year, cadastralId } = ctx.params; - const query: any = { reviewAmount: { $exists: true } }; + const query: any = {}; if (fishType) { - query.fishType = fishType; - } - - if (date) { - query.createdAt = date; - try { - query.createdAt = JSON.parse(date); - } catch (err) {} - } - - // if (cadastralId) { - // query.$raw = { - // condition: `?? @> ?::jsonb`, - // bindings: ['location', { cadastral_id: [cadastralId] }], - // }; - // } - - if (year) { - console.log('year!!!', year); - const yearDate = new Date(year); - const startTime = startOfYear(yearDate); - const endTime = endOfYear(yearDate); - query.createdAt = { - $gte: startTime.toDateString(), - $lt: endTime.toDateString(), - }; + const condition = `fish_batches::jsonb @> '[{"fish_type": {"id": ${fishType} }}]'`; query.$raw = { - condition: ``, + condition, }; } - const fishBatches: FishBatch<'fishStocking' | 'fishType'>[] = await ctx.call( - 'fishBatches.find', - { - query, - populate: ['fishStocking', 'fishType'], - }, - ); - - const stats = fishBatches.reduce((groupedFishBatch, fishBatch) => { - const cadastralId = fishBatch?.fishStocking?.location?.cadastral_id; - const fishTypeId = fishBatch?.fishType?.id; - - if (!cadastralId) return groupedFishBatch; - - groupedFishBatch[cadastralId] = groupedFishBatch[cadastralId] || { - count: 0, - cadastralId: cadastralId, - }; - - groupedFishBatch[cadastralId].count += fishBatch.reviewAmount; - - groupedFishBatch[cadastralId][fishTypeId] = groupedFishBatch[cadastralId][fishTypeId] || { - count: 0, - fishType: { id: fishTypeId, label: fishBatch?.fishType?.label }, - }; - - groupedFishBatch[cadastralId][fishTypeId].count += fishBatch.reviewAmount; - - return groupedFishBatch; - }, {} as FishBatchesStats); - - return Object.values(stats).reduce( - (groupedFishBatch: KeyValue, currentGroupedFishBatch: KeyValue) => { - const { cadastralId, count, ...rest } = currentGroupedFishBatch; - groupedFishBatch[cadastralId] = { - count, - byFishes: Object.values(rest), - }; - return groupedFishBatch; - }, - {} as StatsByCadastralIdAndFish, - ); - } - - @Action({ - rest: { - method: 'GET', - path: '/uetk/statistics2', - }, - params: { - date: [ - { - type: 'string', - optional: true, - }, - { - type: 'object', - optional: true, - }, - ], - fishType: { - type: 'number', - convert: true, - optional: true, - }, - year: 'number|convert|optional', - cadastralId: 'string|optional', - }, - auth: RestrictionType.PUBLIC, - }) - async getStatisticsForUETK2( - ctx: Context<{ date: any; fishType: number; year: number; cadastralId: string }>, - ) { - const { fishType, date, year, cadastralId } = ctx.params; - const query: any = { review_time: { $exists: true } }; - - if (fishType) { - query.fishType = fishType; - } - if (date) { query.reviewTime = date; try { @@ -289,9 +152,12 @@ export default class FishAgesService extends moleculer.Service { } if (cadastralId) { + let condition = `location::jsonb @> '{"cadastral_id": "${cadastralId}"}'`; + if (query.$raw?.condition) { + condition = query.$raw?.condition + ' AND ' + condition; + } query.$raw = { - condition: `?? @> ?::jsonb`, - bindings: ['location', { cadastral_id: [cadastralId] }], + condition, }; } @@ -299,7 +165,6 @@ export default class FishAgesService extends moleculer.Service { const yearDate = new Date().setFullYear(year); const startTime = startOfYear(yearDate); const endTime = endOfYear(yearDate); - console.log('year', yearDate, startTime, endTime); query.reviewTime = { $gte: startTime.toDateString(), $lt: endTime.toDateString(), @@ -313,6 +178,65 @@ export default class FishAgesService extends moleculer.Service { }, ); - return completedFishStockings; + const selectedBatches: Array = + completedFishStockings + ?.map((stocking) => + stocking.fishBatches?.map((batch) => ({ + ...batch, + cadastralId: stocking.location.cadastral_id, + })), + ) + ?.flat(); + + const batchesByCadastralId = selectedBatches.reduce((aggregate, value) => { + const { cadastralId } = value; + if (aggregate[cadastralId]) { + aggregate[cadastralId] = [...aggregate[cadastralId], value]; + } else { + aggregate[cadastralId] = [value]; + } + return aggregate; + }, {} as { [cadastralId: string]: Array }); + + const statistics: { + [cadastralId: string]: { + count: number; + byFish: { + [fishId: string]: { + count: number; + fishType: { + id: number; + label: string; + }; + }; + }; + }; + } = {}; + + for (const cadastralId in batchesByCadastralId) { + const batches = batchesByCadastralId[cadastralId]; + const data = batches.reduce( + (aggregate, value) => { + aggregate.count += value.count; + let fishTypeData = aggregate.byFish?.[value.fish_type.id]; + if (fishTypeData) { + fishTypeData.count += value.count; + } else { + fishTypeData = { count: value.count, fishType: value.fish_type }; + } + aggregate.byFish[value.fish_type.id] = fishTypeData; + return aggregate; + }, + { count: 0, byFish: {} } as { + count: number; + byFish: { + [fishTypeId: string]: { count: number; fishType: { id: number; label: string } }; + }; + }, + ); + statistics[cadastralId] = data; + } + + return statistics; } } From 6713f0b964d6a7ba0a98de17497268a4e8bd4d61 Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 4 Apr 2024 13:13:24 +0300 Subject: [PATCH 03/12] updated types --- services/public.service.ts | 43 +++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/services/public.service.ts b/services/public.service.ts index a28b947..63d5ec0 100644 --- a/services/public.service.ts +++ b/services/public.service.ts @@ -13,6 +13,25 @@ import { CompletedFishBatch, FishStockingsCompleted } from './fishStockingsCompl import { Tenant } from './tenants.service'; import { User } from './users.service'; +type StatsByFishTypeId = { + [fishTypeId: string]: { + count: number; + fishType: { + id: number; + label: string; + }; + }; +}; + +type StatsByCadastralId = { + count: number; + byFish: StatsByFishTypeId; +}; + +type Statistics = { + [cadastralId: string]: StatsByCadastralId; +}; + interface Fields extends CommonFields { id: number; eventTime: Date; @@ -198,27 +217,14 @@ export default class FishAgesService extends moleculer.Service { return aggregate; }, {} as { [cadastralId: string]: Array }); - const statistics: { - [cadastralId: string]: { - count: number; - byFish: { - [fishId: string]: { - count: number; - fishType: { - id: number; - label: string; - }; - }; - }; - }; - } = {}; + const statistics: Statistics = {}; for (const cadastralId in batchesByCadastralId) { const batches = batchesByCadastralId[cadastralId]; const data = batches.reduce( (aggregate, value) => { aggregate.count += value.count; - let fishTypeData = aggregate.byFish?.[value.fish_type.id]; + let fishTypeData = aggregate.byFish[value.fish_type.id]; if (fishTypeData) { fishTypeData.count += value.count; } else { @@ -227,12 +233,7 @@ export default class FishAgesService extends moleculer.Service { aggregate.byFish[value.fish_type.id] = fishTypeData; return aggregate; }, - { count: 0, byFish: {} } as { - count: number; - byFish: { - [fishTypeId: string]: { count: number; fishType: { id: number; label: string } }; - }; - }, + { count: 0, byFish: {} } as StatsByCadastralId, ); statistics[cadastralId] = data; } From 88986d800199b5bf666d7bc4fa448c2c525c4817 Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 4 Apr 2024 13:23:37 +0300 Subject: [PATCH 04/12] simplified reduce --- services/public.service.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/services/public.service.ts b/services/public.service.ts index 63d5ec0..771c577 100644 --- a/services/public.service.ts +++ b/services/public.service.ts @@ -208,12 +208,7 @@ export default class FishAgesService extends moleculer.Service { ?.flat(); const batchesByCadastralId = selectedBatches.reduce((aggregate, value) => { - const { cadastralId } = value; - if (aggregate[cadastralId]) { - aggregate[cadastralId] = [...aggregate[cadastralId], value]; - } else { - aggregate[cadastralId] = [value]; - } + aggregate[value.cadastralId] = [...(aggregate[value.cadastralId] || []), value]; return aggregate; }, {} as { [cadastralId: string]: Array }); From ff5869d5d19234218b15494366412a6a5c0bf2c6 Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 4 Apr 2024 13:25:37 +0300 Subject: [PATCH 05/12] cleanup --- services/public.service.ts | 54 ++------------------------------------ 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/services/public.service.ts b/services/public.service.ts index 771c577..f9ba51f 100644 --- a/services/public.service.ts +++ b/services/public.service.ts @@ -1,17 +1,11 @@ 'use strict'; +import { endOfYear, startOfYear } from 'date-fns/fp'; import moleculer, { Context, RestSchema } from 'moleculer'; import { Action, Service } from 'moleculer-decorators'; -import { CommonFields, CommonPopulates, RestrictionType, Table } from '../types'; - -import { endOfYear, startOfYear } from 'date-fns/fp'; -import { GeomFeature } from '../modules/geometry'; -import { FishBatch } from './fishBatches.service'; -import { FishStockingPhoto } from './fishStockingPhotos.service'; +import { RestrictionType } from '../types'; import { FishStocking } from './fishStockings.service'; import { CompletedFishBatch, FishStockingsCompleted } from './fishStockingsCompleted.service'; -import { Tenant } from './tenants.service'; -import { User } from './users.service'; type StatsByFishTypeId = { [fishTypeId: string]: { @@ -32,48 +26,6 @@ type Statistics = { [cadastralId: string]: StatsByCadastralId; }; -interface Fields extends CommonFields { - id: number; - eventTime: Date; - comment?: string; - tenant?: Tenant['id']; - stockingCustomer?: Tenant['id']; - fishOrigin: string; - fishOriginCompanyName?: string; - fishOriginReservoir?: { - id: string; - name: string; - municipality: { id: string; label: string }; - }; - location: GeomFeature; - geom: any; - batches: Array; - assignedTo: User['id']; - phone: string; - reviewedBy?: User['id']; - reviewLocation?: { lat: number; lng: number }; - reviewTime?: Date; - waybillNo?: string; - veterinaryApprovalNo?: string; - veterinaryApprovalOrderNo?: string; - containerWaterTemp?: number; - waterTemp?: number; - images?: FishStockingPhoto['id']; - signatures?: { - organization: string; - signedBy: string; - signature: string; - }[]; - assignedToInspector?: number; -} - -interface Populates extends CommonPopulates {} - -export type FishAge< - P extends keyof Populates = never, - F extends keyof (Fields & Populates) = keyof Fields, -> = Table; - @Service({ name: 'public', }) @@ -82,7 +34,6 @@ export default class FishAgesService extends moleculer.Service { rest: 'GET /fishStockings', auth: RestrictionType.PUBLIC, }) - //TODO: could be moved to fishStockings service async getPublicFishStockings(ctx: Context) { const params = { ...ctx.params, @@ -105,7 +56,6 @@ export default class FishAgesService extends moleculer.Service { rest: 'GET /statistics', auth: RestrictionType.PUBLIC, }) - //TODO: could be moved to fishStockings service async getStatistics(ctx: Context) { const completedFishStockings: FishStocking[] = await ctx.call('fishStockings.count', { filter: { From bcdbbf7cf133f369f6d28032e963df3c3ef56a14 Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 4 Apr 2024 13:47:51 +0300 Subject: [PATCH 06/12] fix delete fish batch in review --- services/fishBatches.service.ts | 73 ++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/services/fishBatches.service.ts b/services/fishBatches.service.ts index 3b9dd47..68fc134 100644 --- a/services/fishBatches.service.ts +++ b/services/fishBatches.service.ts @@ -1,7 +1,7 @@ 'use strict'; import moleculer, { Context } from 'moleculer'; -import {Action, Method, Service} from 'moleculer-decorators'; +import { Action, Method, Service } from 'moleculer-decorators'; import { filter, map } from 'lodash'; import DbConnection from '../mixins/database.mixin'; @@ -13,11 +13,10 @@ import { CommonPopulates, Table, } from '../types'; +import { UserAuthMeta } from './api.service'; import { FishAge } from './fishAges.service'; import { FishStocking } from './fishStockings.service'; import { FishType } from './fishTypes.service'; -import {UserAuthMeta} from "./api.service"; - interface Fields extends CommonFields { id: number; @@ -95,7 +94,6 @@ export type FishBatch< }, }) export default class FishBatchesService extends moleculer.Service { - @Action({ params: { batches: { @@ -107,8 +105,8 @@ export default class FishBatchesService extends moleculer.Service { fishAge: 'number|integer|positive', amount: 'number|integer|positive', weight: 'number|positive|optional', - } - } + }, + }, }, fishStocking: 'number|integer|positive', }, @@ -121,13 +119,12 @@ export default class FishBatchesService extends moleculer.Service { ) { if (ctx.params.batches) { const batches = ctx.params.batches?.map((batch) => ({ - fishType: batch.fishType, - fishAge: batch.fishAge, - amount: batch.amount, - weight: batch.weight, - fishStocking: ctx.params.fishStocking, - }) - ) + fishType: batch.fishType, + fishAge: batch.fishAge, + amount: batch.amount, + weight: batch.weight, + fishStocking: ctx.params.fishStocking, + })); await ctx.call('fishBatches.createMany', batches); } } @@ -147,20 +144,22 @@ export default class FishBatchesService extends moleculer.Service { weight: 'number|optional', reviewAmount: 'number|integer|positive|optional', reviewWeight: 'number|optional', - } - } + }, + }, }, fishStocking: 'number|integer|positive', }, }) //for admin async updateBatches( - ctx: Context<{ - batches: FishBatch[]; - fishStocking: number; - }, UserAuthMeta>, + ctx: Context< + { + batches: FishBatch[]; + fishStocking: number; + }, + UserAuthMeta + >, ) { - await this.deleteExistingBatches(ctx, ctx.params.fishStocking, ctx.params.batches); await this.createOrUpdateBatches(ctx, ctx.params.fishStocking, ctx.params.batches); return await this.findEntities(ctx, { @@ -182,20 +181,22 @@ export default class FishBatchesService extends moleculer.Service { fishType: 'number|integer|positive|optional', fishAge: 'number|integer|positive|optional', amount: 'number|integer|positive', - weight: 'number|optional' - } - } + weight: 'number|optional', + }, + }, }, fishStocking: 'number|integer|positive', }, }) async updateRegisteredBatches( - ctx: Context<{ + ctx: Context< + { batches: FishBatch[]; fishStocking: number; - }, UserAuthMeta>, + }, + UserAuthMeta + >, ) { - await this.deleteExistingBatches(ctx, ctx.params.fishStocking, ctx.params.batches); await this.createOrUpdateBatches(ctx, ctx.params.fishStocking, ctx.params.batches); @@ -216,18 +217,21 @@ export default class FishBatchesService extends moleculer.Service { properties: { id: 'number|integer|positive', reviewAmount: 'number|integer|positive', - reviewWeight: 'number|optional' - } - } + reviewWeight: 'number|optional', + }, + }, }, fishStocking: 'number|integer|positive', }, }) async reviewBatches( - ctx: Context<{ + ctx: Context< + { batches: FishBatch[]; fishStocking: number; - }, UserAuthMeta>, + }, + UserAuthMeta + >, ) { await this.deleteExistingBatches(ctx, ctx.params.fishStocking, ctx.params.batches); await this.createOrUpdateBatches(ctx, ctx.params.fishStocking, ctx.params.batches); @@ -249,8 +253,9 @@ export default class FishBatchesService extends moleculer.Service { query: { fishStocking: fishStockingId }, }); const deleteBatches = filter( - existingBatches, - (existingBatch: FishBatch) => batches?.find((batch) => batch.id && existingBatch.id == batch.id) + existingBatches, + (existingBatch: FishBatch) => + !batches?.find((batch) => batch.id && existingBatch.id == batch.id), ); const promises = map(deleteBatches, (batch: FishBatch) => this.removeEntity(ctx, batch)); await Promise.all(promises); @@ -258,7 +263,7 @@ export default class FishBatchesService extends moleculer.Service { @Method async createOrUpdateBatches(ctx: Context, fishStocking: number, batches: any[]) { - const promises = batches?.map( (batch: FishBatch) => { + const promises = batches?.map((batch: FishBatch) => { if (batch.id) { return this.updateEntity(ctx, batch); } From 40cdbf7b9fcd235eadbb0034075c7ac994e0c352 Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 4 Apr 2024 15:32:44 +0300 Subject: [PATCH 07/12] fix --- .../migrations/20240404063150_completedFishstockingsView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/20240404063150_completedFishstockingsView.js b/database/migrations/20240404063150_completedFishstockingsView.js index c966556..c6ba678 100644 --- a/database/migrations/20240404063150_completedFishstockingsView.js +++ b/database/migrations/20240404063150_completedFishstockingsView.js @@ -1,5 +1,5 @@ exports.up = function (knex) { - return knex.schema.createViewOrReplace('fish_stockings_completed', function (view) { + return knex.schema.createViewOrReplace('fishStockingsCompleted', function (view) { view.as( knex.raw(` WITH fb AS ( From d1fa95605d030d4dfb052f4c1ae1b4933e25002d Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 4 Apr 2024 15:33:10 +0300 Subject: [PATCH 08/12] fix --- .../migrations/20240404063150_completedFishstockingsView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/20240404063150_completedFishstockingsView.js b/database/migrations/20240404063150_completedFishstockingsView.js index c6ba678..d7b7e02 100644 --- a/database/migrations/20240404063150_completedFishstockingsView.js +++ b/database/migrations/20240404063150_completedFishstockingsView.js @@ -53,5 +53,5 @@ exports.up = function (knex) { }; exports.down = function (knex) { - return knex.schema.dropViewIfExists('fish_stockings_completed'); + return knex.schema.dropViewIfExists('fishStockingsCompleted'); }; From 6f9766e9c5789413a6562625c05b4e4a6ec7e8b7 Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 4 Apr 2024 21:04:36 +0300 Subject: [PATCH 09/12] caching --- services/fishStockings.service.ts | 2 + services/public.service.ts | 214 +++++++++++++++++++----------- 2 files changed, 139 insertions(+), 77 deletions(-) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index 18b3334..1c0a693 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -939,6 +939,8 @@ export default class FishStockingsService extends moleculer.Service { reviewTime: new Date(), }); + await this.broker.cacher.clean('public.**'); + return this.resolveEntities(ctx, { id: ctx.params.id, populate: ['batches', 'images'], diff --git a/services/public.service.ts b/services/public.service.ts index f9ba51f..4995f2a 100644 --- a/services/public.service.ts +++ b/services/public.service.ts @@ -1,14 +1,34 @@ 'use strict'; -import { endOfYear, startOfYear } from 'date-fns/fp'; -import moleculer, { Context, RestSchema } from 'moleculer'; -import { Action, Service } from 'moleculer-decorators'; +import { format } from 'date-fns'; +import moleculer, { Context } from 'moleculer'; +import { Action, Method, Service } from 'moleculer-decorators'; import { RestrictionType } from '../types'; import { FishStocking } from './fishStockings.service'; import { CompletedFishBatch, FishStockingsCompleted } from './fishStockingsCompleted.service'; -type StatsByFishTypeId = { - [fishTypeId: string]: { +const uetkStatisticsParams = { + date: [ + { + type: 'string', + optional: true, + }, + { + type: 'object', + optional: true, + }, + ], + fishType: { + type: 'number', + convert: true, + optional: true, + }, + year: 'number|convert|optional', + cadastralId: 'string|optional', +}; + +type StatsById = { + [id: string]: { count: number; fishType: { id: number; @@ -17,15 +37,64 @@ type StatsByFishTypeId = { }; }; +type StatsByYear = { + [year: string]: StatsByCadastralId; +}; + type StatsByCadastralId = { count: number; - byFish: StatsByFishTypeId; + byFish?: StatsById; + byYear?: StatsByYear; }; type Statistics = { [cadastralId: string]: StatsByCadastralId; }; +type Batches = Array; + +type BatchesById = { + [id: string]: Batches; +}; + +const getByFish = (batches: Batches) => { + return batches?.reduce( + (aggregate, value) => { + aggregate.count += value.count; + let fishTypeData = aggregate.byFish[value.fish_type.id]; + if (fishTypeData) { + fishTypeData.count += value.count; + } else { + fishTypeData = { count: value.count, fishType: value.fish_type }; + } + aggregate.byFish[value.fish_type.id] = fishTypeData; + return aggregate; + }, + { count: 0, byFish: {} } as StatsByCadastralId, + ); +}; + +const getStatsByYear = (batches: Batches) => { + const batchesByYear = batches?.reduce((aggregate, value) => { + aggregate[value.year] = [...(aggregate[value.year] || []), value]; + return aggregate; + }, {} as BatchesById); + + const statistics: StatsByYear = {}; + + for (const year in batchesByYear) { + const yearBatches = batchesByYear[year]; + const stats: Omit = getByFish(yearBatches); + statistics[year] = stats; + } + + return statistics; +}; + +const getCount = (batches: Batches) => { + return batches?.reduce((aggregate, batch) => batch.count + aggregate, 0); +}; + @Service({ name: 'public', }) @@ -64,7 +133,6 @@ export default class FishAgesService extends moleculer.Service { }); const locationsCount = await ctx.call('fishStockings.getLocationsCount'); - const fishCount = await ctx.call('fishStockings.getFishCount'); return { @@ -75,35 +143,61 @@ export default class FishAgesService extends moleculer.Service { } @Action({ - rest: { - method: 'GET', - path: '/uetk/statistics', - }, - params: { - date: [ - { - type: 'string', - optional: true, - }, - { - type: 'object', - optional: true, - }, - ], - fishType: { - type: 'number', - convert: true, - optional: true, - }, - year: 'number|convert|optional', - cadastralId: 'string|optional', + rest: 'GET /uetk/statistics', + params: uetkStatisticsParams, + auth: RestrictionType.PUBLIC, + cache: { + ttl: 24 * 60 * 60, }, + }) + async uetkStatistics( + ctx: Context<{ date: any; fishType: number; year: number; cadastralId: string } & any>, + ) { + const batchesByCadastralId = await this.getFilteredBatches(ctx); + + const statistics: Statistics = {}; + + for (const cadastralId in batchesByCadastralId) { + const cadastralIdBatches = batchesByCadastralId[cadastralId]; + statistics[cadastralId] = getByFish(cadastralIdBatches); + } + + return statistics; + } + + @Action({ + rest: 'GET /uetk/statistics/byYear', + // params: uetkStatisticsParams, auth: RestrictionType.PUBLIC, + cache: { + ttl: 24 * 60 * 60, + }, }) - async getStatisticsForUETK( - ctx: Context<{ date: any; fishType: number; year: number; cadastralId: string }>, + async uetkStatisticsByYear( + ctx: Context<{ date: any; fishType: number; year: number; cadastralId: string } & any>, + ) { + const batchesByCadastralId = await this.getFilteredBatches(ctx); + + const statistics: Statistics = {}; + + for (const cadastralId in batchesByCadastralId) { + const batches = batchesByCadastralId[cadastralId]; + const statsByYear: StatsByYear = getStatsByYear(batches); + statistics[cadastralId] = { + count: getCount(batches), + byYear: statsByYear, + }; + } + + return statistics; + } + + @Method + async getFilteredBatches( + ctx: Context<{ date: any; fishType: number; year: number; cadastralId: string } & any>, ) { - const { fishType, date, year, cadastralId } = ctx.params; + const { fishType, date, cadastralId } = ctx.params; + const query: any = {}; if (fishType) { @@ -130,16 +224,6 @@ export default class FishAgesService extends moleculer.Service { }; } - if (year) { - const yearDate = new Date().setFullYear(year); - const startTime = startOfYear(yearDate); - const endTime = endOfYear(yearDate); - query.reviewTime = { - $gte: startTime.toDateString(), - $lt: endTime.toDateString(), - }; - } - const completedFishStockings: FishStockingsCompleted[] = await ctx.call( 'fishStockingsCompleted.find', { @@ -147,42 +231,18 @@ export default class FishAgesService extends moleculer.Service { }, ); - const selectedBatches: Array = - completedFishStockings - ?.map((stocking) => - stocking.fishBatches?.map((batch) => ({ - ...batch, - cadastralId: stocking.location.cadastral_id, - })), - ) - ?.flat(); - - const batchesByCadastralId = selectedBatches.reduce((aggregate, value) => { + const filteredBatches = completedFishStockings + ?.map((stocking) => + stocking.fishBatches?.map((batch) => ({ + ...batch, + cadastralId: stocking.location.cadastral_id, + year: format(new Date(stocking.reviewTime), 'yyyy'), + })), + ) + ?.flat(); + return filteredBatches.reduce((aggregate, value) => { aggregate[value.cadastralId] = [...(aggregate[value.cadastralId] || []), value]; return aggregate; - }, {} as { [cadastralId: string]: Array }); - - const statistics: Statistics = {}; - - for (const cadastralId in batchesByCadastralId) { - const batches = batchesByCadastralId[cadastralId]; - const data = batches.reduce( - (aggregate, value) => { - aggregate.count += value.count; - let fishTypeData = aggregate.byFish[value.fish_type.id]; - if (fishTypeData) { - fishTypeData.count += value.count; - } else { - fishTypeData = { count: value.count, fishType: value.fish_type }; - } - aggregate.byFish[value.fish_type.id] = fishTypeData; - return aggregate; - }, - { count: 0, byFish: {} } as StatsByCadastralId, - ); - statistics[cadastralId] = data; - } - - return statistics; + }, {} as BatchesById); } } From e9ef092828d363a9bf45b5efb5f72fc659aab46f Mon Sep 17 00:00:00 2001 From: Dovile Date: Thu, 4 Apr 2024 21:08:41 +0300 Subject: [PATCH 10/12] service name --- services/fishStockingsCompleted.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/fishStockingsCompleted.service.ts b/services/fishStockingsCompleted.service.ts index 6f0ff9d..ca4199a 100644 --- a/services/fishStockingsCompleted.service.ts +++ b/services/fishStockingsCompleted.service.ts @@ -89,4 +89,4 @@ export interface FishStockingsCompleted { defaultPopulates: ['geom'], }, }) -export default class PublishingFishStockingsService extends moleculer.Service {} +export default class FishStockingsCompletedService extends moleculer.Service {} From c64541273577de3604403a3e6f28137cf0c04856 Mon Sep 17 00:00:00 2001 From: Dovile Date: Fri, 5 Apr 2024 10:07:19 +0300 Subject: [PATCH 11/12] cache clean hooks --- services/fishStockings.service.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index 1c0a693..14fe6eb 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -417,6 +417,11 @@ export type FishStocking< all: ['beforeSelect', 'handleSort'], export: ['beforeSelect', 'handleSort'], }, + after: { + review: ['handleCache'], + updateFishStocking: ['handleCache'], + remove: ['handleCache'], + }, }, actions: { remove: { @@ -589,7 +594,8 @@ export default class FishStockingsService extends moleculer.Service { } return fishStocking; } - return this.updateEntity(ctx); + + return await this.updateEntity(ctx); } @Action({ @@ -939,8 +945,6 @@ export default class FishStockingsService extends moleculer.Service { reviewTime: new Date(), }); - await this.broker.cacher.clean('public.**'); - return this.resolveEntities(ctx, { id: ctx.params.id, populate: ['batches', 'images'], @@ -1332,4 +1336,9 @@ export default class FishStockingsService extends moleculer.Service { ); } } + + @Method + async handleCache() { + await this.broker.cacher.clean('public.**'); + } } From fce97481e2974a7ce8a55b119f3964c61ec2bda9 Mon Sep 17 00:00:00 2001 From: Dovile Date: Wed, 10 Apr 2024 22:02:58 +0300 Subject: [PATCH 12/12] clear cache event --- services/fishStockings.service.ts | 10 ---------- services/public.service.ts | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/services/fishStockings.service.ts b/services/fishStockings.service.ts index 14fe6eb..c2c0546 100644 --- a/services/fishStockings.service.ts +++ b/services/fishStockings.service.ts @@ -417,11 +417,6 @@ export type FishStocking< all: ['beforeSelect', 'handleSort'], export: ['beforeSelect', 'handleSort'], }, - after: { - review: ['handleCache'], - updateFishStocking: ['handleCache'], - remove: ['handleCache'], - }, }, actions: { remove: { @@ -1336,9 +1331,4 @@ export default class FishStockingsService extends moleculer.Service { ); } } - - @Method - async handleCache() { - await this.broker.cacher.clean('public.**'); - } } diff --git a/services/public.service.ts b/services/public.service.ts index 4995f2a..39f9887 100644 --- a/services/public.service.ts +++ b/services/public.service.ts @@ -2,10 +2,11 @@ import { format } from 'date-fns'; import moleculer, { Context } from 'moleculer'; -import { Action, Method, Service } from 'moleculer-decorators'; -import { RestrictionType } from '../types'; +import { Action, Event, Method, Service } from 'moleculer-decorators'; +import { EntityChangedParams, RestrictionType } from '../types'; import { FishStocking } from './fishStockings.service'; import { CompletedFishBatch, FishStockingsCompleted } from './fishStockingsCompleted.service'; +import { TenantUser } from './tenantUsers.service'; const uetkStatisticsParams = { date: [ @@ -245,4 +246,14 @@ export default class FishAgesService extends moleculer.Service { return aggregate; }, {} as BatchesById); } + + @Event() + async 'fishStockings.*'(ctx: Context>) { + switch (ctx.params.type) { + case 'create': + case 'update': + case 'remove': + await this.broker.cacher.clean('public.**'); + } + } }