User HasOne Guest
Commit Message
User HasOne Guest
Every User automatically creates a Guest
Each User can have at most one Guest (unique index on user_id foreign key)
Sync association types
Run migration:
```console
pnpm psy db:migrate
Run model specs:
pnpm uspec spec/unit/models/user.spec.ts
pnpm uspec spec/unit/models
If migrations had already been run, after adding
a new association, one could run sync instead
(db:migrate runs sync implicitly):
pnpm psy sync
## Changes
```diff
diff --git a/api/.prettierignore b/api/.prettierignore
index f4ef007..9687f36 100644
--- a/api/.prettierignore
+++ b/api/.prettierignore
@@ -9,3 +9,7 @@ dist/
.yarn/
.yarnrc.yml
pnpm-lock.yaml
+
+# only adding so that diffs are clearer for demo app
+src/app/serializers/**/*.ts
+src/db/migrations/*.ts
diff --git a/api/spec/unit/models/User.spec.ts b/api/spec/unit/models/User.spec.ts
index 06dac32..3bdfe80 100644
--- a/api/spec/unit/models/User.spec.ts
+++ b/api/spec/unit/models/User.spec.ts
@@ -1,3 +1,14 @@
+import Guest from '@models/Guest.js'
+import createUser from '@spec/factories/UserFactory.js'
+
describe('User', () => {
- it.todo('add a test here to get started building User')
+ context('upon creation', () => {
+ it('creates a guest for this user and brings it into scope on the newly created user', async () => {
+ const user = await createUser()
+
+ expect(user.guest instanceof Guest).toBe(true)
+ const guest = await user.associationQuery('guest').first()
+ expect(guest instanceof Guest).toBe(true)
+ })
+ })
})
diff --git a/api/src/app/models/User.ts b/api/src/app/models/User.ts
index 0c415a7..09fd61c 100644
--- a/api/src/app/models/User.ts
+++ b/api/src/app/models/User.ts
@@ -1,6 +1,7 @@
+import ApplicationModel from '@models/ApplicationModel.js'
import { Decorators } from '@rvoh/dream'
import { DreamColumn } from '@rvoh/dream/types'
-import ApplicationModel from '@models/ApplicationModel.js'
+import Guest from './Guest.js'
const deco = new Decorators<typeof User>()
@@ -13,4 +14,12 @@ export default class User extends ApplicationModel {
public email: DreamColumn<User, 'email'>
public createdAt: DreamColumn<User, 'createdAt'>
public updatedAt: DreamColumn<User, 'updatedAt'>
+
+ @deco.AfterCreate()
+ public async createGuest(this: User) {
+ this.guest = await this.createAssociation('guest')
+ }
+
+ @deco.HasOne('Guest')
+ public guest: Guest
}
diff --git a/api/src/db/migrations/1765410597252-create-guest.ts b/api/src/db/migrations/1765410597252-create-guest.ts
index 5e39ada..4293a81 100644
--- a/api/src/db/migrations/1765410597252-create-guest.ts
+++ b/api/src/db/migrations/1765410597252-create-guest.ts
@@ -9,7 +9,8 @@ export async function up(db: Kysely<any>): Promise<void> {
.primaryKey()
.defaultTo(sql`uuidv7()`),
)
- .addColumn('user_id', 'uuid', col => col.references('users.id').onDelete('restrict').notNull())
+ // added .unique() to `user_id`
+ .addColumn('user_id', 'uuid', col => col.references('users.id').onDelete('restrict').notNull().unique())
.addColumn('created_at', 'timestamp', col => col.notNull())
.addColumn('updated_at', 'timestamp', col => col.notNull())
.execute()
diff --git a/api/src/types/db.ts b/api/src/types/db.ts
index 9d90506..e236e7c 100644
--- a/api/src/types/db.ts
+++ b/api/src/types/db.ts
@@ -70,6 +70,13 @@ export type Generated<T> =
export type Timestamp = ColumnType<DateTime | CalendarDate>
+export interface Guests {
+ createdAt: Timestamp
+ id: Generated<string>
+ updatedAt: Timestamp
+ userId: string
+}
+
export interface Users {
createdAt: Timestamp
email: string
@@ -78,9 +85,11 @@ export interface Users {
}
export interface DB {
+ guests: Guests
users: Users
}
export class DBClass {
+ guests: Guests
users: Users
}
diff --git a/api/src/types/dream.globals.ts b/api/src/types/dream.globals.ts
index 901c67b..6c9f10f 100644
--- a/api/src/types/dream.globals.ts
+++ b/api/src/types/dream.globals.ts
@@ -57,5 +57,5 @@ us humans, he says:
*/
export const globalTypeConfig = {
- serializers: [],
+ serializers: ['GuestSerializer', 'GuestSummarySerializer'],
} as const
diff --git a/api/src/types/dream.ts b/api/src/types/dream.ts
index b6fde7f..28bcc2f 100644
--- a/api/src/types/dream.ts
+++ b/api/src/types/dream.ts
@@ -60,6 +60,64 @@ import { type CalendarDate, type DateTime } from '@rvoh/dream'
import {} from './db.js'
export const schema = {
+ guests: {
+ serializerKeys: ['default', 'summary'],
+ scopes: {
+ default: [],
+ named: [],
+ },
+ nonJsonColumnNames: ['createdAt', 'id', 'updatedAt', 'userId'],
+ columns: {
+ createdAt: {
+ coercedType: {} as DateTime,
+ enumType: null,
+ enumArrayType: null,
+ enumValues: null,
+ dbType: 'timestamp without time zone',
+ allowNull: false,
+ isArray: false,
+ },
+ id: {
+ coercedType: {} as string,
+ enumType: null,
+ enumArrayType: null,
+ enumValues: null,
+ dbType: 'uuid',
+ allowNull: false,
+ isArray: false,
+ },
+ updatedAt: {
+ coercedType: {} as DateTime,
+ enumType: null,
+ enumArrayType: null,
+ enumValues: null,
+ dbType: 'timestamp without time zone',
+ allowNull: false,
+ isArray: false,
+ },
+ userId: {
+ coercedType: {} as string,
+ enumType: null,
+ enumArrayType: null,
+ enumValues: null,
+ dbType: 'uuid',
+ allowNull: false,
+ isArray: false,
+ },
+ },
+ virtualColumns: [],
+ associations: {
+ user: {
+ type: 'BelongsTo',
+ foreignKey: 'userId',
+ foreignKeyTypeColumn: null,
+ tables: ['users'],
+ optional: false,
+ requiredAndClauses: null,
+ passthroughAndClauses: null,
+ },
+ },
+ },
users: {
serializerKeys: [],
scopes: {
@@ -106,7 +164,17 @@ export const schema = {
},
},
virtualColumns: [],
- associations: {},
+ associations: {
+ guest: {
+ type: 'HasOne',
+ foreignKey: 'userId',
+ foreignKeyTypeColumn: null,
+ tables: ['guests'],
+ optional: null,
+ requiredAndClauses: null,
+ passthroughAndClauses: null,
+ },
+ },
},
} as const
@@ -115,6 +183,7 @@ export const connectionTypeConfig = {
allDefaultScopeNames: [],
globalNames: {
models: {
+ Guest: 'guests',
User: 'users',
},
},