Skip to content

Commit

Permalink
Merge branch 'access-request-seed' of https://github.com/bcgov/biohubbc
Browse files Browse the repository at this point in the history
… into access-request-seed
  • Loading branch information
MacQSL committed Sep 5, 2024
2 parents 4be7648 + 1950aa1 commit fcfedb8
Show file tree
Hide file tree
Showing 45 changed files with 3,826 additions and 280 deletions.
3 changes: 3 additions & 0 deletions api/.docker/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ ENV PATH ${HOME}/node_modules/.bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/us
# Copy the rest of the files
COPY . ./

# Update log directory file permissions, prevents permission errors for linux environments
RUN chmod -R a+rw data/logs/*

VOLUME ${HOME}

# start api with live reload
Expand Down
2 changes: 1 addition & 1 deletion api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"winston": "^3.3.3",
"winston-daily-rotate-file": "^5.0.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz",
"zod": "^3.23.0"
"zod": "^3.23.8"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
Expand Down
76 changes: 76 additions & 0 deletions api/src/models/observation-analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { z } from 'zod';

export const QualitativeMeasurementAnalyticsSchema = z.object({
option: z.object({
option_id: z.string(),
option_label: z.string()
}),
taxon_measurement_id: z.string(),
measurement_name: z.string()
});

export type QualitativeMeasurementAnalytics = z.infer<typeof QualitativeMeasurementAnalyticsSchema>;

export const QuantitativeMeasurementAnalyticsSchema = z.object({
value: z.number(),
taxon_measurement_id: z.string(),
measurement_name: z.string()
});

export type QuantitativeMeasurementAnalytics = z.infer<typeof QuantitativeMeasurementAnalyticsSchema>;

export const ObservationCountByGroupSchema = z.object({
row_count: z.number(),
individual_count: z.number(),
individual_percentage: z.number()
});

export type ObservationCountByGroup = z.infer<typeof ObservationCountByGroupSchema>;

export const ObservationCountByGroupWithNamedMeasurementsSchema = ObservationCountByGroupSchema.extend({
qualitative_measurements: z.array(QualitativeMeasurementAnalyticsSchema),
quantitative_measurements: z.array(QuantitativeMeasurementAnalyticsSchema)
});

export type ObservationCountByGroupWithNamedMeasurements = z.infer<
typeof ObservationCountByGroupWithNamedMeasurementsSchema
>;

export const ObservationCountByGroupWithMeasurementsSchema = z.object({
quant_measurements: z.array(
z.object({
value: z.number().nullable(),
critterbase_taxon_measurement_id: z.string()
})
),
qual_measurements: z.array(
z.object({
option_id: z.string().nullable(),
critterbase_taxon_measurement_id: z.string()
})
)
});

export type ObservationCountByGroupWithMeasurements = z.infer<typeof ObservationCountByGroupWithMeasurementsSchema>;

export const ObservationCountByGroupSQLResponse = z
.object({
id: z.string(),
row_count: z.number(),
individual_count: z.number(),
individual_percentage: z.number(),
quant_measurements: z.record(z.string(), z.number().nullable()),
qual_measurements: z.record(z.string(), z.string().nullable())
})
// Allow additional properties
.catchall(z.any());

export type ObservationCountByGroupSQLResponse = z.infer<typeof ObservationCountByGroupSQLResponse>;

export const ObservationAnalyticsResponse = ObservationCountByGroupWithNamedMeasurementsSchema.merge(
ObservationCountByGroupSchema
)
// Allow additional properties
.catchall(z.any());

export type ObservationAnalyticsResponse = z.infer<typeof ObservationAnalyticsResponse>;
243 changes: 243 additions & 0 deletions api/src/paths/analytics/observations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { Operation } from 'express-openapi';
import { RequestHandler } from 'http-proxy-middleware';
import { PROJECT_PERMISSION, SYSTEM_ROLE } from '../../constants/roles';
import { getDBConnection } from '../../database/db';
import { authorizeRequestHandler } from '../../request-handlers/security/authorization';
import { AnalyticsService } from '../../services/analytics-service';
import { getLogger } from '../../utils/logger';

const defaultLog = getLogger('paths/analytics/observations');

export const GET: Operation = [
authorizeRequestHandler((req) => {
return {
or: [
{
validProjectPermissions: [
PROJECT_PERMISSION.COORDINATOR,
PROJECT_PERMISSION.COLLABORATOR,
PROJECT_PERMISSION.OBSERVER
],
surveyId: Number(req.params.surveyId),
discriminator: 'ProjectPermission'
},
{
validSystemRoles: [SYSTEM_ROLE.DATA_ADMINISTRATOR],
discriminator: 'SystemRole'
},
{
validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN],
discriminator: 'SystemRole'
}
]
};
}),
getObservationCountByGroup()
];

GET.apiDoc = {
description: 'get analytics about observations for one or more surveys',
tags: ['analytics'],
security: [
{
Bearer: []
}
],
parameters: [
{
in: 'query',
name: 'surveyIds',
schema: {
type: 'array',
items: {
type: 'integer',
minimum: 1
}
},
required: true
},
{
in: 'query',
name: 'groupByColumns',
schema: {
type: 'array',
items: {
type: 'string'
}
},
description: 'An array of column names to group the observations data by'
},
{
in: 'query',
name: 'groupByQuantitativeMeasurements',
schema: {
type: 'array',
items: {
type: 'string'
}
},
description: 'An array of quantitative taxon_measurement_ids to group the observations data by'
},
{
in: 'query',
name: 'groupByQualitativeMeasurements',
schema: {
type: 'array',
items: {
type: 'string'
}
},
description: 'An array of qualitative taxon_measurement_ids to group the observations data by'
}
],
responses: {
200: {
description: 'Analytics calculated OK.',
content: {
'application/json': {
schema: {
title: 'Observation analytics response object',
type: 'array',
items: {
type: 'object',
required: [
'id',
'row_count',
'individual_count',
'individual_percentage',
'quantitative_measurements',
'qualitative_measurements'
],
// Additional properties is intentionally true to allow for dynamic key-value measurement pairs
additionalProperties: true,
properties: {
id: {
type: 'string',
format: 'uuid',
description: 'Unique identifier for the group. Will not be consistent between requests.'
},
row_count: {
type: 'number',
description: 'Number of rows in the group'
},
individual_count: {
type: 'number',
description: 'Sum of subcount values across all rows in the group'
},
individual_percentage: {
type: 'number',
description:
'Sum of subcount values across the group divided by the sum of subcount values across all observations in the specified surveys'
},
quantitative_measurements: {
type: 'array',
items: {
type: 'object',
description: 'Quantitative measurement groupings',
required: ['taxon_measurement_id', 'measurement_name', 'value'],
additionalProperties: false,
properties: {
taxon_measurement_id: {
type: 'string',
format: 'uuid'
},
measurement_name: {
type: 'string'
},
value: {
type: 'number',
nullable: true
}
}
}
},
qualitative_measurements: {
type: 'array',
items: {
type: 'object',
description: 'Qualitative measurement groupings',
required: ['taxon_measurement_id', 'measurement_name', 'option'],
additionalProperties: false,
properties: {
taxon_measurement_id: {
type: 'string',
format: 'uuid'
},
measurement_name: {
type: 'string'
},
option: {
type: 'object',
required: ['option_id', 'option_label'],
additionalProperties: false,
properties: {
option_id: {
type: 'string',
format: 'uuid',
nullable: true
},
option_label: {
type: 'string',
nullable: true
}
}
}
}
}
}
}
}
}
}
}
},
400: {
$ref: '#/components/responses/400'
},
401: {
$ref: '#/components/responses/401'
},
403: {
$ref: '#/components/responses/403'
},
500: {
$ref: '#/components/responses/500'
},
default: {
$ref: '#/components/responses/default'
}
}
};

export function getObservationCountByGroup(): RequestHandler {
return async (req, res) => {
defaultLog.debug({ label: 'getObservationCountByGroup' });

const connection = getDBConnection(req.keycloak_token);

try {
const { surveyIds, groupByColumns, groupByQuantitativeMeasurements, groupByQualitativeMeasurements } = req.query;

await connection.open();

const analyticsService = new AnalyticsService(connection);

const response = await analyticsService.getObservationCountByGroup(
(surveyIds as string[]).map(Number),
(groupByColumns as string[]) ?? [],
(groupByQuantitativeMeasurements as string[]) ?? [],
(groupByQualitativeMeasurements as string[]) ?? []
);

await connection.commit();

return res.status(200).json(response);
} catch (error) {
defaultLog.error({ label: 'getObservationCountByGroup', message: 'error', error });
await connection.rollback();
throw error;
} finally {
connection.release();
}
};
}
Loading

0 comments on commit fcfedb8

Please sign in to comment.