diff --git a/apps/blog-next/config/app.ts b/apps/blog-next/config/app.ts index 3b239c6..23ff776 100644 --- a/apps/blog-next/config/app.ts +++ b/apps/blog-next/config/app.ts @@ -20,6 +20,6 @@ export default defineAppConfig({ jobs: 'server/jobs', events: 'server/events', listeners: 'server/listeners', - generatedSchema: 'server/db/schema.generated.ts', + generatedSchema: '.holo-js/generated/schema.generated.ts', }, }) diff --git a/apps/blog-next/server/db/migrations/2026_05_02_115334_create_comments_table.ts b/apps/blog-next/server/db/migrations/2026_05_02_115334_create_comments_table.ts new file mode 100644 index 0000000..cb02e19 --- /dev/null +++ b/apps/blog-next/server/db/migrations/2026_05_02_115334_create_comments_table.ts @@ -0,0 +1,19 @@ +import { defineMigration, type MigrationContext } from '@holo-js/db' + +export default defineMigration({ + async up({ schema }: MigrationContext) { + await schema.createTable('comments', (table) => { + table.id() + table.integer('post_id') + table.index(['post_id']) + table.integer('user_id') + table.index(['user_id']) + table.text('body') + table.string('status').default('pending') + table.timestamps() + }) + }, + async down({ schema }: MigrationContext) { + await schema.dropTable('comments') + }, +}) diff --git a/apps/blog-next/server/db/schema.generated.ts b/apps/blog-next/server/db/schema.generated.ts deleted file mode 100644 index d68ec72..0000000 --- a/apps/blog-next/server/db/schema.generated.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { column, defineGeneratedTable, registerGeneratedTables } from '@holo-js/db' - -export const holoMigrations = defineGeneratedTable("_holo_migrations", { - "id": column.id(), - "name": column.string().unique(), - "batch": column.integer(), - "migrated_at": column.timestamp().defaultNow(), -}) - -export const authIdentities = defineGeneratedTable("auth_identities", { - "id": column.id(), - "user_id": column.string(), - "guard": column.string().default("web"), - "auth_provider": column.string().default("users"), - "provider": column.string(), - "provider_user_id": column.string(), - "email": column.string().nullable(), - "email_verified": column.boolean().default(false), - "profile": column.json().default({}), - "tokens": column.json().default({}), - "created_at": column.timestamp().defaultNow(), - "updated_at": column.timestamp().defaultNow(), -}, { indexes: [{ columns: ["user_id"], unique: false }, { columns: ["provider","provider_user_id"], unique: true, name: "auth_identities_provider_user_unique" }] }) - -export const categories = defineGeneratedTable("categories", { - "id": column.id(), - "name": column.string(), - "slug": column.string().unique(), - "description": column.text().nullable(), - "created_at": column.timestamp().defaultNow(), - "updated_at": column.timestamp().defaultNow(), -}) - -export const emailVerificationTokens = defineGeneratedTable("email_verification_tokens", { - "id": column.uuid().primaryKey(), - "provider": column.string().default("users"), - "user_id": column.string(), - "email": column.string(), - "token_hash": column.string(), - "expires_at": column.timestamp(), - "used_at": column.timestamp().nullable(), - "created_at": column.timestamp().defaultNow(), - "updated_at": column.timestamp().defaultNow(), -}, { indexes: [{ columns: ["provider"], unique: false }, { columns: ["user_id"], unique: false }, { columns: ["email"], unique: false }] }) - -export const notifications = defineGeneratedTable("notifications", { - "id": column.string().primaryKey(), - "type": column.string().nullable(), - "notifiable_type": column.string(), - "notifiable_id": column.string(), - "data": column.json().default({}), - "read_at": column.timestamp().nullable(), - "created_at": column.timestamp(), - "updated_at": column.timestamp(), -}, { indexes: [{ columns: ["notifiable_type","notifiable_id"], unique: false }, { columns: ["read_at"], unique: false }] }) - -export const passwordResetTokens = defineGeneratedTable("password_reset_tokens", { - "id": column.uuid().primaryKey(), - "provider": column.string().default("users"), - "email": column.string(), - "token_hash": column.string(), - "expires_at": column.timestamp(), - "used_at": column.timestamp().nullable(), - "created_at": column.timestamp().defaultNow(), - "updated_at": column.timestamp().defaultNow(), -}, { indexes: [{ columns: ["provider"], unique: false }, { columns: ["email"], unique: false }] }) - -export const personalAccessTokens = defineGeneratedTable("personal_access_tokens", { - "id": column.uuid().primaryKey(), - "provider": column.string().default("users"), - "user_id": column.string(), - "name": column.string(), - "token_hash": column.string().unique(), - "abilities": column.json().default([]), - "last_used_at": column.timestamp().nullable(), - "expires_at": column.timestamp().nullable(), - "created_at": column.timestamp().defaultNow(), - "updated_at": column.timestamp().defaultNow(), -}, { indexes: [{ columns: ["provider"], unique: false }, { columns: ["user_id"], unique: false }] }) - -export const postTags = defineGeneratedTable("post_tags", { - "post_id": column.integer(), - "tag_id": column.integer(), - "created_at": column.timestamp().defaultNow(), - "updated_at": column.timestamp().defaultNow(), -}) - -export const posts = defineGeneratedTable("posts", { - "id": column.id(), - "user_id": column.integer(), - "category_id": column.integer().nullable(), - "title": column.string(), - "slug": column.string().unique(), - "status": column.string().default("draft"), - "excerpt": column.text().nullable(), - "body": column.text(), - "published_at": column.timestamp().nullable(), - "created_at": column.timestamp().defaultNow(), - "updated_at": column.timestamp().defaultNow(), -}) - -export const sessions = defineGeneratedTable("sessions", { - "id": column.string().primaryKey(), - "store": column.string().default("database"), - "data": column.json().default({}), - "created_at": column.timestamp(), - "last_activity_at": column.timestamp(), - "expires_at": column.timestamp(), - "invalidated_at": column.timestamp().nullable(), - "remember_token_hash": column.string().nullable(), -}, { indexes: [{ columns: ["expires_at"], unique: false }] }) - -export const tags = defineGeneratedTable("tags", { - "id": column.id(), - "name": column.string(), - "slug": column.string().unique(), - "created_at": column.timestamp().defaultNow(), - "updated_at": column.timestamp().defaultNow(), -}) - -export const users = defineGeneratedTable("users", { - "id": column.id(), - "name": column.string(), - "email": column.string().unique(), - "password": column.string().nullable(), - "avatar": column.string().nullable(), - "email_verified_at": column.timestamp().nullable(), - "created_at": column.timestamp().defaultNow(), - "updated_at": column.timestamp().defaultNow(), -}) - -declare module '@holo-js/db' { - interface GeneratedSchemaTables { - "_holo_migrations": typeof holoMigrations - "auth_identities": typeof authIdentities - "categories": typeof categories - "email_verification_tokens": typeof emailVerificationTokens - "notifications": typeof notifications - "password_reset_tokens": typeof passwordResetTokens - "personal_access_tokens": typeof personalAccessTokens - "post_tags": typeof postTags - "posts": typeof posts - "sessions": typeof sessions - "tags": typeof tags - "users": typeof users - } -} - -export const tables = { "_holo_migrations": holoMigrations, "auth_identities": authIdentities, "categories": categories, "email_verification_tokens": emailVerificationTokens, "notifications": notifications, "password_reset_tokens": passwordResetTokens, "personal_access_tokens": personalAccessTokens, "post_tags": postTags, "posts": posts, "sessions": sessions, "tags": tags, "users": users } as const - -registerGeneratedTables(tables) diff --git a/apps/blog-next/server/holo-models.d.ts b/apps/blog-next/server/holo-models.d.ts new file mode 100644 index 0000000..7ffa216 --- /dev/null +++ b/apps/blog-next/server/holo-models.d.ts @@ -0,0 +1,57 @@ +import type { + BelongsToManyRelationDefinition, + BelongsToRelationDefinition, + EmptyScopeMap, + GeneratedSchemaTable, + HasManyRelationDefinition, + ModelReference, + RelationMap, +} from '@holo-js/db' + +type CategoryTable = GeneratedSchemaTable<'categories'> +type CommentTable = GeneratedSchemaTable<'comments'> +type PostTable = GeneratedSchemaTable<'posts'> +type PostTagTable = GeneratedSchemaTable<'post_tags'> +type TagTable = GeneratedSchemaTable<'tags'> +type UserTable = GeneratedSchemaTable<'users'> + +interface CategoryRelations extends RelationMap { + readonly posts: HasManyRelationDefinition +} + +interface CommentRelations extends RelationMap { + readonly post: BelongsToRelationDefinition + readonly user: BelongsToRelationDefinition +} + +interface PostRelations extends RelationMap { + readonly user: BelongsToRelationDefinition + readonly category: BelongsToRelationDefinition + readonly tags: BelongsToManyRelationDefinition + readonly comments: HasManyRelationDefinition +} + +interface TagRelations extends RelationMap { + readonly posts: BelongsToManyRelationDefinition +} + +interface UserRelations extends RelationMap { + readonly posts: HasManyRelationDefinition + readonly comments: HasManyRelationDefinition +} + +type CategoryModel = ModelReference +type CommentModel = ModelReference +type PostModel = ModelReference +type TagModel = ModelReference +type UserModel = ModelReference + +declare module '@holo-js/db' { + interface RegisteredModels { + Category: CategoryModel + Comment: CommentModel + Post: PostModel + Tag: TagModel + User: UserModel + } +} diff --git a/apps/blog-next/server/models/Category.ts b/apps/blog-next/server/models/Category.ts index dcdb862..801ab60 100644 --- a/apps/blog-next/server/models/Category.ts +++ b/apps/blog-next/server/models/Category.ts @@ -1,5 +1,8 @@ -import { defineModel } from '@holo-js/db' +import { defineModel, hasMany } from '@holo-js/db' export default defineModel('categories', { fillable: ['name', 'slug', 'description'], + relations: { + posts: hasMany('Post', { foreignKey: 'category_id' }), + }, }) diff --git a/apps/blog-next/server/models/Comment.ts b/apps/blog-next/server/models/Comment.ts new file mode 100644 index 0000000..2165791 --- /dev/null +++ b/apps/blog-next/server/models/Comment.ts @@ -0,0 +1,9 @@ +import { belongsTo, defineModel } from '@holo-js/db' + +export default defineModel('comments', { + fillable: ['post_id', 'user_id', 'body'], + relations: { + post: belongsTo('Post', { foreignKey: 'post_id' }), + user: belongsTo('User', { foreignKey: 'user_id' }), + }, +}) diff --git a/apps/blog-next/server/models/Post.ts b/apps/blog-next/server/models/Post.ts index a2dd591..b241784 100644 --- a/apps/blog-next/server/models/Post.ts +++ b/apps/blog-next/server/models/Post.ts @@ -1,15 +1,14 @@ -import { belongsTo, belongsToMany, defineModel } from '@holo-js/db' - -import Category from './Category' -import Tag from './Tag' +import { belongsTo, belongsToMany, defineModel, hasMany } from '@holo-js/db' const relations = { - category: belongsTo(() => Category, { foreignKey: 'category_id' }), - tags: belongsToMany(() => Tag, { + user: belongsTo('User', { foreignKey: 'user_id' }), + category: belongsTo('Category', { foreignKey: 'category_id' }), + tags: belongsToMany('Tag', { pivotTable: 'post_tags', foreignPivotKey: 'post_id', relatedPivotKey: 'tag_id', }), + comments: hasMany('Comment', { foreignKey: 'post_id' }), } export default defineModel('posts', { diff --git a/apps/blog-next/server/models/Tag.ts b/apps/blog-next/server/models/Tag.ts index 442fb85..2840e98 100644 --- a/apps/blog-next/server/models/Tag.ts +++ b/apps/blog-next/server/models/Tag.ts @@ -1,5 +1,12 @@ -import { defineModel } from '@holo-js/db' +import { belongsToMany, defineModel } from '@holo-js/db' export default defineModel('tags', { fillable: ['name', 'slug'], + relations: { + posts: belongsToMany('Post', { + pivotTable: 'post_tags', + foreignPivotKey: 'tag_id', + relatedPivotKey: 'post_id', + }), + }, }) diff --git a/apps/blog-next/server/models/User.ts b/apps/blog-next/server/models/User.ts index 3f4ff6a..2de4b8e 100644 --- a/apps/blog-next/server/models/User.ts +++ b/apps/blog-next/server/models/User.ts @@ -1,6 +1,10 @@ -import { defineModel } from '@holo-js/db' +import { defineModel, hasMany } from '@holo-js/db' export default defineModel('users', { fillable: ['name', 'email', 'password', 'avatar'], hidden: ['password'], + relations: { + posts: hasMany('Post', { foreignKey: 'user_id' }), + comments: hasMany('Comment', { foreignKey: 'user_id' }), + }, }) diff --git a/apps/blog-next/tests/blog-logic.mjs b/apps/blog-next/tests/blog-logic.mjs index f11b8fe..a9ebfca 100644 --- a/apps/blog-next/tests/blog-logic.mjs +++ b/apps/blog-next/tests/blog-logic.mjs @@ -5,6 +5,7 @@ import { initializeHoloAdapterProject } from '@holo-js/core' import Category from '../server/models/Category.ts' import Post from '../server/models/Post.ts' import Tag from '../server/models/Tag.ts' +import User from '../server/models/User.ts' import { createCategory, createPost, @@ -37,6 +38,13 @@ try { assert.equal(home.categories.length, 2) assert.equal(home.tags.length, 3) + const firstUser = await User.with('posts').first() + assert.ok(firstUser) + assert.ok(Array.isArray(firstUser.posts)) + + const featuredPost = await Post.with('user').where('slug', home.featured?.slug ?? '').first() + assert.ok(featuredPost?.user) + const dashboard = await getAdminDashboardData() assert.equal(dashboard.postCount, 2) assert.equal(dashboard.publishedCount, 2) diff --git a/apps/blog-nuxt/config/app.ts b/apps/blog-nuxt/config/app.ts index f60afbe..0b576b2 100644 --- a/apps/blog-nuxt/config/app.ts +++ b/apps/blog-nuxt/config/app.ts @@ -20,6 +20,6 @@ export default defineAppConfig({ jobs: 'server/jobs', events: 'server/events', listeners: 'server/listeners', - generatedSchema: 'server/db/schema.generated.ts', + generatedSchema: '.holo-js/generated/schema.generated.ts', }, }) diff --git a/apps/blog-nuxt/pages/admin/categories/index.vue b/apps/blog-nuxt/pages/admin/categories/index.vue index ae578ef..3d9bd09 100644 --- a/apps/blog-nuxt/pages/admin/categories/index.vue +++ b/apps/blog-nuxt/pages/admin/categories/index.vue @@ -1,5 +1,7 @@