feat: implement comments feature (#171)

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-27 00:37:52 +08:00
committed by GitHub
parent 37825d1def
commit cb40fe74d0
116 changed files with 6637 additions and 1129 deletions

View File

@@ -0,0 +1,36 @@
CREATE TYPE "public"."comment_status" AS ENUM('pending', 'approved', 'rejected', 'hidden');--> statement-breakpoint
CREATE TABLE "comment_reaction" (
"id" text PRIMARY KEY NOT NULL,
"tenant_id" text NOT NULL,
"comment_id" text NOT NULL,
"user_id" text NOT NULL,
"reaction" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "uq_comment_reaction_user" UNIQUE("tenant_id","comment_id","user_id","reaction")
);
--> statement-breakpoint
CREATE TABLE "comment" (
"id" text PRIMARY KEY NOT NULL,
"tenant_id" text NOT NULL,
"photo_id" text NOT NULL,
"user_id" text NOT NULL,
"parent_id" text,
"content" text NOT NULL,
"status" "comment_status" DEFAULT 'approved' NOT NULL,
"user_agent" text,
"client_ip" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"deleted_at" timestamp
);
--> statement-breakpoint
ALTER TABLE "photo_asset" ALTER COLUMN "manifest_version" SET DEFAULT 'v9';--> statement-breakpoint
ALTER TABLE "comment_reaction" ADD CONSTRAINT "comment_reaction_tenant_id_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "comment_reaction" ADD CONSTRAINT "comment_reaction_comment_id_comment_id_fk" FOREIGN KEY ("comment_id") REFERENCES "public"."comment"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "comment_reaction" ADD CONSTRAINT "comment_reaction_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "comment" ADD CONSTRAINT "comment_tenant_id_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "comment" ADD CONSTRAINT "comment_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "idx_comment_reaction_comment" ON "comment_reaction" USING btree ("tenant_id","comment_id");--> statement-breakpoint
CREATE INDEX "idx_comment_tenant_photo" ON "comment" USING btree ("tenant_id","photo_id");--> statement-breakpoint
CREATE INDEX "idx_comment_parent" ON "comment" USING btree ("parent_id");--> statement-breakpoint
CREATE INDEX "idx_comment_user" ON "comment" USING btree ("user_id");

File diff suppressed because it is too large Load Diff

View File

@@ -71,6 +71,13 @@
"when": 1764070049538,
"tag": "0009_stormy_sway",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1764154685207,
"tag": "0010_wise_doorman",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,7 @@
import type { PhotoManifestItem } from '@afilmory/builder'
import type { ManifestVersion } from '@afilmory/builder/manifest/version'
import { CURRENT_MANIFEST_VERSION } from '@afilmory/builder/manifest/version'
import type { ManifestVersion } from '@afilmory/builder/manifest/version.js'
import { CURRENT_MANIFEST_VERSION } from '@afilmory/builder/manifest/version.ts'
import { relations } from 'drizzle-orm'
import {
bigint,
boolean,
@@ -31,6 +32,7 @@ export const userRoleEnum = pgEnum('user_role', ['user', 'admin', 'superadmin'])
export const tenantStatusEnum = pgEnum('tenant_status', ['active', 'inactive', 'suspended'])
export const tenantDomainStatusEnum = pgEnum('tenant_domain_status', ['pending', 'verified', 'disabled'])
export const photoSyncStatusEnum = pgEnum('photo_sync_status', ['pending', 'synced', 'conflict'])
export const commentStatusEnum = pgEnum('comment_status', ['pending', 'approved', 'rejected', 'hidden'])
export const CURRENT_PHOTO_MANIFEST_VERSION: ManifestVersion = CURRENT_MANIFEST_VERSION
export type PhotoAssetConflictType = 'missing-in-storage' | 'metadata-mismatch' | 'photo-id-conflict'
@@ -228,6 +230,63 @@ export const reactions = pgTable(
(t) => [index('idx_reactions_tenant_ref_key').on(t.tenantId, t.refKey)],
)
export const comments = pgTable(
'comment',
{
id: snowflakeId,
tenantId: text('tenant_id')
.notNull()
.references(() => tenants.id, { onDelete: 'cascade' }),
photoId: text('photo_id').notNull(),
userId: text('user_id')
.notNull()
.references(() => authUsers.id, { onDelete: 'cascade' }),
parentId: text('parent_id'),
content: text('content').notNull(),
status: commentStatusEnum('status').notNull().default('approved'),
userAgent: text('user_agent'),
clientIp: text('client_ip'),
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(),
deletedAt: timestamp('deleted_at', { mode: 'string' }),
},
(t) => [
index('idx_comment_tenant_photo').on(t.tenantId, t.photoId),
index('idx_comment_parent').on(t.parentId),
index('idx_comment_user').on(t.userId),
],
)
export const commentsRelations = relations(comments, ({ one, many }) => ({
parent: one(comments, {
fields: [comments.parentId],
references: [comments.id],
}),
children: many(comments),
}))
export const commentReactions = pgTable(
'comment_reaction',
{
id: snowflakeId,
tenantId: text('tenant_id')
.notNull()
.references(() => tenants.id, { onDelete: 'cascade' }),
commentId: text('comment_id')
.notNull()
.references(() => comments.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => authUsers.id, { onDelete: 'cascade' }),
reaction: text('reaction').notNull(),
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
},
(t) => [
unique('uq_comment_reaction_user').on(t.tenantId, t.commentId, t.userId, t.reaction),
index('idx_comment_reaction_comment').on(t.tenantId, t.commentId),
],
)
export const managedStorageUsages = pgTable(
'managed_storage_usage',
{
@@ -411,6 +470,8 @@ export const dbSchema = {
settings,
systemSettings,
reactions,
comments,
commentReactions,
managedStorageUsages,
managedStorageFileReferences,
photoAssets,

View File

@@ -14,6 +14,7 @@
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowImportingTsExtensions": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,

View File

@@ -30,7 +30,7 @@ export type LoggerContextProvider = () => LoggerContextValue | LoggerContextValu
const globalContextProviders = new Set<LoggerContextProvider>()
function toContextStrings (value: LoggerContextValue | LoggerContextValue[] | undefined): string[] {
function toContextStrings(value: LoggerContextValue | LoggerContextValue[] | undefined): string[] {
if (Array.isArray(value)) {
return value.flatMap((item) => (typeof item === 'string' && item ? [item] : []))
}
@@ -38,7 +38,7 @@ function toContextStrings (value: LoggerContextValue | LoggerContextValue[] | un
return typeof value === 'string' && value ? [value] : []
}
function invokeContextProvider (provider: LoggerContextProvider): string[] {
function invokeContextProvider(provider: LoggerContextProvider): string[] {
try {
return toContextStrings(provider())
} catch {