Skip to content

Commit

Permalink
SIMSBIOHUB-637: Add Geometry to Survey Blocks (Backend) (#1421)
Browse files Browse the repository at this point in the history
* api changes

* ignore-skip

* ignore-skip

* ignore-skip

* ignore-skip
  • Loading branch information
mauberti-bc authored Nov 18, 2024
1 parent 825bfe6 commit 9f51398
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 28 deletions.
11 changes: 7 additions & 4 deletions api/src/openapi/schemas/survey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ export const surveyBlockSchema: OpenAPIV3.SchemaObject = {
title: 'Survey Block',
type: 'object',
additionalProperties: false,
required: ['name', 'description'],
required: ['name', 'description', 'survey_id', 'geojson'],
properties: {
survey_block_id: {
description: 'Survey block id',
Expand All @@ -494,18 +494,21 @@ export const surveyBlockSchema: OpenAPIV3.SchemaObject = {
survey_id: {
description: 'Survey id',
type: 'integer',
nullable: true
minimum: 1
},
name: {
description: 'Name',
type: 'string',
nullable: true
type: 'string'
},
description: {
description: 'Description',
type: 'string',
nullable: true
},
geojson: {
description: 'Geojson',
type: 'object'
},
sample_block_count: {
description: 'Sample block count',
type: 'number'
Expand Down
53 changes: 49 additions & 4 deletions api/src/repositories/survey-block-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('SurveyBlockRepository', () => {
survey_id: 1,
name: '',
description: '',
geojson: '',
create_date: '',
create_user: 1,
update_date: '',
Expand Down Expand Up @@ -82,7 +83,18 @@ describe('SurveyBlockRepository', () => {
});

const repo = new SurveyBlockRepository(dbConnection);
const block: PostSurveyBlock = { survey_block_id: 1, survey_id: 1, name: 'Updated name', description: 'block' };
const block: PostSurveyBlock = {
survey_block_id: 1,
survey_id: 1,
name: 'Updated name',
description: 'block',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
};
const response = await repo.updateSurveyBlock(block);
expect(response.survey_block_id).to.be.eql(1);
expect(response.name).to.be.eql('Updated name');
Expand All @@ -98,7 +110,18 @@ describe('SurveyBlockRepository', () => {
});

const repo = new SurveyBlockRepository(dbConnection);
const block: PostSurveyBlock = { survey_block_id: null, survey_id: 1, name: 'new', description: 'block' };
const block: PostSurveyBlock = {
survey_block_id: null,
survey_id: 1,
name: 'new',
description: 'block',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
};
try {
await repo.updateSurveyBlock(block);
expect.fail();
Expand Down Expand Up @@ -131,7 +154,18 @@ describe('SurveyBlockRepository', () => {
});
const repo = new SurveyBlockRepository(dbConnection);

const block: PostSurveyBlock = { survey_block_id: null, survey_id: 1, name: 'new', description: 'block' };
const block: PostSurveyBlock = {
survey_block_id: null,
survey_id: 1,
name: 'new',
description: 'block',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
};
const response = await repo.insertSurveyBlock(block);

expect(response.name).to.be.eql('new');
Expand All @@ -143,18 +177,29 @@ describe('SurveyBlockRepository', () => {
rows: [],
rowCount: 0
} as any as Promise<QueryResult<any>>;

const dbConnection = getMockDBConnection({
sql: () => mockResponse
});

const repo = new SurveyBlockRepository(dbConnection);

try {
const block = {
survey_block_id: null,
survey_id: 1,
name: null,
description: null
description: null,
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
} as any as PostSurveyBlock;

await repo.insertSurveyBlock(block);

expect.fail();
} catch (error) {
expect((error as any as ApiExecuteSQLError).message).to.be.eq('Failed to insert survey block');
Expand Down
45 changes: 33 additions & 12 deletions api/src/repositories/survey-block-repository.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import { Feature } from 'geojson';
import SQL from 'sql-template-strings';
import { z } from 'zod';
import { ApiExecuteSQLError } from '../errors/api-error';
import { generateGeometryCollectionSQL } from '../utils/spatial-utils';
import { BaseRepository } from './base-repository';

export interface PostSurveyBlock {
survey_block_id: number | null;
survey_id: number;
name: string;
description: string;
geojson: Feature;
}

// This describes the a row in the database for Survey Block
export const SurveyBlockRecord = z.object({
survey_block_id: z.number(),
survey_id: z.number(),
name: z.string(),
description: z.string(),
geojson: z.any(),
revision_count: z.number()
});
export type SurveyBlockRecord = z.infer<typeof SurveyBlockRecord>;

// This describes the a row in the database for Survey Block
export const SurveyBlockRecordWithCount = z.object({
survey_block_id: z.number(),
survey_id: z.number(),
name: z.string(),
description: z.string(),
revision_count: z.number(),
export const SurveyBlockRecordWithCount = SurveyBlockRecord.extend({
sample_block_count: z.number()
});
export type SurveyBlockRecordWithCount = z.infer<typeof SurveyBlockRecordWithCount>;
Expand All @@ -52,6 +52,7 @@ export class SurveyBlockRepository extends BaseRepository {
sb.survey_id,
sb.name,
sb.description,
sb.geojson,
sb.revision_count,
COUNT(ssb.survey_block_id)::integer AS sample_block_count
FROM
Expand All @@ -65,6 +66,7 @@ export class SurveyBlockRepository extends BaseRepository {
sb.survey_id,
sb.name,
sb.description,
sb.geojson,
sb.revision_count;
`;

Expand All @@ -86,15 +88,23 @@ export class SurveyBlockRepository extends BaseRepository {
SET
name = ${block.name},
description = ${block.description},
survey_id=${block.survey_id}
survey_id = ${block.survey_id},
geojson = ${JSON.stringify(block.geojson)},
geography = public.geography(
public.ST_Force2D(
public.ST_SetSRID(`.append(generateGeometryCollectionSQL(block.geojson)).append(`, 4326)
)
)
WHERE
survey_block_id = ${block.survey_block_id}
RETURNING
survey_block_id,
survey_id,
name,
description,
geojson,
revision_count;
`;
`);
const response = await this.connection.sql(sql, SurveyBlockRecord);

if (!response.rowCount) {
Expand All @@ -119,18 +129,29 @@ export class SurveyBlockRepository extends BaseRepository {
INSERT INTO survey_block (
survey_id,
name,
description
description,
geojson,
geography
) VALUES (
${block.survey_id},
${block.name},
${block.description}
)
${block.description},
${JSON.stringify(block.geojson)},
public.geography(
public.ST_Force2D(
public.ST_SetSRID(`.append(generateGeometryCollectionSQL(block.geojson)).append(`, 4326)
)
)
)
RETURNING
survey_block_id,
survey_id,
name,
description,
geojson,
revision_count;
`;
`);

const response = await this.connection.sql(sql, SurveyBlockRecord);

if (!response.rowCount) {
Expand Down
52 changes: 48 additions & 4 deletions api/src/services/survey-block-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,30 @@ describe('SurveyBlockService', () => {
const updateBlock = sinon.stub(SurveyBlockRepository.prototype, 'updateSurveyBlock').resolves();

const blocks: PostSurveyBlock[] = [
{ survey_block_id: null, survey_id: 1, name: 'Old Block', description: 'Updated' },
{ survey_block_id: null, survey_id: 1, name: 'New Block', description: 'block' }
{
survey_block_id: null,
survey_id: 1,
name: 'Old Block',
description: 'Updated',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
},
{
survey_block_id: null,
survey_id: 1,
name: 'New Block',
description: 'block',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
}
];
await service.upsertSurveyBlocks(1, blocks);

Expand Down Expand Up @@ -106,8 +128,30 @@ describe('SurveyBlockService', () => {
const updateBlock = sinon.stub(SurveyBlockRepository.prototype, 'updateSurveyBlock').resolves();

const blocks: PostSurveyBlock[] = [
{ survey_block_id: 10, survey_id: 1, name: 'Old Block', description: 'Updated' },
{ survey_block_id: null, survey_id: 1, name: 'New Block', description: 'block' }
{
survey_block_id: 10,
survey_id: 1,
name: 'Old Block',
description: 'Updated',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
},
{
survey_block_id: null,
survey_id: 1,
name: 'New Block',
description: 'block',
geojson: {
type: 'Feature',
geometry: { type: 'Point', coordinates: [0, 0] },
properties: {},
id: 'testid1'
}
}
];
await service.upsertSurveyBlocks(1, blocks);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ const CreateSurveyBlockDialog: React.FC<ICreateBlockProps> = (props) => {
survey_block_id: null,
name: '',
description: '',
geojson: {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [0, 0]
},
properties: {
name: 'Sample',
description: 'This is a placeholder.'
}
},
sample_block_count: 0
},
validationSchema: BlockCreateYupSchema
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import EditDialog from 'components/dialog/EditDialog';
import BlockForm from './BlockForm';
import { BlockEditYupSchema, ISurveyBlock } from './SurveyBlockForm';
import { BlockEditYupSchema, IPostSurveyBlock } from './SurveyBlockForm';

interface IEditBlockProps {
open: boolean;
initialData?: ISurveyBlock;
initialData?: IPostSurveyBlock;
onSave: (data: any, index?: number) => void;
onClose: () => void;
}
Expand All @@ -23,6 +23,7 @@ const EditSurveyBlockDialog: React.FC<IEditBlockProps> = (props) => {
survey_block_id: initialData?.block.survey_block_id || null,
name: initialData?.block.name || '',
description: initialData?.block.description || '',
geojson: initialData?.block.geojson || '',
sample_block_count: initialData?.block.sample_block_count
},
validationSchema: BlockEditYupSchema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Menu, { MenuProps } from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import YesNoDialog from 'components/dialog/YesNoDialog';
import { useFormikContext } from 'formik';
import { Feature } from 'geojson';
import { IEditSurveyRequest } from 'interfaces/useSurveyApi.interface';
import React, { useState } from 'react';
import { TransitionGroup } from 'react-transition-group';
Expand All @@ -28,19 +29,21 @@ export const SurveyBlockInitialValues = {
export const BlockCreateYupSchema = yup.object({
name: yup.string().required('Name is required').max(50, 'Maximum 50 characters'),
description: yup.string().required('Description is required').max(250, 'Maximum 250 characters')
// TODO: Include geojson in validation after adding map control for blocks
});

// Form validation for Block Item
export const BlockEditYupSchema = BlockCreateYupSchema.shape({
sample_block_count: yup.number().required('Sample block count is required.')
});

export interface ISurveyBlock {
export interface IPostSurveyBlock {
index: number;
block: {
survey_block_id: number | null;
name: string;
description: string;
geojson: Feature;
sample_block_count: number;
};
}
Expand All @@ -50,7 +53,7 @@ const SurveyBlockForm: React.FC = () => {
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isYesNoDialogOpen, setIsYesNoDialogOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState<MenuProps['anchorEl']>(null);
const [editData, setEditData] = useState<ISurveyBlock | undefined>(undefined);
const [editData, setEditData] = useState<IPostSurveyBlock | undefined>(undefined);

const formikProps = useFormikContext<IEditSurveyRequest>();
const { values, handleSubmit, setFieldValue } = formikProps;
Expand Down
Loading

0 comments on commit 9f51398

Please sign in to comment.