Skip to main content

Generate Room resource

Commit Message

Generate Room resource

```console
pnpm psy g:resource --sti-base-serializer --owning-model=Place v1/host/places/\{\}/rooms Room type:enum:room_types:Bathroom,Bedroom,Kitchen,Den,LivingRoom Place:belongs_to position:integer:optional deleted_at:datetime:optional

The places/{} (escaped to places/\{\} for the console) makes rooms a nested resource within the places resource, which you can see with pnpm psy routes:

pnpm psy db:migrate
pnpm psy routes

## Changes

```diff
diff --git a/api/spec/factories/RoomFactory.ts b/api/spec/factories/RoomFactory.ts
new file mode 100644
index 0000000..c75d9f4
--- /dev/null
+++ b/api/spec/factories/RoomFactory.ts
@@ -0,0 +1,10 @@
+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 ? null : 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
new file mode 100644
index 0000000..6c5f66d
--- /dev/null
+++ b/api/spec/unit/controllers/V1/Host/Places/RoomsController.spec.ts
@@ -0,0 +1,175 @@
+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 createUser from '@spec/factories/UserFactory.js'
+import createPlace from '@spec/factories/PlaceFactory.js'
+import { RequestBody, session, SpecRequestType } from '@spec/unit/helpers/authentication.js'
+
+describe('V1/Host/Places/RoomsController', () => {
+ let request: SpecRequestType
+ let user: User
+ let place: Place
+
+ beforeEach(async () => {
+ user = await createUser()
+ place = await createPlace({ user })
+ request = await session(user)
+ })
+
+ describe('GET index', () => {
+ const indexRooms = async <StatusCode extends 200 | 400 | 404>(expectedStatus: StatusCode) => {
+ return request.get('/v1/host/places/{placeId}/rooms', expectedStatus, {
+ placeId: place.id,
+ })
+ }
+
+ it('returns the index of Rooms', async () => {
+ const room = await createRoom({ place })
+
+ const { body } = await indexRooms(200)
+
+ expect(body.results).toEqual([
+ expect.objectContaining({
+ id: room.id,
+ }),
+ ])
+ })
+
+ context('Rooms created by another Place', () => {
+ it('are omitted', async () => {
+ await createRoom()
+
+ const { body } = await indexRooms(200)
+
+ expect(body.results).toEqual([])
+ })
+ })
+ })
+
+ describe('GET show', () => {
+ const showRoom = async <StatusCode extends 200 | 400 | 404>(room: Room, expectedStatus: StatusCode) => {
+ return request.get('/v1/host/places/{placeId}/rooms/{id}', expectedStatus, {
+ placeId: place.id,
+ id: room.id,
+ })
+ }
+
+ it('returns the specified Room', async () => {
+ const room = await createRoom({ place })
+
+ const { body } = await showRoom(room, 200)
+
+ expect(body).toEqual(
+ expect.objectContaining({
+ id: room.id,
+ type: room.type,
+ position: room.position,
+ }),
+ )
+ })
+
+ context('Room created by another Place', () => {
+ it('is not found', async () => {
+ const otherPlaceRoom = await createRoom()
+
+ await showRoom(otherPlaceRoom, 404)
+ })
+ })
+ })
+
+ describe('POST create', () => {
+ const createRoom = async <StatusCode extends 201 | 400 | 404>(
+ data: RequestBody<'post', '/v1/host/places/{placeId}/rooms'>,
+ expectedStatus: StatusCode
+ ) => {
+ return request.post('/v1/host/places/{placeId}/rooms', expectedStatus, {
+ placeId: place.id,
+ data
+ })
+ }
+
+ it('creates a Room for this Place', async () => {
+ const { body } = await createRoom({
+ position: 1,
+ }, 201)
+
+ const room = await place.associationQuery('rooms').firstOrFail()
+ expect(room.position).toEqual(1)
+
+ expect(body).toEqual(
+ expect.objectContaining({
+ id: room.id,
+ type: room.type,
+ position: room.position,
+ }),
+ )
+ })
+ })
+
+ describe('PATCH update', () => {
+ const updateRoom = async <StatusCode extends 204 | 400 | 404>(
+ room: Room,
+ data: RequestBody<'patch', '/v1/host/places/{placeId}/rooms/{id}'>,
+ expectedStatus: StatusCode
+ ) => {
+ return request.patch('/v1/host/places/{placeId}/rooms/{id}', expectedStatus, {
+ placeId: place.id,
+ id: room.id,
+ data,
+ })
+ }
+
+ it('updates the Room', async () => {
+ const room = await createRoom({ place })
+
+ await updateRoom(room, {
+ position: 2,
+ }, 204)
+
+ await room.reload()
+ expect(room.position).toEqual(2)
+ })
+
+ context('a Room created by another Place', () => {
+ it('is not updated', async () => {
+ const room = await createRoom()
+ const originalPosition = room.position
+
+ await updateRoom(room, {
+ position: 2,
+ }, 404)
+
+ await room.reload()
+ expect(room.position).toEqual(originalPosition)
+ })
+ })
+ })
+
+ describe('DELETE destroy', () => {
+ const destroyRoom = async <StatusCode extends 204 | 400 | 404>(room: Room, expectedStatus: StatusCode) => {
+ return request.delete('/v1/host/places/{placeId}/rooms/{id}', expectedStatus, {
+ placeId: place.id,
+ id: room.id,
+ })
+ }
+
+ it('deletes the Room', async () => {
+ const room = await createRoom({ place })
+
+ await destroyRoom(room, 204)
+
+ expect(await Room.find(room.id)).toBeNull()
+ })
+
+ context('a Room created by another Place', () => {
+ it('is not deleted', async () => {
+ const room = await createRoom()
+
+ await destroyRoom(room, 404)
+
+ expect(await Room.find(room.id)).toMatchDreamModel(room)
+ })
+ })
+ })
+})
diff --git a/api/spec/unit/models/Room.spec.ts b/api/spec/unit/models/Room.spec.ts
new file mode 100644
index 0000000..1e8d460
--- /dev/null
+++ b/api/spec/unit/models/Room.spec.ts
@@ -0,0 +1,3 @@
+describe('Room', () => {
+ it.todo('add a test here to get started building Room')
+})
diff --git a/api/src/app/controllers/V1/Host/Places/BaseController.ts b/api/src/app/controllers/V1/Host/Places/BaseController.ts
new file mode 100644
index 0000000..475dacc
--- /dev/null
+++ b/api/src/app/controllers/V1/Host/Places/BaseController.ts
@@ -0,0 +1,5 @@
+import V1HostBaseController from '../BaseController.js'
+
+export default class V1HostPlacesBaseController extends V1HostBaseController {
+
+}
diff --git a/api/src/app/controllers/V1/Host/Places/RoomsController.ts b/api/src/app/controllers/V1/Host/Places/RoomsController.ts
new file mode 100644
index 0000000..f508ae6
--- /dev/null
+++ b/api/src/app/controllers/V1/Host/Places/RoomsController.ts
@@ -0,0 +1,70 @@
+import { OpenAPI } from '@rvoh/psychic'
+import V1HostPlacesBaseController from './BaseController.js'
+import Room from '@models/Room.js'
+
+const openApiTags = ['rooms']
+
+export default class V1HostPlacesRoomsController extends V1HostPlacesBaseController {
+ @OpenAPI(Room, {
+ status: 200,
+ tags: openApiTags,
+ description: 'Paginated index of Rooms',
+ cursorPaginate: true,
+ serializerKey: 'summary',
+ })
+ public async index() {
+ // const rooms = await this.currentPlace.associationQuery('rooms')
+ // .preloadFor('summary')
+ // .cursorPaginate({ cursor: this.castParam('cursor', 'string', { allowNull: true }) })
+ // this.ok(rooms)
+ }
+
+ @OpenAPI(Room, {
+ status: 200,
+ tags: openApiTags,
+ description: 'Fetch a Room',
+ })
+ public async show() {
+ // const room = await this.room()
+ // this.ok(room)
+ }
+
+ @OpenAPI(Room, {
+ status: 201,
+ tags: openApiTags,
+ description: 'Create a Room',
+ })
+ public async create() {
+ // let room = await this.currentPlace.createAssociation('rooms', this.paramsFor(Room))
+ // if (room.isPersisted) room = await room.loadFor('default').execute()
+ // this.created(room)
+ }
+
+ @OpenAPI(Room, {
+ status: 204,
+ tags: openApiTags,
+ description: 'Update a Room',
+ })
+ public async update() {
+ // const room = await this.room()
+ // await room.update(this.paramsFor(Room))
+ // this.noContent()
+ }
+
+ @OpenAPI({
+ status: 204,
+ tags: openApiTags,
+ description: 'Destroy a Room',
+ })
+ public async destroy() {
+ // 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', 'string'))
+ }
+}
diff --git a/api/src/app/models/Room.ts b/api/src/app/models/Room.ts
new file mode 100644
index 0000000..4ea9146
--- /dev/null
+++ b/api/src/app/models/Room.ts
@@ -0,0 +1,30 @@
+import { Decorators } from '@rvoh/dream'
+import { DreamColumn, DreamSerializers } from '@rvoh/dream/types'
+import ApplicationModel from '@models/ApplicationModel.js'
+import Place from '@models/Place.js'
+
+const deco = new Decorators<typeof Room>()
+
+export default class Room extends ApplicationModel {
+ public override get table() {
+ return 'rooms' as const
+ }
+
+ public get serializers(): DreamSerializers<Room> {
+ return {
+ default: 'RoomSerializer',
+ summary: 'RoomSummarySerializer',
+ }
+ }
+
+ public id: DreamColumn<Room, 'id'>
+ public type: DreamColumn<Room, 'type'>
+ public position: DreamColumn<Room, 'position'>
+ public deletedAt: DreamColumn<Room, 'deletedAt'>
+ public createdAt: DreamColumn<Room, 'createdAt'>
+ public updatedAt: DreamColumn<Room, 'updatedAt'>
+
+ @deco.BelongsTo('Place', { on: 'placeId' })
+ public place: Place
+ public placeId: DreamColumn<Room, 'placeId'>
+}
diff --git a/api/src/app/serializers/RoomSerializer.ts b/api/src/app/serializers/RoomSerializer.ts
new file mode 100644
index 0000000..79390f9
--- /dev/null
+++ b/api/src/app/serializers/RoomSerializer.ts
@@ -0,0 +1,12 @@
+import { DreamSerializer } from '@rvoh/dream'
+import Room from '@models/Room.js'
+
+export const RoomSummarySerializer = <T extends Room>(StiChildClass: typeof Room, room: T) =>
+ DreamSerializer(StiChildClass ?? Room, room)
+ .attribute('id')
+
+export const RoomSerializer = <T extends Room>(StiChildClass: typeof Room, room: T) =>
+ RoomSummarySerializer(StiChildClass, room)
+ .attribute('type', { openapi: { type: 'string', enum: [(StiChildClass ?? Room).sanitizedName] } })
+ .attribute('position')
+ .attribute('deletedAt')
diff --git a/api/src/conf/routes.ts b/api/src/conf/routes.ts
index 198aa15..036f15f 100644
--- a/api/src/conf/routes.ts
+++ b/api/src/conf/routes.ts
@@ -4,7 +4,11 @@ import { PsychicRouter } from '@rvoh/psychic'
export default function routes(r: PsychicRouter) {
r.namespace('v1', r => {
r.namespace('host', r => {
- r.resources('places')
+ r.resources('places', r => {
+ r.resources('rooms')
+
+ })
+
})
})

diff --git a/api/src/db/migrations/1765413832143-create-room.ts b/api/src/db/migrations/1765413832143-create-room.ts
new file mode 100644
index 0000000..1de9d4c
--- /dev/null
+++ b/api/src/db/migrations/1765413832143-create-room.ts
@@ -0,0 +1,51 @@
+import { Kysely, sql } from 'kysely'
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function up(db: Kysely<any>): Promise<void> {
+ await db.schema
+ .createType('room_types_enum')
+ .asEnum([
+ 'Bathroom',
+ 'Bedroom',
+ 'Kitchen',
+ 'Den',
+ 'LivingRoom'
+ ])
+ .execute()
+
+ await db.schema
+ .createTable('rooms')
+ .addColumn('id', 'uuid', col =>
+ col
+ .primaryKey()
+ .defaultTo(sql`uuidv7()`),
+ )
+ .addColumn('type', sql`room_types_enum`, col => col.notNull())
+ .addColumn('place_id', 'uuid', col => col.references('places.id').onDelete('restrict').notNull())
+ .addColumn('position', 'integer')
+ .addColumn('deleted_at', 'timestamp')
+ .addColumn('created_at', 'timestamp', col => col.notNull())
+ .addColumn('updated_at', 'timestamp', col => col.notNull())
+ .execute()
+
+ await db.schema
+ .createIndex('rooms_type')
+ .on('rooms')
+ .column('type')
+ .execute()
+
+ await db.schema
+ .createIndex('rooms_place_id')
+ .on('rooms')
+ .column('place_id')
+ .execute()
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function down(db: Kysely<any>): Promise<void> {
+ await db.schema.dropIndex('rooms_type').execute()
+ await db.schema.dropIndex('rooms_place_id').execute()
+ await db.schema.dropTable('rooms').execute()
+
+ await db.schema.dropType('room_types_enum').execute()
+}
\ No newline at end of file
diff --git a/api/src/openapi/mobile.openapi.json b/api/src/openapi/mobile.openapi.json
index f376703..1230573 100644
--- a/api/src/openapi/mobile.openapi.json
+++ b/api/src/openapi/mobile.openapi.json
@@ -293,6 +293,306 @@
}
}
}
+ },
+ "/v1/host/places/{placeId}/rooms": {
+ "parameters": [
+ {
+ "in": "path",
+ "name": "placeId",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "required": false,
+ "name": "cursor",
+ "description": "Pagination cursor",
+ "allowReserved": true,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Paginated index of Rooms",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "cursor",
+ "results"
+ ],
+ "properties": {
+ "cursor": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/RoomSummary"
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Create a Room",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "position": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bathroom",
+ "Bedroom",
+ "Den",
+ "Kitchen",
+ "LivingRoom"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Room"
+ }
+ }
+ },
+ "description": "Created"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
+ },
+ "/v1/host/places/{placeId}/rooms/{id}": {
+ "parameters": [
+ {
+ "in": "path",
+ "name": "placeId",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Fetch a Room",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Room"
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ },
+ "patch": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Update a Room",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "position": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bathroom",
+ "Bedroom",
+ "Den",
+ "Kitchen",
+ "LivingRoom"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "204": {
+ "description": "Success, no content",
+ "$ref": "#/components/responses/NoContent"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Destroy a Room",
+ "responses": {
+ "204": {
+ "description": "Success, no content",
+ "$ref": "#/components/responses/NoContent"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
}
},
"components": {
@@ -399,6 +699,48 @@
}
}
},
+ "Room": {
+ "type": "object",
+ "required": [
+ "deletedAt",
+ "id",
+ "position",
+ "type"
+ ],
+ "properties": {
+ "deletedAt": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "id": {
+ "type": "string"
+ },
+ "position": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "The following values will be allowed:\n Room"
+ }
+ }
+ },
+ "RoomSummary": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ }
+ },
"ValidationErrors": {
"type": "object",
"required": [
diff --git a/api/src/openapi/openapi.json b/api/src/openapi/openapi.json
index 0462482..0464845 100644
--- a/api/src/openapi/openapi.json
+++ b/api/src/openapi/openapi.json
@@ -293,6 +293,306 @@
}
}
}
+ },
+ "/v1/host/places/{placeId}/rooms": {
+ "parameters": [
+ {
+ "in": "path",
+ "name": "placeId",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "required": false,
+ "name": "cursor",
+ "description": "Pagination cursor",
+ "allowReserved": true,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Paginated index of Rooms",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "cursor",
+ "results"
+ ],
+ "properties": {
+ "cursor": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/RoomSummary"
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Create a Room",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "position": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bathroom",
+ "Bedroom",
+ "Den",
+ "Kitchen",
+ "LivingRoom"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Room"
+ }
+ }
+ },
+ "description": "Created"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
+ },
+ "/v1/host/places/{placeId}/rooms/{id}": {
+ "parameters": [
+ {
+ "in": "path",
+ "name": "placeId",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Fetch a Room",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Room"
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ },
+ "patch": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Update a Room",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "position": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bathroom",
+ "Bedroom",
+ "Den",
+ "Kitchen",
+ "LivingRoom"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "204": {
+ "description": "Success, no content",
+ "$ref": "#/components/responses/NoContent"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Destroy a Room",
+ "responses": {
+ "204": {
+ "description": "Success, no content",
+ "$ref": "#/components/responses/NoContent"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
}
},
"components": {
@@ -407,6 +707,50 @@
}
}
},
+ "Room": {
+ "type": "object",
+ "required": [
+ "deletedAt",
+ "id",
+ "position",
+ "type"
+ ],
+ "properties": {
+ "deletedAt": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "id": {
+ "type": "string"
+ },
+ "position": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Room"
+ ]
+ }
+ }
+ },
+ "RoomSummary": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ }
+ },
"ValidationErrors": {
"type": "object",
"required": [
diff --git a/api/src/openapi/tests.openapi.json b/api/src/openapi/tests.openapi.json
index 692a565..ec35862 100644
--- a/api/src/openapi/tests.openapi.json
+++ b/api/src/openapi/tests.openapi.json
@@ -293,6 +293,306 @@
}
}
}
+ },
+ "/v1/host/places/{placeId}/rooms": {
+ "parameters": [
+ {
+ "in": "path",
+ "name": "placeId",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "required": false,
+ "name": "cursor",
+ "description": "Pagination cursor",
+ "allowReserved": true,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Paginated index of Rooms",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "cursor",
+ "results"
+ ],
+ "properties": {
+ "cursor": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/RoomSummary"
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Create a Room",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "position": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bathroom",
+ "Bedroom",
+ "Den",
+ "Kitchen",
+ "LivingRoom"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Room"
+ }
+ }
+ },
+ "description": "Created"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
+ },
+ "/v1/host/places/{placeId}/rooms/{id}": {
+ "parameters": [
+ {
+ "in": "path",
+ "name": "placeId",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Fetch a Room",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Room"
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ },
+ "patch": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Update a Room",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "position": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bathroom",
+ "Bedroom",
+ "Den",
+ "Kitchen",
+ "LivingRoom"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "204": {
+ "description": "Success, no content",
+ "$ref": "#/components/responses/NoContent"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "rooms"
+ ],
+ "description": "Destroy a Room",
+ "responses": {
+ "204": {
+ "description": "Success, no content",
+ "$ref": "#/components/responses/NoContent"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "422": {
+ "$ref": "#/components/responses/ValidationErrors"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
}
},
"components": {
@@ -407,6 +707,50 @@
}
}
},
+ "Room": {
+ "type": "object",
+ "required": [
+ "deletedAt",
+ "id",
+ "position",
+ "type"
+ ],
+ "properties": {
+ "deletedAt": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "id": {
+ "type": "string"
+ },
+ "position": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Room"
+ ]
+ }
+ }
+ },
+ "RoomSummary": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ }
+ },
"ValidationErrors": {
"type": "object",
"required": [
diff --git a/api/src/types/db.ts b/api/src/types/db.ts
index 6769319..b11f983 100644
--- a/api/src/types/db.ts
+++ b/api/src/types/db.ts
@@ -87,6 +87,21 @@ export const PlaceStylesEnumValues = [
'treehouse',
] as const

+export type RoomTypesEnum =
+ | 'Bathroom'
+ | 'Bedroom'
+ | 'Den'
+ | 'Kitchen'
+ | 'LivingRoom'
+
+export const RoomTypesEnumValues = [
+ 'Bathroom',
+ 'Bedroom',
+ 'Den',
+ 'Kitchen',
+ 'LivingRoom',
+] as const
+
export type Timestamp = ColumnType<DateTime | CalendarDate>

export interface Guests {
@@ -122,6 +137,16 @@ export interface Places {
updatedAt: Timestamp
}

+export interface Rooms {
+ createdAt: Timestamp
+ deletedAt: Timestamp | null
+ id: Generated<string>
+ placeId: string
+ position: number | null
+ type: RoomTypesEnum
+ updatedAt: Timestamp
+}
+
export interface Users {
createdAt: Timestamp
email: string
@@ -134,6 +159,7 @@ export interface DB {
host_places: HostPlaces
hosts: Hosts
places: Places
+ rooms: Rooms
users: Users
}

@@ -142,5 +168,6 @@ export class DBClass {
host_places: HostPlaces
hosts: Hosts
places: Places
+ rooms: Rooms
users: Users
}
diff --git a/api/src/types/dream.globals.ts b/api/src/types/dream.globals.ts
index 538a900..a55e48c 100644
--- a/api/src/types/dream.globals.ts
+++ b/api/src/types/dream.globals.ts
@@ -64,5 +64,7 @@ export const globalTypeConfig = {
'HostSummarySerializer',
'PlaceSerializer',
'PlaceSummarySerializer',
+ 'RoomSerializer',
+ 'RoomSummarySerializer',
],
} as const
diff --git a/api/src/types/dream.ts b/api/src/types/dream.ts
index 6efd28d..a613c27 100644
--- a/api/src/types/dream.ts
+++ b/api/src/types/dream.ts
@@ -57,7 +57,12 @@ us humans, he says:
*/

import { type CalendarDate, type DateTime } from '@rvoh/dream'
-import { type PlaceStylesEnum, PlaceStylesEnumValues } from './db.js'
+import {
+ type PlaceStylesEnum,
+ type RoomTypesEnum,
+ PlaceStylesEnumValues,
+ RoomTypesEnumValues,
+} from './db.js'

export const schema = {
guests: {
@@ -388,6 +393,99 @@ export const schema = {
},
},
},
+ rooms: {
+ serializerKeys: ['default', 'summary'],
+ scopes: {
+ default: [],
+ named: [],
+ },
+ nonJsonColumnNames: [
+ 'createdAt',
+ 'deletedAt',
+ 'id',
+ 'placeId',
+ 'position',
+ 'type',
+ 'updatedAt',
+ ],
+ columns: {
+ createdAt: {
+ coercedType: {} as DateTime,
+ enumType: null,
+ enumArrayType: null,
+ enumValues: null,
+ dbType: 'timestamp without time zone',
+ allowNull: false,
+ isArray: false,
+ },
+ deletedAt: {
+ coercedType: {} as DateTime | null,
+ enumType: null,
+ enumArrayType: null,
+ enumValues: null,
+ dbType: 'timestamp without time zone',
+ allowNull: true,
+ isArray: false,
+ },
+ id: {
+ coercedType: {} as string,
+ enumType: null,
+ enumArrayType: null,
+ enumValues: null,
+ dbType: 'uuid',
+ allowNull: false,
+ isArray: false,
+ },
+ placeId: {
+ coercedType: {} as string,
+ enumType: null,
+ enumArrayType: null,
+ enumValues: null,
+ dbType: 'uuid',
+ allowNull: false,
+ isArray: false,
+ },
+ position: {
+ coercedType: {} as number | null,
+ enumType: null,
+ enumArrayType: null,
+ enumValues: null,
+ dbType: 'integer',
+ allowNull: true,
+ isArray: false,
+ },
+ type: {
+ coercedType: {} as RoomTypesEnum,
+ enumType: {} as RoomTypesEnum,
+ enumArrayType: [] as RoomTypesEnum[],
+ enumValues: RoomTypesEnumValues,
+ dbType: 'room_types_enum',
+ allowNull: false,
+ isArray: false,
+ },
+ updatedAt: {
+ coercedType: {} as DateTime,
+ enumType: null,
+ enumArrayType: null,
+ enumValues: null,
+ dbType: 'timestamp without time zone',
+ allowNull: false,
+ isArray: false,
+ },
+ },
+ virtualColumns: [],
+ associations: {
+ place: {
+ type: 'BelongsTo',
+ foreignKey: 'placeId',
+ foreignKeyTypeColumn: null,
+ tables: ['places'],
+ optional: false,
+ requiredAndClauses: null,
+ passthroughAndClauses: null,
+ },
+ },
+ },
users: {
serializerKeys: [],
scopes: {
@@ -466,6 +564,7 @@ export const connectionTypeConfig = {
Host: 'hosts',
HostPlace: 'host_places',
Place: 'places',
+ Room: 'rooms',
User: 'users',
},
},
diff --git a/api/src/types/openapi/tests.openapi.d.ts b/api/src/types/openapi/tests.openapi.d.ts
index 9818b61..2208354 100644
--- a/api/src/types/openapi/tests.openapi.d.ts
+++ b/api/src/types/openapi/tests.openapi.d.ts
@@ -190,6 +190,205 @@ export interface paths {
};
trace?: never;
};
+ "/v1/host/places/{placeId}/rooms": {
+ parameters: {
+ query?: {
+ /** @description Pagination cursor */
+ cursor?: string | null;
+ };
+ header?: never;
+ path: {
+ placeId: string;
+ };
+ cookie?: never;
+ };
+ /** @description Paginated index of Rooms */
+ get: {
+ parameters: {
+ query?: {
+ /** @description Pagination cursor */
+ cursor?: string | null;
+ };
+ header?: never;
+ path: {
+ placeId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Success */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ cursor: string | null;
+ results: components["schemas"]["RoomSummary"][];
+ };
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ 409: components["responses"]["Conflict"];
+ 422: components["responses"]["ValidationErrors"];
+ 500: components["responses"]["InternalServerError"];
+ };
+ };
+ put?: never;
+ /** @description Create a Room */
+ post: {
+ parameters: {
+ query?: {
+ /** @description Pagination cursor */
+ cursor?: string | null;
+ };
+ header?: never;
+ path: {
+ placeId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ "application/json": {
+ position?: number | null;
+ /** @enum {string} */
+ type?: "Bathroom" | "Bedroom" | "Den" | "Kitchen" | "LivingRoom";
+ };
+ };
+ };
+ responses: {
+ /** @description Created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Room"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ 409: components["responses"]["Conflict"];
+ 422: components["responses"]["ValidationErrors"];
+ 500: components["responses"]["InternalServerError"];
+ };
+ };
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/v1/host/places/{placeId}/rooms/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ placeId: string;
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** @description Fetch a Room */
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ placeId: string;
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Success */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Room"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ 409: components["responses"]["Conflict"];
+ 422: components["responses"]["ValidationErrors"];
+ 500: components["responses"]["InternalServerError"];
+ };
+ };
+ put?: never;
+ post?: never;
+ /** @description Destroy a Room */
+ delete: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ placeId: string;
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Success, no content */
+ 204: components["responses"]["NoContent"];
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ 409: components["responses"]["Conflict"];
+ 422: components["responses"]["ValidationErrors"];
+ 500: components["responses"]["InternalServerError"];
+ };
+ };
+ options?: never;
+ head?: never;
+ /** @description Update a Room */
+ patch: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ placeId: string;
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ "application/json": {
+ position?: number | null;
+ /** @enum {string} */
+ type?: "Bathroom" | "Bedroom" | "Den" | "Kitchen" | "LivingRoom";
+ };
+ };
+ };
+ responses: {
+ /** @description Success, no content */
+ 204: components["responses"]["NoContent"];
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ 409: components["responses"]["Conflict"];
+ 422: components["responses"]["ValidationErrors"];
+ 500: components["responses"]["InternalServerError"];
+ };
+ };
+ trace?: never;
+ };
}
export type webhooks = Record<string, never>;
export interface components {
@@ -220,6 +419,17 @@ export interface components {
id: string;
name: string;
};
+ Room: {
+ /** Format: date-time */
+ deletedAt: string | null;
+ id: string;
+ position: number | null;
+ /** @enum {string} */
+ type: "Room";
+ };
+ RoomSummary: {
+ id: string;
+ };
ValidationErrors: {
/** @enum {string} */
type: "validation";