Skip to main content

Rooms controller specs passing

Commit Message​

Rooms controller specs passing

NOTE: if, after running `pnpm psy sync`, your editor starts showing errors like
`Unsafe argument of type error typed assigned to a parameter of type 'undefined'.`
OR
`Unsafe assignment of an error typed value.`,
restart the ESLint server (the `ESLint: Restart ESLint Server`
command in VSCode / Cursor)

Note the changes in api/spec/unit/controllers/V1/Host/Places/RoomsController.spec.ts:

1. The room is no longer the base STI model because, in STI, we cannot instantiate the base model, only child models, so we choose an arbitrary Room child to stand in for any Room.
2. The request body to the create and update endpoints is changed to reflect the STI child we are creating/updating.

```console
pnpm psy sync
pnpm build:spec # to surface any errors so you can fix them
pnpm uspec spec/unit/controllers/V1/Host/Places/RoomsController.spec.ts
```

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqRHFQRjCHrGh9xPhFnGUR

Changes​

diff --git a/api/spec/factories/Room/BathroomFactory.ts b/api/spec/factories/Room/BathroomFactory.ts
index 8a9e812..3f7a5b7 100644
--- a/api/spec/factories/Room/BathroomFactory.ts
+++ b/api/spec/factories/Room/BathroomFactory.ts
@@ -1,8 +1,10 @@
import { UpdateableProperties } from '@rvoh/dream/types'
import Bathroom from '#models/Room/Bathroom.js'
+import createPlace from '#spec/factories/PlaceFactory.js'

export default async function createBathroom(attrs: UpdateableProperties<Bathroom> = {}) {
return await Bathroom.create({
+ place: attrs.place ?? (await createPlace()),
bathOrShowerStyle: 'bath',
...attrs,
})
diff --git a/api/spec/factories/Room/BedroomFactory.ts b/api/spec/factories/Room/BedroomFactory.ts
index cb7e175..bbb7711 100644
--- a/api/spec/factories/Room/BedroomFactory.ts
+++ b/api/spec/factories/Room/BedroomFactory.ts
@@ -1,8 +1,10 @@
import { UpdateableProperties } from '@rvoh/dream/types'
import Bedroom from '#models/Room/Bedroom.js'
+import createPlace from '#spec/factories/PlaceFactory.js'

export default async function createBedroom(attrs: UpdateableProperties<Bedroom> = {}) {
return await Bedroom.create({
+ place: attrs.place ?? (await createPlace()),
bedTypes: ['twin'],
...attrs,
})
diff --git a/api/spec/factories/Room/DenFactory.ts b/api/spec/factories/Room/DenFactory.ts
index b240c0f..b9c0726 100644
--- a/api/spec/factories/Room/DenFactory.ts
+++ b/api/spec/factories/Room/DenFactory.ts
@@ -1,8 +1,10 @@
import { UpdateableProperties } from '@rvoh/dream/types'
import Den from '#models/Room/Den.js'
+import createPlace from '#spec/factories/PlaceFactory.js'

export default async function createDen(attrs: UpdateableProperties<Den> = {}) {
return await Den.create({
+ place: attrs.place ?? (await createPlace()),
...attrs,
})
}
diff --git a/api/spec/factories/Room/KitchenFactory.ts b/api/spec/factories/Room/KitchenFactory.ts
index 46ded7a..8305bbf 100644
--- a/api/spec/factories/Room/KitchenFactory.ts
+++ b/api/spec/factories/Room/KitchenFactory.ts
@@ -1,8 +1,10 @@
import { UpdateableProperties } from '@rvoh/dream/types'
import Kitchen from '#models/Room/Kitchen.js'
+import createPlace from '#spec/factories/PlaceFactory.js'

export default async function createKitchen(attrs: UpdateableProperties<Kitchen> = {}) {
return await Kitchen.create({
+ place: attrs.place ?? (await createPlace()),
appliances: ['stove'],
...attrs,
})
diff --git a/api/spec/factories/Room/LivingRoomFactory.ts b/api/spec/factories/Room/LivingRoomFactory.ts
index 2f29333..0c2fdbb 100644
--- a/api/spec/factories/Room/LivingRoomFactory.ts
+++ b/api/spec/factories/Room/LivingRoomFactory.ts
@@ -1,8 +1,10 @@
import { UpdateableProperties } from '@rvoh/dream/types'
import LivingRoom from '#models/Room/LivingRoom.js'
+import createPlace from '#spec/factories/PlaceFactory.js'

export default async function createLivingRoom(attrs: UpdateableProperties<LivingRoom> = {}) {
return await LivingRoom.create({
+ place: attrs.place ?? (await createPlace()),
...attrs,
})
}
diff --git a/api/spec/factories/RoomFactory.ts b/api/spec/factories/RoomFactory.ts
deleted file mode 100644
index 36bf9f8..0000000
--- a/api/spec/factories/RoomFactory.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { UpdateableProperties } from '@rvoh/dream/types'
-import Room from '#models/Room.js'
-import createPlace from '#spec/factories/PlaceFactory.js'
-
-export default async function createRoom(attrs: UpdateableProperties<Room> = {}) {
- return await Room.create({
- place: attrs.place ?? (await createPlace()),
- ...attrs,
- })
-}
diff --git a/api/spec/unit/controllers/V1/Host/Places/RoomsController.spec.ts b/api/spec/unit/controllers/V1/Host/Places/RoomsController.spec.ts
index 085fed3..fa4ab0f 100644
--- a/api/spec/unit/controllers/V1/Host/Places/RoomsController.spec.ts
+++ b/api/spec/unit/controllers/V1/Host/Places/RoomsController.spec.ts
@@ -1,7 +1,10 @@
+import Kitchen from '#models/Room/Kitchen.js'
import Room from '#models/Room.js'
import User from '#models/User.js'
import Place from '#models/Place.js'
-import createRoom from '#spec/factories/RoomFactory.js'
+import createHost from '#spec/factories/HostFactory.js'
+import createHostPlace from '#spec/factories/HostPlaceFactory.js'
+import createKitchen from '#spec/factories/Room/KitchenFactory.js'
import createUser from '#spec/factories/UserFactory.js'
import createPlace from '#spec/factories/PlaceFactory.js'
import { RequestBody, session, SpecRequestType } from '#spec/unit/helpers/authentication.js'
@@ -13,7 +16,9 @@ describe('V1/Host/Places/RoomsController', () => {

beforeEach(async () => {
user = await createUser()
- place = await createPlace({ user })
+ const host = await createHost({ user })
+ place = await createPlace()
+ await createHostPlace({ host, place })
request = await session(user)
})

@@ -25,7 +30,7 @@ describe('V1/Host/Places/RoomsController', () => {
}

it('returns the index of Rooms', async () => {
- const room = await createRoom({ place })
+ const room = await createKitchen({ place })

const { body } = await index(200)

@@ -38,7 +43,7 @@ describe('V1/Host/Places/RoomsController', () => {

context('Rooms created by another Place', () => {
it('are omitted', async () => {
- await createRoom()
+ await createKitchen()

const { body } = await index(200)

@@ -56,7 +61,7 @@ describe('V1/Host/Places/RoomsController', () => {
}

it('returns the specified Room', async () => {
- const room = await createRoom({ place })
+ const room = await createKitchen({ place })

const { body } = await show(room, 200)

@@ -71,7 +76,7 @@ describe('V1/Host/Places/RoomsController', () => {

context('Room created by another Place', () => {
it('is not found', async () => {
- const otherPlaceRoom = await createRoom()
+ const otherPlaceRoom = await createKitchen()

await show(otherPlaceRoom, 404)
})
@@ -91,17 +96,19 @@ describe('V1/Host/Places/RoomsController', () => {

it('creates a Room for this Place', async () => {
const { body } = await create({
- position: 1,
+ type: 'Kitchen',
+ appliances: ['oven', 'stove'],
}, 201)

const room = await place.associationQuery('rooms').firstOrFail()
- expect(room.position).toEqual(1)
+ expect(room.type).toEqual('Kitchen')
+ expect((room as Kitchen).appliances).toEqual(['oven', 'stove'])

expect(body).toEqual(
expect.objectContaining({
id: room.id,
- type: room.type,
- position: room.position,
+ type: 'Kitchen',
+ appliances: ['oven', 'stove'],
}),
)
})
@@ -121,27 +128,26 @@ describe('V1/Host/Places/RoomsController', () => {
}

it('updates the Room', async () => {
- const room = await createRoom({ place })
+ const room = await createKitchen({ place, appliances: ['microwave'] })

await update(room, {
- position: 2,
+ appliances: ['dishwasher'],
}, 204)

await room.reload()
- expect(room.position).toEqual(2)
+ expect(room.appliances).toEqual(['dishwasher'])
})

context('a Room created by another Place', () => {
it('is not updated', async () => {
- const room = await createRoom()
- const originalPosition = room.position
+ const room = await createKitchen({ appliances: ['microwave'] })

await update(room, {
- position: 2,
+ appliances: ['dishwasher'],
}, 404)

await room.reload()
- expect(room.position).toEqual(originalPosition)
+ expect(room.appliances).toEqual(['microwave'])
})
})
})
@@ -155,7 +161,7 @@ describe('V1/Host/Places/RoomsController', () => {
}

it('deletes the Room', async () => {
- const room = await createRoom({ place })
+ const room = await createKitchen({ place })

await destroy(room, 204)

@@ -164,7 +170,7 @@ describe('V1/Host/Places/RoomsController', () => {

context('a Room created by another Place', () => {
it('is not deleted', async () => {
- const room = await createRoom()
+ const room = await createKitchen()

await destroy(room, 404)

diff --git a/api/src/app/controllers/V1/Host/Places/BaseController.ts b/api/src/app/controllers/V1/Host/Places/BaseController.ts
index 475dacc..5a0fab6 100644
--- a/api/src/app/controllers/V1/Host/Places/BaseController.ts
+++ b/api/src/app/controllers/V1/Host/Places/BaseController.ts
@@ -1,5 +1,14 @@
+import Place from '#models/Place.js'
+import { BeforeAction } from '@rvoh/psychic'
import V1HostBaseController from '../BaseController.js'

export default class V1HostPlacesBaseController extends V1HostBaseController {
+ protected currentPlace: Place

+ @BeforeAction()
+ protected async loadCurrentPlace() {
+ this.currentPlace = await this.currentHost
+ .associationQuery('places')
+ .findOrFail(this.castParam('placeId', 'uuid'))
+ }
}
diff --git a/api/src/app/controllers/V1/Host/Places/RoomsController.ts b/api/src/app/controllers/V1/Host/Places/RoomsController.ts
index 234033c..171e16f 100644
--- a/api/src/app/controllers/V1/Host/Places/RoomsController.ts
+++ b/api/src/app/controllers/V1/Host/Places/RoomsController.ts
@@ -1,11 +1,22 @@
import { OpenAPI } from '@rvoh/psychic'
import { DreamParamSafeColumnNames } from '@rvoh/dream/types'
+import Bathroom from '#models/Room/Bathroom.js'
+import Bedroom from '#models/Room/Bedroom.js'
+import Den from '#models/Room/Den.js'
+import Kitchen from '#models/Room/Kitchen.js'
+import LivingRoom from '#models/Room/LivingRoom.js'
+import { RoomTypesEnumValues } from '#src/types/db.js'
import V1HostPlacesBaseController from './BaseController.js'
import Room from '#models/Room.js'

const openApiTags = ['rooms']

-const paramSafeColumns: DreamParamSafeColumnNames<Room>[] = ['position']
+const paramSafeColumns: DreamParamSafeColumnNames<Room>[] = [
+ 'appliances',
+ 'bathOrShowerStyle',
+ 'bedTypes',
+ 'position',
+]

export default class V1HostPlacesRoomsController extends V1HostPlacesBaseController {
@OpenAPI(Room, {
@@ -17,10 +28,11 @@ export default class V1HostPlacesRoomsController extends V1HostPlacesBaseControl
fastJsonStringify: true,
})
public async index() {
- // const rooms = await this.currentPlace.associationQuery('rooms')
- // .preloadFor('summary')
- // .cursorPaginate({ cursor: this.castParam('cursor', 'string', { allowNull: true }) })
- // this.ok(rooms)
+ const rooms = await this.currentPlace
+ .associationQuery('rooms')
+ .preloadFor('summary')
+ .cursorPaginate({ cursor: this.castParam('cursor', 'string', { allowNull: true }) })
+ this.ok(rooms)
}

@OpenAPI(Room, {
@@ -30,8 +42,8 @@ export default class V1HostPlacesRoomsController extends V1HostPlacesBaseControl
fastJsonStringify: true,
})
public async show() {
- // const room = await this.room()
- // this.ok(room)
+ const room = await this.room()
+ this.ok(room)
}

@OpenAPI(Room, {
@@ -41,12 +53,38 @@ export default class V1HostPlacesRoomsController extends V1HostPlacesBaseControl
fastJsonStringify: true,
requestBody: {
params: paramSafeColumns,
+ including: ['type'],
},
})
public async create() {
- // let room = await this.currentPlace.createAssociation('rooms', this.extractParams(Room, paramSafeColumns))
- // if (room.isPersisted) room = await room.loadFor('default').execute()
- // this.created(room)
+ let room: Room
+ const roomType = this.castParam('type', 'string', { enum: RoomTypesEnumValues })
+ const roomParams = this.extractParams(Room, paramSafeColumns)
+
+ switch (roomType) {
+ case 'Bathroom':
+ room = await Bathroom.create({ place: this.currentPlace, ...roomParams })
+ break
+ case 'Bedroom':
+ room = await Bedroom.create({ place: this.currentPlace, ...roomParams })
+ break
+ case 'Den':
+ room = await Den.create({ place: this.currentPlace, ...roomParams })
+ break
+ case 'Kitchen':
+ room = await Kitchen.create({ place: this.currentPlace, ...roomParams })
+ break
+ case 'LivingRoom':
+ room = await LivingRoom.create({ place: this.currentPlace, ...roomParams })
+ break
+ default: {
+ const _never: never = roomType
+ throw new Error(`Unhandled RoomTypesEnum: ${String(_never)}`)
+ }
+ }
+
+ if (room.isPersisted) room = await room.loadFor('default').execute()
+ this.created(room)
}

@OpenAPI(Room, {
@@ -59,9 +97,9 @@ export default class V1HostPlacesRoomsController extends V1HostPlacesBaseControl
},
})
public async update() {
- // const room = await this.room()
- // await room.update(this.extractParams(Room, paramSafeColumns))
- // this.noContent()
+ const room = await this.room()
+ await room.update(this.extractParams(Room, paramSafeColumns))
+ this.noContent()
}

@OpenAPI({
@@ -71,14 +109,15 @@ export default class V1HostPlacesRoomsController extends V1HostPlacesBaseControl
fastJsonStringify: true,
})
public async destroy() {
- // const room = await this.room()
- // await room.destroy()
- // this.noContent()
+ const room = await this.room()
+ await room.destroy()
+ this.noContent()
}

private async room() {
- // return await this.currentPlace.associationQuery('rooms')
- // .preloadFor('default')
- // .findOrFail(this.castParam('id', 'uuid'))
+ return await this.currentPlace
+ .associationQuery('rooms')
+ .preloadFor('default')
+ .findOrFail(this.castParam('id', 'uuid'))
}
}
diff --git a/api/src/app/models/Place.ts b/api/src/app/models/Place.ts
index e7ad092..5f6d8be 100644
--- a/api/src/app/models/Place.ts
+++ b/api/src/app/models/Place.ts
@@ -3,6 +3,7 @@ import { DreamColumn, DreamSerializers } from '@rvoh/dream/types'
import ApplicationModel from '#models/ApplicationModel.js'
import Host from '#models/Host.js'
import HostPlace from '#models/HostPlace.js'
+import Room from '#models/Room.js'

const deco = new Decorators<typeof Place>()

@@ -32,4 +33,7 @@ export default class Place extends ApplicationModel {

@deco.HasMany('Host', { through: 'hostPlaces' })
public hosts: Host[]
+
+ @deco.HasMany('Room', { dependent: 'destroy' })
+ public rooms: Room[]
}
diff --git a/api/src/openapi/mobile.openapi.json b/api/src/openapi/mobile.openapi.json
index 4f186d0..a0ec6d8 100644
--- a/api/src/openapi/mobile.openapi.json
+++ b/api/src/openapi/mobile.openapi.json
@@ -384,11 +384,60 @@
"schema": {
"type": "object",
"properties": {
+ "appliances": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "dishwasher",
+ "microwave",
+ "oven",
+ "stove"
+ ]
+ }
+ },
+ "bathOrShowerStyle": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "bath",
+ "bath_and_shower",
+ "none",
+ "shower",
+ null
+ ]
+ },
+ "bedTypes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "bunk",
+ "cot",
+ "king",
+ "queen",
+ "sofabed",
+ "twin"
+ ]
+ }
+ },
"position": {
"type": [
"integer",
"null"
]
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bathroom",
+ "Bedroom",
+ "Den",
+ "Kitchen",
+ "LivingRoom"
+ ]
}
}
}
@@ -525,6 +574,45 @@
"schema": {
"type": "object",
"properties": {
+ "appliances": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "dishwasher",
+ "microwave",
+ "oven",
+ "stove"
+ ]
+ }
+ },
+ "bathOrShowerStyle": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "bath",
+ "bath_and_shower",
+ "none",
+ "shower",
+ null
+ ]
+ },
+ "bedTypes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "bunk",
+ "cot",
+ "king",
+ "queen",
+ "sofabed",
+ "twin"
+ ]
+ }
+ },
"position": {
"type": [
"integer",
diff --git a/api/src/openapi/openapi.json b/api/src/openapi/openapi.json
index 39e7c12..e245d0e 100644
--- a/api/src/openapi/openapi.json
+++ b/api/src/openapi/openapi.json
@@ -384,11 +384,60 @@
"schema": {
"type": "object",
"properties": {
+ "appliances": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "dishwasher",
+ "microwave",
+ "oven",
+ "stove"
+ ]
+ }
+ },
+ "bathOrShowerStyle": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "bath",
+ "bath_and_shower",
+ "none",
+ "shower",
+ null
+ ]
+ },
+ "bedTypes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "bunk",
+ "cot",
+ "king",
+ "queen",
+ "sofabed",
+ "twin"
+ ]
+ }
+ },
"position": {
"type": [
"integer",
"null"
]
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bathroom",
+ "Bedroom",
+ "Den",
+ "Kitchen",
+ "LivingRoom"
+ ]
}
}
}
@@ -525,6 +574,45 @@
"schema": {
"type": "object",
"properties": {
+ "appliances": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "dishwasher",
+ "microwave",
+ "oven",
+ "stove"
+ ]
+ }
+ },
+ "bathOrShowerStyle": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "bath",
+ "bath_and_shower",
+ "none",
+ "shower",
+ null
+ ]
+ },
+ "bedTypes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "bunk",
+ "cot",
+ "king",
+ "queen",
+ "sofabed",
+ "twin"
+ ]
+ }
+ },
"position": {
"type": [
"integer",
diff --git a/api/src/openapi/tests.openapi.json b/api/src/openapi/tests.openapi.json
index dada378..8d80904 100644
--- a/api/src/openapi/tests.openapi.json
+++ b/api/src/openapi/tests.openapi.json
@@ -384,11 +384,60 @@
"schema": {
"type": "object",
"properties": {
+ "appliances": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "dishwasher",
+ "microwave",
+ "oven",
+ "stove"
+ ]
+ }
+ },
+ "bathOrShowerStyle": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "bath",
+ "bath_and_shower",
+ "none",
+ "shower",
+ null
+ ]
+ },
+ "bedTypes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "bunk",
+ "cot",
+ "king",
+ "queen",
+ "sofabed",
+ "twin"
+ ]
+ }
+ },
"position": {
"type": [
"integer",
"null"
]
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bathroom",
+ "Bedroom",
+ "Den",
+ "Kitchen",
+ "LivingRoom"
+ ]
}
}
}
@@ -525,6 +574,45 @@
"schema": {
"type": "object",
"properties": {
+ "appliances": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "dishwasher",
+ "microwave",
+ "oven",
+ "stove"
+ ]
+ }
+ },
+ "bathOrShowerStyle": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "bath",
+ "bath_and_shower",
+ "none",
+ "shower",
+ null
+ ]
+ },
+ "bedTypes": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "bunk",
+ "cot",
+ "king",
+ "queen",
+ "sofabed",
+ "twin"
+ ]
+ }
+ },
"position": {
"type": [
"integer",
diff --git a/api/src/types/dream.ts b/api/src/types/dream.ts
index facc6e2..4d7d356 100644
--- a/api/src/types/dream.ts
+++ b/api/src/types/dream.ts
@@ -420,6 +420,15 @@ export const schema = {
requiredAndClauses: null,
passthroughAndClauses: null,
},
+ rooms: {
+ type: 'HasMany',
+ foreignKey: 'placeId',
+ foreignKeyTypeColumn: null,
+ tables: ['rooms'],
+ optional: null,
+ requiredAndClauses: null,
+ passthroughAndClauses: null,
+ },
},
},
rooms: {
diff --git a/api/src/types/openapi/tests.openapi.d.ts b/api/src/types/openapi/tests.openapi.d.ts
index ea6f7fc..83ea146 100644
--- a/api/src/types/openapi/tests.openapi.d.ts
+++ b/api/src/types/openapi/tests.openapi.d.ts
@@ -249,7 +249,13 @@ export interface paths {
requestBody?: {
content: {
"application/json": {
+ appliances?: ("dishwasher" | "microwave" | "oven" | "stove")[];
+ /** @enum {string|null} */
+ bathOrShowerStyle?: "bath" | "bath_and_shower" | "none" | "shower" | null;
+ bedTypes?: ("bunk" | "cot" | "king" | "queen" | "sofabed" | "twin")[];
position?: number | null;
+ /** @enum {string} */
+ type?: "Bathroom" | "Bedroom" | "Den" | "Kitchen" | "LivingRoom";
};
};
};
@@ -358,6 +364,10 @@ export interface paths {
requestBody?: {
content: {
"application/json": {
+ appliances?: ("dishwasher" | "microwave" | "oven" | "stove")[];
+ /** @enum {string|null} */
+ bathOrShowerStyle?: "bath" | "bath_and_shower" | "none" | "shower" | null;
+ bedTypes?: ("bunk" | "cot" | "king" | "queen" | "sofabed" | "twin")[];
position?: number | null;
};
};