Skip to content

Commit

Permalink
Merge pull request #271 from tnc-ca-geo/feature/229-add_delete_camera…
Browse files Browse the repository at this point in the history
…_task

Creating DeleteCamera Task
  • Loading branch information
jue-henry authored Dec 24, 2024
2 parents d0da6f8 + 1bd44cc commit db4309e
Show file tree
Hide file tree
Showing 17 changed files with 270 additions and 18 deletions.
2 changes: 1 addition & 1 deletion Dockerfile-test
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ COPY ./ $HOME/
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y curl git

RUN export NODEV='20.12.2' \
RUN export NODEV='20.17.0' \
&& curl "https://nodejs.org/dist/v${NODEV}/node-v${NODEV}-linux-x64.tar.gz" | tar -xzv \
&& cp ./node-v${NODEV}-linux-x64/bin/node /usr/bin/ \
&& ./node-v${NODEV}-linux-x64/bin/npm install -g npm
Expand Down
3 changes: 1 addition & 2 deletions 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,4 @@
"typescript": "^5.4.4",
"undici": "^6.0.0"
}
}
}
10 changes: 10 additions & 0 deletions src/@types/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@ export type CreateViewPayload = {
view?: Maybe<View>;
};

export type DeleteCameraInput = {
cameraId: Scalars['ID']['input'];
};

export type DeleteDeploymentInput = {
cameraId: Scalars['ID']['input'];
deploymentId: Scalars['ID']['input'];
Expand Down Expand Up @@ -602,6 +606,7 @@ export type Mutation = {
createUpload?: Maybe<CreateUploadPayload>;
createUser?: Maybe<StandardPayload>;
createView?: Maybe<CreateViewPayload>;
deleteCameraConfig?: Maybe<Task>;
deleteDeployment?: Maybe<Task>;
deleteImageComment?: Maybe<ImageCommentsPayload>;
deleteImageTag?: Maybe<ImageTagsPayload>;
Expand Down Expand Up @@ -722,6 +727,11 @@ export type MutationCreateViewArgs = {
};


export type MutationDeleteCameraConfigArgs = {
input: DeleteCameraInput;
};


export type MutationDeleteDeploymentArgs = {
input: DeleteDeploymentInput;
};
Expand Down
2 changes: 2 additions & 0 deletions src/api/auth/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const WRITE_DEPLOYMENTS_ROLES = [MANAGER];
const WRITE_AUTOMATION_RULES_ROLES = [MANAGER];
const WRITE_CAMERA_REGISTRATION_ROLES = [MANAGER];
const WRITE_CAMERA_SERIAL_NUMBER_ROLES = [MANAGER];
const WRITE_DELETE_CAMERA_ROLES = [MANAGER];
const WRITE_TAGS_ROLES = [MANAGER, MEMBER];

export {
Expand All @@ -31,5 +32,6 @@ export {
WRITE_AUTOMATION_RULES_ROLES,
WRITE_CAMERA_REGISTRATION_ROLES,
WRITE_CAMERA_SERIAL_NUMBER_ROLES,
WRITE_DELETE_CAMERA_ROLES,
WRITE_TAGS_ROLES,
};
90 changes: 90 additions & 0 deletions src/api/db/models/Camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import retry from 'async-retry';
import {
WRITE_CAMERA_REGISTRATION_ROLES,
WRITE_CAMERA_SERIAL_NUMBER_ROLES,
WRITE_DELETE_CAMERA_ROLES,
} from '../../auth/roles.js';
import { ProjectModel } from './Project.js';
import { BaseAuthedModel, MethodParams, roleCheck, idMatch } from './utils.js';
Expand Down Expand Up @@ -236,6 +237,69 @@ export class CameraModel {
}
}

// NOTE: this function is called by the async task handler as part of the delete camera task
static async removeProjectRegistration(
input: { cameraId: string },
context: Pick<Context, 'user'>,
): Promise<{ wirelessCameras: WirelessCameraSchema[]; project?: ProjectSchema }> {
const projectId = context.user['curr_project'];

try {
const wirelessCameras = await WirelessCamera.find();
const cam = wirelessCameras.find((c) => idMatch(c._id, input.cameraId));

if (!cam) {
const msg = `Couldn't find camera record for camera ${input.cameraId}`;
throw new CameraRegistrationError(msg);
}
const activeReg = cam.projRegistrations.find((pr) => pr.active);

// if active registration === curr_project,
// set default_project registration to active
if (activeReg?.projectId === projectId) {
const defaultProjReg = cam.projRegistrations.find(
(pr) => pr.projectId === 'default_project',
);
if (defaultProjReg) defaultProjReg.active = true;
else {
cam.projRegistrations.push({
_id: new ObjectId(),
projectId: 'default_project',
active: true,
});
}
}
const currProjIndex = cam.projRegistrations.findIndex((pr) => pr.projectId === projectId);
cam.projRegistrations.splice(currProjIndex, 1);
await cam.save();

// make sure there's a Project.cameraConfig record for this camera
// in the default_project and create one if not
let defaultProj = await Project.findOne({ _id: 'default_project' });
if (!defaultProj) {
throw new CameraRegistrationError('Could not find default project');
}

let addedNewCamConfig = false;
const camConfig = defaultProj.cameraConfigs.find((cc) => idMatch(cc._id, input.cameraId));
if (!camConfig) {
defaultProj = (await ProjectModel.createCameraConfig(
{
projectId: 'default_project',
cameraId: input.cameraId,
},
context,
))!;
addedNewCamConfig = true;
}

return { wirelessCameras, ...(addedNewCamConfig && { project: defaultProj }) };
} catch (err) {
if (err instanceof GraphQLError) throw err;
throw new InternalServerError(err as string);
}
}

static async updateSerialNumberTask(
input: gql.UpdateCameraSerialNumberInput,
context: Pick<Context, 'user' | 'config'>,
Expand Down Expand Up @@ -380,6 +444,27 @@ export class CameraModel {
throw new InternalServerError(err as string);
}
}

static async deleteCameraTask(
input: gql.DeleteCameraInput,
context: Pick<Context, 'user' | 'config'>,
): Promise<HydratedDocument<TaskSchema>> {
try {
console.log('CameraModel.deleteCameraTask - input: ', input);
return await TaskModel.create(
{
type: 'DeleteCamera',
projectId: context.user['curr_project'],
user: context.user.sub,
config: input,
},
context,
);
} catch (err) {
if (err instanceof GraphQLError) throw err;
throw new InternalServerError(err as string);
}
}
}

export default class AuthedCameraModel extends BaseAuthedModel {
Expand All @@ -405,6 +490,11 @@ export default class AuthedCameraModel extends BaseAuthedModel {
async updateSerialNumber(...args: MethodParams<typeof CameraModel.updateSerialNumberTask>) {
return await CameraModel.updateSerialNumberTask(...args);
}

@roleCheck(WRITE_DELETE_CAMERA_ROLES)
async deleteCameraConfig(...args: MethodParams<typeof CameraModel.deleteCameraTask>) {
return await CameraModel.deleteCameraTask(...args);
}
}

interface OperationMetadata {
Expand Down
80 changes: 78 additions & 2 deletions src/api/db/models/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export class ProjectModel {

static async createCameraConfig(
input: { projectId: string; cameraId: string },
context: Pick<Context, 'user'>,
_: Pick<Context, 'user'>,
): Promise<HydratedDocument<ProjectSchema>> {
console.log('Project.createCameraConfig - input: ', input);
const { projectId, cameraId } = input;
Expand Down Expand Up @@ -238,6 +238,39 @@ export class ProjectModel {
}
}

static async deleteCameraConfig(
input: { cameraId: string },
context: Pick<Context, 'user'>,
): Promise<HydratedDocument<ProjectSchema>> {
try {
return await retry(
async () => {
let project = await Project.findOne({ _id: context.user['curr_project'] });
if (!project) throw new NotFoundError('Project not found');
console.log('originalProject: ', project);

console.log('Deleting camera config with _id: ', input.cameraId);
// NOTE: using findOneAndUpdate() to update Projects to preserve atomicity of the
// operation and avoid race conditions
const updatedProject = await Project.findOneAndUpdate(
{ _id: context.user['curr_project'] },
{
$pull: { cameraConfigs: { _id: input.cameraId } },
},
{ returnDocument: 'after' },
);

console.log('updatedProject: ', updatedProject);
return updatedProject!;
},
{ retries: 2 },
);
} catch (err) {
if (err instanceof GraphQLError) throw err;
throw new InternalServerError(err as string);
}
}

static async createView(
input: gql.CreateViewInput,
context: Pick<Context, 'user'>,
Expand Down Expand Up @@ -288,7 +321,7 @@ export class ProjectModel {
bail(new ForbiddenError(`View ${view?.name} is not editable`));
}

// appy updates & save project
// apply updates & save project
Object.assign(view, input.diffs);
const updatedProj = await project.save();
return updatedProj.views.find((v): v is HydratedSingleSubdocument<ViewSchema> =>
Expand Down Expand Up @@ -334,6 +367,49 @@ export class ProjectModel {
}
}

static async removeCameraFromViews(
input: { cameraId: string },
context: Pick<Context, 'user'>,
): Promise<HydratedDocument<ProjectSchema>> {
try {
return await retry(
async () => {
// find project
let project = await Project.findOne({ _id: context.user['curr_project'] });
if (!project) throw new NotFoundError('Project not found');

// get all deployment ids for the camera
const projectDeps =
project.cameraConfigs
.find((cc) => cc._id === input.cameraId)
?.deployments.map((d) => d._id.toString()) ?? [];

console.log('deployments to be removed from views: ', projectDeps);

project.views.forEach((v, index) => {
// if view filters has the camera id or any of the deployment ids
if (
v.filters.cameras?.includes(input.cameraId) ||
projectDeps.some((d) => v.filters.deployments?.includes(d))
) {
v.filters.cameras = v.filters.cameras?.filter((c) => c !== input.cameraId);
v.filters.deployments = v.filters.deployments?.filter(
(d) => !projectDeps.includes(d),
);
project.views[index] = v;
}
});

return project.save();
},
{ retries: 2 },
);
} catch (err) {
if (err instanceof GraphQLError) throw err;
throw new InternalServerError(err as string);
}
}

static async updateAutomationRules(
{ automationRules }: gql.UpdateAutomationRulesInput,
context: Pick<Context, 'user'>,
Expand Down
2 changes: 1 addition & 1 deletion src/api/db/models/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class TaskModel {
projectId: input.projectId,
type: input.type,
});

console.log('TaskModel.create task: ', task);
const sqs = new SQS.SQSClient({ region: process.env.AWS_DEFAULT_REGION });

await task.save();
Expand Down
8 changes: 4 additions & 4 deletions src/api/db/models/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ export function buildTagPipeline(tags: string[]): PipelineStage[] {

pipeline.push({
$match: {
tags: { $in: tags }
}
tags: { $in: tags },
},
});

return pipeline
return pipeline;
}

export function buildLabelPipeline(labels: string[]): PipelineStage[] {
Expand Down Expand Up @@ -194,7 +194,7 @@ export function buildPipeline(
}

// match reviewedFilter
if (reviewed !== null) {
if (reviewed !== null && reviewed !== undefined) {
pipeline.push({
$match: {
reviewed: reviewed,
Expand Down
1 change: 1 addition & 0 deletions src/api/db/schemas/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const TaskSchema = new Schema({
'UpdateSerialNumber',
'DeleteImages',
'DeleteImagesByFilter',
'DeleteCamera',
],
},
status: {
Expand Down
9 changes: 9 additions & 0 deletions src/api/resolvers/Mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,15 @@ export default {
return context.models.Camera.updateSerialNumber(input, context);
},

deleteCameraConfig: async (
_: unknown,
{ input }: gql.MutationDeleteCameraConfigArgs,
context: Context,
): Promise<gql.Task> => {
console.log('Mutation.deleteCamera input:', input);
return context.models.Camera.deleteCameraConfig(input, context);
},

createProject: async (
_: unknown,
{ input }: gql.MutationCreateProjectArgs,
Expand Down
5 changes: 5 additions & 0 deletions src/api/type-defs/inputs/DeleteCameraInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default /* GraphQL */ `
input DeleteCameraInput {
cameraId: ID!
}
`;
1 change: 1 addition & 0 deletions src/api/type-defs/root/Mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default /* GraphQL */ `
registerCamera(input: RegisterCameraInput!): RegisterCameraPayload
unregisterCamera(input: UnregisterCameraInput!): UnregisterCameraPayload
updateCameraSerialNumber(input: UpdateCameraSerialNumberInput!): Task
deleteCameraConfig(input: DeleteCameraInput!): Task
createView(input: CreateViewInput!): CreateViewPayload
updateView(input: UpdateViewInput!): UpdateViewPayload
Expand Down
Loading

0 comments on commit db4309e

Please sign in to comment.