Skip to main content

PlacesController specs passing

Commit Message​

PlacesController specs passing

NOTE: if, after running `pnpm psy sync`, your editor starts
showing errors like `Unsafe call of a(n) 'error' type typed value`,
restart the ESLint server (the `ESLint: Restart ESLint Server`
command in VSCode / Cursor)

```console
pnpm uspec spec/unit/controllers/V1/Host/PlacesController.spec.ts
# if it hangs, it's likely that you haven't implemented the controller
# changes; simply adding `this.ok()` or `this.noContent()` to each action
# in the controller will get the spec run to complete
```

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

Changes​

diff --git a/api/spec/unit/controllers/V1/Host/PlacesController.spec.ts b/api/spec/unit/controllers/V1/Host/PlacesController.spec.ts
index 363f0b2..800cd7e 100644
--- a/api/spec/unit/controllers/V1/Host/PlacesController.spec.ts
+++ b/api/spec/unit/controllers/V1/Host/PlacesController.spec.ts
@@ -1,6 +1,7 @@
import Place from '#models/Place.js'
import User from '#models/User.js'
import Host from '#models/Host.js'
+import createHostPlace from '#spec/factories/HostPlaceFactory.js'
import createPlace from '#spec/factories/PlaceFactory.js'
import createUser from '#spec/factories/UserFactory.js'
import createHost from '#spec/factories/HostFactory.js'
@@ -23,7 +24,8 @@ describe('V1/Host/PlacesController', () => {
}

it('returns the index of Places', async () => {
- const place = await createPlace({ host })
+ const place = await createPlace()
+ await createHostPlace({ host, place })

const { body } = await index(200)

@@ -53,7 +55,8 @@ describe('V1/Host/PlacesController', () => {
}

it('returns the specified Place', async () => {
- const place = await createPlace({ host })
+ const place = await createPlace()
+ await createHostPlace({ host, place })

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

@@ -122,7 +125,8 @@ describe('V1/Host/PlacesController', () => {
}

it('updates the Place', async () => {
- const place = await createPlace({ host })
+ const place = await createPlace()
+ await createHostPlace({ host, place })

await update(place, {
name: 'Updated Place name',
@@ -165,7 +169,8 @@ describe('V1/Host/PlacesController', () => {
}

it('deletes the Place', async () => {
- const place = await createPlace({ host })
+ const place = await createPlace()
+ await createHostPlace({ host, place })

await destroy(place, 204)

diff --git a/api/src/app/controllers/AuthedController.ts b/api/src/app/controllers/AuthedController.ts
index ee7bac1..b4a9a22 100644
--- a/api/src/app/controllers/AuthedController.ts
+++ b/api/src/app/controllers/AuthedController.ts
@@ -1,18 +1,15 @@
import ApplicationController from '#controllers/ApplicationController.js'
import resolveCurrentUser from '#controllers/helpers/resolveCurrentUser.js'
+import User from '#models/User.js'
import { BeforeAction } from '@rvoh/psychic'
-/** uncomment after creating User model */
-// import User from '#models/User.js'

export default class AuthedController extends ApplicationController {
- /** uncomment after creating User model */
- // protected currentUser: User
+ protected currentUser: User

@BeforeAction()
protected async authenticate() {
const user = await resolveCurrentUser(this)
if (!user) return this.unauthorized()
- /** uncomment after creating User model */
- // this.currentUser = user
+ this.currentUser = user
}
}
diff --git a/api/src/app/controllers/V1/Host/BaseController.ts b/api/src/app/controllers/V1/Host/BaseController.ts
index 1a200d0..c71d4a1 100644
--- a/api/src/app/controllers/V1/Host/BaseController.ts
+++ b/api/src/app/controllers/V1/Host/BaseController.ts
@@ -1,5 +1,15 @@
+import Host from '#models/Host.js'
+import { BeforeAction } from '@rvoh/psychic'
import V1BaseController from '../BaseController.js'

export default class V1HostBaseController extends V1BaseController {
+ protected currentHost: Host

+ @BeforeAction()
+ protected async loadCurrentHost() {
+ const host = await this.currentUser.associationQuery('host').first()
+ if (!host) return this.forbidden()
+
+ this.currentHost = host
+ }
}
diff --git a/api/src/app/controllers/V1/Host/PlacesController.ts b/api/src/app/controllers/V1/Host/PlacesController.ts
index 432bbff..5240341 100644
--- a/api/src/app/controllers/V1/Host/PlacesController.ts
+++ b/api/src/app/controllers/V1/Host/PlacesController.ts
@@ -1,7 +1,9 @@
import { OpenAPI } from '@rvoh/psychic'
import { DreamParamSafeColumnNames } from '@rvoh/dream/types'
-import V1HostBaseController from './BaseController.js'
+import ApplicationModel from '#models/ApplicationModel.js'
+import HostPlace from '#models/HostPlace.js'
import Place from '#models/Place.js'
+import V1HostBaseController from './BaseController.js'

const openApiTags = ['places']

@@ -17,10 +19,11 @@ export default class V1HostPlacesController extends V1HostBaseController {
fastJsonStringify: true,
})
public async index() {
- // const places = await this.currentHost.associationQuery('places')
- // .preloadFor('summary')
- // .cursorPaginate({ cursor: this.castParam('cursor', 'string', { allowNull: true }) })
- // this.ok(places)
+ const places = await this.currentHost
+ .associationQuery('places')
+ .preloadFor('summary')
+ .cursorPaginate({ cursor: this.castParam('cursor', 'string', { allowNull: true }) })
+ this.ok(places)
}

@OpenAPI(Place, {
@@ -30,8 +33,8 @@ export default class V1HostPlacesController extends V1HostBaseController {
fastJsonStringify: true,
})
public async show() {
- // const place = await this.place()
- // this.ok(place)
+ const place = await this.place()
+ this.ok(place)
}

@OpenAPI(Place, {
@@ -44,9 +47,14 @@ export default class V1HostPlacesController extends V1HostBaseController {
},
})
public async create() {
- // let place = await this.currentHost.createAssociation('places', this.extractParams(Place, paramSafeColumns))
- // if (place.isPersisted) place = await place.loadFor('default').execute()
- // this.created(place)
+ let place = await ApplicationModel.transaction(async txn => {
+ const place = await Place.txn(txn).create(this.extractParams(Place, paramSafeColumns))
+ await HostPlace.txn(txn).create({ host: this.currentHost, place })
+ return place
+ })
+
+ if (place.isPersisted) place = await place.loadFor('default').execute()
+ this.created(place)
}

@OpenAPI(Place, {
@@ -59,9 +67,9 @@ export default class V1HostPlacesController extends V1HostBaseController {
},
})
public async update() {
- // const place = await this.place()
- // await place.update(this.extractParams(Place, paramSafeColumns))
- // this.noContent()
+ const place = await this.place()
+ await place.update(this.extractParams(Place, paramSafeColumns))
+ this.noContent()
}

@OpenAPI({
@@ -71,14 +79,15 @@ export default class V1HostPlacesController extends V1HostBaseController {
fastJsonStringify: true,
})
public async destroy() {
- // const place = await this.place()
- // await place.destroy()
- // this.noContent()
+ const place = await this.place()
+ await place.destroy()
+ this.noContent()
}

private async place() {
- // return await this.currentHost.associationQuery('places')
- // .preloadFor('default')
- // .findOrFail(this.castParam('id', 'uuid'))
+ return await this.currentHost
+ .associationQuery('places')
+ .preloadFor('default')
+ .findOrFail(this.castParam('id', 'uuid'))
}
}
diff --git a/api/src/app/controllers/helpers/resolveCurrentUser.ts b/api/src/app/controllers/helpers/resolveCurrentUser.ts
index c285e0f..4c6553a 100644
--- a/api/src/app/controllers/helpers/resolveCurrentUser.ts
+++ b/api/src/app/controllers/helpers/resolveCurrentUser.ts
@@ -1,14 +1,10 @@
import AppEnv from '#conf/AppEnv.js'
import { DecryptionError, DecryptionRotationError } from '@rvoh/dream/errors'
+import User from '#models/User.js'
import { Encrypt } from '@rvoh/dream/utils'
import { PsychicController } from '@rvoh/psychic'
-/** uncomment after creating User model */
-// import User from '#models/User.js'

-// eslint-disable-next-line @typescript-eslint/require-await
-export default async function resolveCurrentUser(controller: PsychicController): Promise<string | null> {
- /** replace previous line with uncommented next line after creating User model */
- // export default async function resolveCurrentUser(controller: PsychicController): Promise<User | null> {
+export default async function resolveCurrentUser(controller: PsychicController): Promise<User | null> {
if (!AppEnv.isTest)
throw new Error(
'The current authentication scheme is only for early development. Replace with a production grade authentication scheme.',
@@ -44,7 +40,5 @@ export default async function resolveCurrentUser(controller: PsychicController):
const userId = payload.userId
if (!userId) return null

- /** uncomment after creating User model */
- // return await User.find(userId)
- return userId
+ return await User.find(userId)
}