Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6486be7
refactor: remove updated_at field from post schema and related defini…
mikan3rd Jan 7, 2026
5bf2beb
feat: enhance post creation and logging functionality
mikan3rd Jan 7, 2026
ca3606d
feat: include post ID in logging during post creation
mikan3rd Jan 7, 2026
a9ae53f
feat: add public_id to user creation and tests
mikan3rd Jan 7, 2026
95ee2af
feat: update post schema and tests for user_id handling
mikan3rd Jan 7, 2026
03c240b
fix: remove FIXME comments for public_id generation in user tests and…
mikan3rd Jan 7, 2026
4207aa1
refactor: standardize public_id generation in user and post tests
mikan3rd Jan 7, 2026
55d631a
refactor: update post schema to remove updated_at field and adjust te…
mikan3rd Jan 7, 2026
943db31
refactor: standardize public_id generation in post tests
mikan3rd Jan 7, 2026
f6655c1
feat: log post details upon update in post_logs table
mikan3rd Jan 7, 2026
044e5c8
test: integrate post_logs verification in post tests
mikan3rd Jan 7, 2026
d7f7d2e
refactor: streamline public_id generation in post creation logic
mikan3rd Jan 8, 2026
2a055a9
feat: add updated_at field to post schema and tests
mikan3rd Jan 8, 2026
4d41a98
feat: add updated_at field to post schema and update required fields
mikan3rd Jan 8, 2026
7370668
fix: include updated_at field in post response object
mikan3rd Jan 8, 2026
6da46b7
fix: update updated_at field in post tests to null
mikan3rd Jan 8, 2026
63e04b6
fix: update updated_at field to allow null values in post schema and …
mikan3rd Jan 8, 2026
f65031b
fix: set updated_at field to null in PostCard mock data
mikan3rd Jan 8, 2026
b4c2109
feat: introduce first_created_at field in post schema and update rela…
mikan3rd Jan 17, 2026
e8bb6cf
refactor: update post schema and mock data to include first_created_a…
mikan3rd Jan 17, 2026
1c7e9bb
refactor: enhance post schema by consolidating properties and improvi…
mikan3rd Jan 17, 2026
0132741
refactor: update post schema and response handling to include updated…
mikan3rd Jan 18, 2026
61afb2e
refactor: streamline post response handling with transformation funct…
mikan3rd Jan 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions packages/backend_app/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,17 @@
"created_at": {
"type": "string",
"format": "date-time",
"description": "The date and time the post was created",
"description": "The date and time the post was originally created",
"example": "2025-01-01T00:00:00Z"
},
"updated_at": {
"type": "string",
"format": "date-time",
"description": "The date and time the post was updated",
"description": "The date and time the post was created (or last updated in case of delete&insert)",
"example": "2025-01-01T00:00:00Z"
}
},
"required": ["public_id", "content", "created_at", "updated_at"],
"additionalProperties": false
"required": ["public_id", "content", "created_at", "updated_at"]
}
},
"parameters": {}
Expand Down Expand Up @@ -399,8 +398,7 @@
"properties": {
"user": { "$ref": "#/components/schemas/user" }
},
"required": ["user"],
"additionalProperties": false
"required": ["user"]
}
]
}
Expand Down Expand Up @@ -582,8 +580,7 @@
"properties": {
"user": { "$ref": "#/components/schemas/user" }
},
"required": ["user"],
"additionalProperties": false
"required": ["user"]
}
]
}
Expand Down Expand Up @@ -752,7 +749,8 @@
"description": "The content of the post",
"example": "test"
}
}
},
"required": ["content"]
}
}
}
Expand All @@ -773,8 +771,7 @@
"properties": {
"user": { "$ref": "#/components/schemas/user" }
},
"required": ["user"],
"additionalProperties": false
"required": ["user"]
}
]
}
Expand Down
78 changes: 51 additions & 27 deletions packages/backend_app/src/apps/posts/dto.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,69 @@
import { z } from "@hono/zod-openapi";
import { postPublicFields } from "../../db/field";
import { postsTable } from "../../db/schema";
import {
createInsertSchema,
createSelectSchema,
createUpdateSchema,
} from "../factory";
import { createInsertSchema, createUpdateSchema } from "../factory";
import { userSelectSchema } from "../user/dto";

const postSelectSchema = createSelectSchema(postsTable, {
public_id: (schema) =>
schema.openapi({
export const postSchema = z
.object({
public_id: z.uuid().openapi({
description: "Public ID",
example: "123e4567-e89b-12d3-a456-426614174000",
}),
content: (schema) =>
schema.openapi({
content: z.string().openapi({
description: "The content of the post",
example: "test",
}),
created_at: (schema) =>
schema.openapi({
description: "The date and time the post was created",
created_at: z.iso.datetime().openapi({
description: "The date and time the post was originally created",
example: "2025-01-01T00:00:00Z",
format: "date-time",
}),
updated_at: (schema) =>
schema.openapi({
description: "The date and time the post was updated",
updated_at: z.iso.datetime().openapi({
description:
"The date and time the post was created (or last updated in case of delete&insert)",
example: "2025-01-01T00:00:00Z",
format: "date-time",
}),
})
.pick(postPublicFields)
.strict()
})
.openapi("post");

export const postWithUserSelectSchema = postSelectSchema.extend({
user: userSelectSchema,
// DB データから API レスポンス形式への変換関数
export const transformPost = (post: {
public_id: string;
content: string;
first_created_at: Date;
created_at: Date;
}): z.infer<typeof postSchema> => ({
public_id: post.public_id,
content: post.content,
created_at: post.first_created_at.toISOString(), // first_created_at → created_at
updated_at: post.created_at.toISOString(), // created_at → updated_at
});
Comment on lines +6 to 41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the file exists and examine its content
if [ -f "packages/backend_app/src/apps/posts/dto.ts" ]; then
  wc -l packages/backend_app/src/apps/posts/dto.ts
  echo "---"
  cat -n packages/backend_app/src/apps/posts/dto.ts
else
  echo "File not found. Searching for dto files..."
  find . -name "dto.ts" -type f 2>/dev/null | grep -i post
fi

Repository: mikan3rd/hono-next-example

Length of output: 3473


🏁 Script executed:

#!/bin/bash
# Check if zod supports nullable chaining
# Also search for any tests or usages of transformPost
rg "transformPost" --type ts --type js -A 3 -B 3

Repository: mikan3rd/hono-next-example

Length of output: 3431


🏁 Script executed:

#!/bin/bash
# Look for database schema or models that define the post structure
fd -t f -e ts -e js | xargs grep -l "first_created_at\|created_at" 2>/dev/null | head -20

Repository: mikan3rd/hono-next-example

Length of output: 378


🏁 Script executed:

#!/bin/bash
# Check the database schema to understand first_created_at vs created_at
cat -n packages/backend_app/src/db/schema.ts | grep -A 30 "postsTable\|first_created_at\|created_at"

Repository: mikan3rd/hono-next-example

Length of output: 1160


🏁 Script executed:

#!/bin/bash
# Check test file to understand expected behavior
wc -l packages/backend_app/src/apps/posts/index.test.ts
head -100 packages/backend_app/src/apps/posts/index.test.ts

Repository: mikan3rd/hono-next-example

Length of output: 3348


🏁 Script executed:

#!/bin/bash
# Check if created_at is ever explicitly set to first_created_at or if timestamps differ in practice
rg "created_at" packages/backend_app/src/apps/posts/ -A 2 -B 2 | head -40

Repository: mikan3rd/hono-next-example

Length of output: 2687


🏁 Script executed:

#!/bin/bash
# Check the zod version and confirm nullable() support
cat packages/backend_app/package.json | grep -A 5 '"@hono/zod-openapi"'

Repository: mikan3rd/hono-next-example

Length of output: 263


updated_at always populated; consider nullable to distinguish unedited posts.

The API currently always sets updated_at to a value, making it impossible to signal whether a post has been edited. The database design supports this distinction via first_created_at (immutable) vs created_at (updates on delete&insert pattern), but the API doesn't expose it. If the intent is to indicate "never updated," make updated_at nullable in the schema and conditionally null it when first_created_at === created_at. Note: exact timestamp comparison may be unreliable due to microsecond differences; consider row timestamp precision or an explicit edit flag if strict equality checking is needed.

🤖 Prompt for AI Agents
In `@packages/backend_app/src/apps/posts/dto.ts` around lines 6 - 41, postSchema
currently requires updated_at and transformPost always sets it, preventing
signaling "never updated"; make updated_at nullable in postSchema (allow null in
the OpenAPI schema) and update transformPost to return updated_at as null when
the post was never edited by comparing timestamps (use millisecond-safe
comparison like first_created_at.getTime() === created_at.getTime()) otherwise
return the ISO string; reference postSchema and transformPost when making these
changes and ensure the OpenAPI metadata reflects nullability.


// user 付きの変換関数
export const transformPostWithUser = <
T extends {
public_id: string;
content: string;
first_created_at: Date;
created_at: Date;
user: z.infer<typeof userSelectSchema>;
},
>(
post: T,
): z.infer<typeof postWithUserResponseSchema> => ({
...transformPost(post),
user: post.user,
});

export const postWithUserResponseSchema = postSchema.and(
z.object({
user: userSelectSchema,
}),
);

export const getPostsResponseSchema = z.object({
posts: postWithUserSelectSchema.array(),
posts: postWithUserResponseSchema.array(),
});

export const postPostRequestSchema = createInsertSchema(postsTable, {
Expand All @@ -55,7 +77,7 @@ export const postPostRequestSchema = createInsertSchema(postsTable, {
});

export const postPostResponseSchema = z.object({
post: postWithUserSelectSchema,
post: postWithUserResponseSchema,
});

export const updatePostParamsSchema = z.object({
Expand All @@ -68,9 +90,11 @@ export const updatePostRequestSchema = createUpdateSchema(postsTable, {
description: "The content of the post",
example: "test",
}),
}).pick({
content: true,
});
})
.pick({
content: true,
})
.required();

export const updatePostResponseSchema = postPostResponseSchema;

Expand Down
102 changes: 76 additions & 26 deletions packages/backend_app/src/apps/posts/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ClientRequestOptions } from "hono/client";
import { testClient } from "hono/testing";
import { app } from "../../apps";
import { db } from "../../db";
import { postsTable, usersTable } from "../../db/schema";
import { postLogsTable, postsTable, usersTable } from "../../db/schema";
import { supabaseUid } from "../../test/supabase";

describe("postsApp", () => {
Expand All @@ -21,6 +21,7 @@ describe("postsApp", () => {
await db
.insert(usersTable)
.values({
public_id: faker.string.uuid(),
supabase_uid: supabaseUid,
display_name: faker.person.fullName(),
})
Expand All @@ -33,6 +34,7 @@ describe("postsApp", () => {
await db
.insert(usersTable)
.values({
public_id: faker.string.uuid(),
supabase_uid: faker.string.uuid(),
display_name: faker.person.fullName(),
})
Expand All @@ -58,12 +60,19 @@ describe("postsApp", () => {

describe("when there are some posts", () => {
beforeEach(async () => {
await db
.insert(postsTable)
.values({ user_id: user.id, content: "test" });
await db
.insert(postsTable)
.values({ user_id: user.id, content: "test2" });
const now = new Date();
await db.insert(postsTable).values({
public_id: faker.string.uuid(),
user_id: user.id,
content: "test",
first_created_at: now,
});
await db.insert(postsTable).values({
public_id: faker.string.uuid(),
user_id: user.id,
content: "test2",
first_created_at: now,
});
});

it("should return 200 Response", async () => {
Expand All @@ -76,8 +85,8 @@ describe("postsApp", () => {
expect(json.posts[0]).toEqual({
public_id: expect.any(String),
content: "test2",
created_at: expect.any(String),
updated_at: expect.any(String),
created_at: expect.any(String), // first_created_at → created_at に変換
updated_at: expect.any(String), // created_at → updated_at に変換
user: {
public_id: user.public_id,
display_name: user.display_name,
Expand All @@ -86,8 +95,8 @@ describe("postsApp", () => {
expect(json.posts[1]).toEqual({
public_id: expect.any(String),
content: "test",
created_at: expect.any(String),
updated_at: expect.any(String),
created_at: expect.any(String), // first_created_at → created_at に変換
updated_at: expect.any(String), // created_at → updated_at に変換
user: {
public_id: user.public_id,
display_name: user.display_name,
Expand Down Expand Up @@ -133,7 +142,18 @@ describe("postsApp", () => {
const post = posts[0];
if (!post) throw new Error("post is not found");
expect(post.content).toBe(content);
expect(post.created_at).toEqual(post.updated_at);

const postLogs = await db.select().from(postLogsTable);
expect(postLogs).toHaveLength(1);
const postLog = postLogs[0];
if (!postLog) throw new Error("postLog is not found");
expect(postLog.public_id).toBe(post.public_id);
expect(postLog.user_id).toBe(post.user_id);
expect(postLog.content).toBe(content);

// first_created_atが正しく設定されていることを確認
expect(post.first_created_at).toBeDefined();
expect(post.first_created_at).toBeInstanceOf(Date);
});
});

Expand Down Expand Up @@ -189,10 +209,21 @@ describe("postsApp", () => {
});

describe("when post is found", () => {
let firstPost: typeof postsTable.$inferSelect;

beforeEach(async () => {
await db
const result = await db
.insert(postsTable)
.values({ public_id, user_id: user.id, content: "test" });
.values({
public_id,
user_id: user.id,
content: "test",
first_created_at: new Date(),
})
.returning();
const post = result[0];
if (!post) throw new Error("post is not found");
firstPost = post;
});

it("should return 200 Response", async () => {
Expand All @@ -209,17 +240,30 @@ describe("postsApp", () => {
const post = posts[0];
if (!post) throw new Error("post is not found");
expect(post.content).toBe(content);
expect(post.created_at.getTime()).toBeLessThan(
post.updated_at.getTime(),
);
expect(post.public_id).toBe(public_id);

// first_created_atが維持されていることを確認
expect(post.first_created_at).toEqual(firstPost.first_created_at);

const postLogs = await db.select().from(postLogsTable);
expect(postLogs).toHaveLength(1);
const postLog = postLogs[0];
if (!postLog) throw new Error("postLog is not found");
expect(postLog.id).toBe(post.id);
expect(postLog.public_id).toBe(post.public_id);
expect(postLog.user_id).toBe(post.user_id);
expect(postLog.content).toBe(content);
});
});

describe("when post user is not the same as the current user", () => {
beforeEach(async () => {
await db
.insert(postsTable)
.values({ public_id, user_id: anotherUser.id, content: "test" });
await db.insert(postsTable).values({
public_id,
user_id: anotherUser.id,
content: "test",
first_created_at: new Date(),
});
});

it("should return 403 Response", async () => {
Expand Down Expand Up @@ -289,9 +333,12 @@ describe("postsApp", () => {

describe("when post is found", () => {
beforeEach(async () => {
await db
.insert(postsTable)
.values({ public_id, user_id: user.id, content: "test" });
await db.insert(postsTable).values({
public_id,
user_id: user.id,
content: "test",
first_created_at: new Date(),
});
});

it("should return 200 Response", async () => {
Expand All @@ -305,9 +352,12 @@ describe("postsApp", () => {

describe("when post user is not the same as the current user", () => {
beforeEach(async () => {
await db
.insert(postsTable)
.values({ public_id, user_id: anotherUser.id, content: "test" });
await db.insert(postsTable).values({
public_id,
user_id: anotherUser.id,
content: "test",
first_created_at: new Date(),
});
});

it("should return 403 Response", async () => {
Expand Down
Loading