Skip to content

Commit

Permalink
feat: support updating venue profiles
Browse files Browse the repository at this point in the history
  • Loading branch information
FelixZY committed Nov 19, 2023
1 parent f1a1fe5 commit 8ca0d50
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 12 deletions.
24 changes: 24 additions & 0 deletions src/__test__/model/profiles/venues/patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { generateBasePatchProfileModel } from "@/__test__/model/profiles/base/patch";
import { generateCoordsModel } from "@/__test__/model/profiles/coords";
import { PatchVenueModel } from "@/model/profiles/venues/patch";
import { faker } from "@faker-js/faker";
import { createId } from "@paralleldrive/cuid2";
import { ProfileType } from "@prisma/client";

export function generatePatchVenueModel(
overrides: Partial<PatchVenueModel> = {}
): PatchVenueModel {
return {
...generateBasePatchProfileModel(),
type: ProfileType.venue,
name: faker.helpers.maybe(() => faker.commerce.department()),
coords: faker.helpers.maybe(() => generateCoordsModel()),
permanentlyClosed: faker.helpers.maybe(() =>
faker.datatype.boolean({ probability: 0.2 })
),
parentId: faker.helpers.maybe(() =>
faker.datatype.boolean() ? createId() : null
),
...overrides,
};
}
40 changes: 40 additions & 0 deletions src/api/dto/profiles/venues/patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { BasePatchProfileDtoSchema } from "@/api/dto/profiles/base/patch";
import { CreateVenueDtoSchema } from "@/api/dto/profiles/venues/create";
import { registry } from "@/api/registry";
import z from "@/api/zod";

export type PatchVenueDto = z.infer<typeof PatchVenueDtoSchema>;
export const PatchVenueDtoSchema = registry.register(
"PatchVenueDto",
BasePatchProfileDtoSchema.merge(
z.object({
coords: CreateVenueDtoSchema.shape.coords.optional(),
permanentlyClosed:
CreateVenueDtoSchema.shape.permanentlyClosed.optional(),
parentId: CreateVenueDtoSchema.shape.parentId.optional(),
})
).refine(
(dto) => {
const dtoKeys = Object.keys(dto);
const containsValidChanges = Object.keys(PatchVenueDtoSchema).some(
(key) => dtoKeys.includes(key)
);

// HACK(FelixZY): Typescript does not allow a direct `return containsChanges`
// as we are referencing `PatchVenueDtoSchema` in this function:
// ```
// 'PatchVenueDtoSchema' implicitly has type 'any' because it does not
// have a type annotation and is referenced directly or indirectly in its own initializer.
// ts(7022)
// ```
if (containsValidChanges) {
return true;
} else {
return false;
}
},
{
message: "At least one field modification is required",
}
)
);
97 changes: 97 additions & 0 deletions src/db/dao/profiles/venue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import { mockCreateImageUploadUrlFetchResponses } from "@/__test__/cloudflare";
import { withTestDatabaseForEach } from "@/__test__/db";
import { generateCreateIndividualModel } from "@/__test__/model/profiles/individuals/create";
import { generateCreateVenueModel } from "@/__test__/model/profiles/venues/create";
import { generatePatchVenueModel } from "@/__test__/model/profiles/venues/patch";
import { BaseProfileDao } from "@/db/dao/profiles/base";
import { IndividualDao } from "@/db/dao/profiles/individual";
import { VenueDao } from "@/db/dao/profiles/venue";
import { ImageDao } from "@/db/dao/storage/image";
import { mapVenueModelToReferenceModel } from "@/mapping/profiles/venues/profile";
import { BaseProfileModel } from "@/model/profiles/base/profile";
import { CreateIndividualModel } from "@/model/profiles/individuals/create";
import { CreateVenueModel } from "@/model/profiles/venues/create";
import { PatchVenueModel } from "@/model/profiles/venues/patch";
import { VenueModel } from "@/model/profiles/venues/profile";
import { VenueReferenceModel } from "@/model/profiles/venues/reference";
import fetch from "jest-fetch-mock";
Expand Down Expand Up @@ -292,4 +295,98 @@ describe("VenueDao integration tests", () => {
},
});
});

test("create and patch full venue profile", async () => {
// Arrange
const venueLevel1CreateModel: CreateVenueModel = generateCreateVenueModel({
images: {
coverId: null,
posterId: null,
squareId: null,
},
parentId: null,
});
const venueLevel2CreateModel: CreateVenueModel = generateCreateVenueModel({
images: {
coverId: null,
posterId: null,
squareId: null,
},
parentId: null,
});

const venueLevel1PatchModel: PatchVenueModel = generatePatchVenueModel({
images: {
coverId: null,
posterId: null,
squareId: null,
},
parentId: null,
});
const venueLevel2PatchModel: PatchVenueModel = generatePatchVenueModel({
images: {
coverId: null,
posterId: null,
squareId: null,
},
parentId: null,
});

// Act
const venueLevel1Created = await VenueDao.create(venueLevel1CreateModel);
venueLevel1PatchModel.id = venueLevel1Created.id;
venueLevel2PatchModel.parentId = venueLevel1Created.id;

const venueLevel2Created = await VenueDao.create(venueLevel2CreateModel);
venueLevel2PatchModel.id = venueLevel2Created.id;

await VenueDao.patch(venueLevel1PatchModel);
const venueLevel2Patched = await VenueDao.patch(venueLevel2PatchModel);
const venueLevel1Patched = await VenueDao.getById(venueLevel1PatchModel.id);

// Assert
expect(venueLevel1Created).toMatchObject<
Omit<VenueModel, keyof BaseProfileModel>
>({
coords: venueLevel1CreateModel.coords,
permanentlyClosed: venueLevel1CreateModel.permanentlyClosed,
ancestors: [],
children: [],
});
expect(venueLevel2Created).toMatchObject<
Omit<VenueModel, keyof BaseProfileModel>
>({
coords: venueLevel2CreateModel.coords,
permanentlyClosed: venueLevel2CreateModel.permanentlyClosed,
ancestors: [],
children: [],
});

expect(venueLevel1Patched).not.toBeNull();
expect(venueLevel2Patched).not.toBeNull();
if (venueLevel1Patched === null || venueLevel2Patched === null) {
return;
}

expect(venueLevel1Patched).toMatchObject<
Omit<VenueModel, keyof BaseProfileModel>
>({
coords: venueLevel1PatchModel.coords ?? venueLevel1CreateModel.coords,
permanentlyClosed:
venueLevel1PatchModel.permanentlyClosed ??
venueLevel1CreateModel.permanentlyClosed,
ancestors: [],
children: [mapVenueModelToReferenceModel(venueLevel2Patched)],
});
expect(venueLevel2Patched).toMatchObject<
Omit<VenueModel, keyof BaseProfileModel>
>({
coords: venueLevel2PatchModel.coords ?? venueLevel2CreateModel.coords,
permanentlyClosed:
venueLevel2PatchModel.permanentlyClosed ??
venueLevel2CreateModel.permanentlyClosed,
ancestors: [mapVenueModelToReferenceModel(venueLevel1Patched)],
children: [],
});
});
});
15 changes: 15 additions & 0 deletions src/db/dao/profiles/venue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BaseProfileModel } from "@/model/profiles/base/profile";
import { BaseProfileReferenceModel } from "@/model/profiles/base/reference";
import { CoordsModel } from "@/model/profiles/coords";
import { CreateVenueModel } from "@/model/profiles/venues/create";
import { PatchVenueModel } from "@/model/profiles/venues/patch";
import { VenueModel } from "@/model/profiles/venues/profile";
import { VenueReferenceModel } from "@/model/profiles/venues/reference";
import { isNonNull } from "@/util/is_defined";
Expand Down Expand Up @@ -34,6 +35,20 @@ export const VenueDao = {
}
return profile;
},
/**
* Update a venue's profile
*/
async patch(model: PatchVenueModel): Promise<VenueModel | null> {
await BaseProfileDao.patch(model);
await getDbClient().venueEntity.update(model);
const profile = await this.getById(model.id);
if (profile === null) {
throw new Error(
`Profile id ${model.id} successfully updated but could not be retrieved`
);
}
return profile;
},
/**
* Delete a profile by its id
* @throws {import("@/db/dao/profiles/base").ProfileInUseError} if the profile cannot be deleted due to being linked to one or more events
Expand Down
114 changes: 109 additions & 5 deletions src/db/venues/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
*/

import { withTestDatabaseForEach } from "@/__test__/db";
import { generateCoordsModel } from "@/__test__/model/profiles/coords";
import { generateCreateVenueModel } from "@/__test__/model/profiles/venues/create";
import { getDbClient } from "@/db";
import { VenueDao } from "@/db/dao/profiles/venue";
import { mapVenueModelToReferenceModel } from "@/mapping/profiles/venues/profile";
import { CreateVenueModel } from "@/model/profiles/venues/create";
import { VenueModel } from "@/model/profiles/venues/profile";
import { faker } from "@faker-js/faker";
import { ProfileType } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";

describe("Venue hierarchy constraints", () => {
Expand Down Expand Up @@ -77,11 +80,11 @@ describe("Venue hierarchy constraints", () => {
}

// Act
const promise = getDbClient().$executeRaw`
UPDATE "profiles"."venues"
SET parent_id = ${models[parentIndex].id}
WHERE profile_id = ${models[childIndex].id}
`;
const promise = getDbClient().venueEntity.update({
id: models[childIndex].id,
type: ProfileType.venue,
parentId: models[parentIndex].id,
});

// Assert
await expectConstraintViolation(models[childIndex].id, promise);
Expand All @@ -92,6 +95,107 @@ describe("Venue hierarchy constraints", () => {
describe("Venues manual SQL", () => {
withTestDatabaseForEach();

test("update coords field", async () => {
// Arrange
const model = await VenueDao.create(
generateCreateVenueModel({
images: {
coverId: null,
posterId: null,
squareId: null,
},
parentId: null,
})
);
const newCoords = generateCoordsModel();

// Act
await getDbClient().venueEntity.update({
id: model.id,
type: ProfileType.venue,
coords: newCoords,
});

// Assert
await expect(VenueDao.getById(model.id)).resolves.toMatchObject<
Pick<VenueModel, "coords">
>({
coords: newCoords,
});
});

test("update permanentlyClosed field", async () => {
// Arrange
const model = await VenueDao.create(
generateCreateVenueModel({
permanentlyClosed: false,
images: {
coverId: null,
posterId: null,
squareId: null,
},
parentId: null,
})
);

// Act
await getDbClient().venueEntity.update({
id: model.id,
type: ProfileType.venue,
permanentlyClosed: true,
});

// Assert
await expect(VenueDao.getById(model.id)).resolves.toMatchObject<
Pick<VenueModel, "permanentlyClosed">
>({
permanentlyClosed: true,
});
});

test("update parentId field", async () => {
// Arrange
const model1 = await VenueDao.create(
generateCreateVenueModel({
images: {
coverId: null,
posterId: null,
squareId: null,
},
parentId: null,
})
);
const model2 = await VenueDao.create(
generateCreateVenueModel({
images: {
coverId: null,
posterId: null,
squareId: null,
},
parentId: null,
})
);

// Act
await getDbClient().venueEntity.update({
id: model2.id,
type: ProfileType.venue,
parentId: model1.id,
});

// Assert
await expect(VenueDao.getById(model1.id)).resolves.toMatchObject<
Pick<VenueModel, "children">
>({
children: [mapVenueModelToReferenceModel(model2)],
});
await expect(VenueDao.getById(model2.id)).resolves.toMatchObject<
Pick<VenueModel, "ancestors">
>({
ancestors: [mapVenueModelToReferenceModel(model1)],
});
});

test("retrieve coordinates for a venue", async () => {
// Arrange
const db = getDbClient();
Expand Down
Loading

1 comment on commit 8ca0d50

@vercel
Copy link

@vercel vercel bot commented on 8ca0d50 Nov 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

api – ./

api-git-main-dansdata.vercel.app
api-dansdata.vercel.app
api.dansdata.se

Please sign in to comment.