Compare commits

...

5 Commits

Author SHA1 Message Date
codegen-sh[bot] 5b6e47e1bc Fix validation error message to match existing tests
- Split validation into two separate refine calls for clearer error messages
- First validation ensures one of documentId or collectionId is required for non-folders
- Second validation ensures folders cannot have documentId or collectionId
- This maintains backward compatibility with existing test expectations
2025-09-14 14:40:31 +00:00
codegen-sh[bot] b7957a607d Fix Star model isFolder column definition
- Add explicit allowNull: false and defaultValue: false to isFolder column
- This ensures proper database constraints and prevents test failures
- Fixes failing test-server checks in CI
2025-09-14 14:11:19 +00:00
codegen-sh[bot] fe7182d034 Fix database migration syntax for PostgreSQL
- Quote column names in check constraint to ensure proper PostgreSQL syntax
- Use removeConstraint method instead of raw SQL for rollback consistency
- Ensures folders cannot have documentId or collectionId set
2025-09-14 14:06:57 +00:00
codegen-sh[bot] 8610101377 Add API enhancements for Star folder operations
- Add isFolder and parentId parameters to stars.create endpoint
- Add parentId filtering to stars.list endpoint
- Add validation to ensure parent folders exist and belong to user
- Update starCreator command to handle folder creation
- Add model hook to prevent nested folders (folders inside folders)
- Update API schemas with proper validation for folder constraints

This enables full API support for creating and organizing star folders
while maintaining data integrity and preventing invalid folder structures.
2025-09-14 13:53:12 +00:00
codegen-sh[bot] 07ea5fe84d Add database migration and server model changes for Star folders
- Add parentId and isFolder columns to stars table
- Add self-referential relationship for hierarchical structure
- Add database constraints to ensure folders don't have content
- Add indexes for efficient folder queries
- Update server Star model with new fields and relationships
- Update Star presenter to include new fields in API responses

This enables users to organize their starred items into folders
while maintaining backward compatibility with existing stars.

API-only changes - no client-side modifications included.
2025-09-14 13:49:02 +00:00
6 changed files with 171 additions and 7 deletions
+15 -1
View File
@@ -10,6 +10,10 @@ type Props = {
documentId?: string;
/** The collection to star */
collectionId?: string;
/** The parent folder ID */
parentId?: string;
/** Whether this star is a folder */
isFolder?: boolean;
/** The sorted index for the star in the sidebar If no index is provided then it will be at the end */
index?: string;
/** The request context */
@@ -27,6 +31,8 @@ export default async function starCreator({
user,
documentId,
collectionId,
parentId,
isFolder = false,
ctx,
...rest
}: Props): Promise<Star> {
@@ -54,7 +60,13 @@ export default async function starCreator({
}
const [star] = await Star.findOrCreateWithCtx(ctx, {
where: documentId
where: isFolder
? {
userId: user.id,
parentId,
isFolder: true,
}
: documentId
? {
userId: user.id,
documentId,
@@ -65,6 +77,8 @@ export default async function starCreator({
},
defaults: {
index,
parentId,
isFolder,
},
});
@@ -0,0 +1,64 @@
"use strict";
module.exports = {
up: async (queryInterface, Sequelize) => {
// Add parentId column for hierarchical structure
await queryInterface.addColumn("stars", "parentId", {
type: Sequelize.UUID,
allowNull: true,
references: {
model: "stars",
key: "id",
},
onDelete: "CASCADE",
});
// Add isFolder column to distinguish folders from regular stars
await queryInterface.addColumn("stars", "isFolder", {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
});
// Add index for efficient parent-child queries
await queryInterface.addIndex("stars", ["parentId"], {
name: "stars_parent_id",
});
// Add composite index for user-specific folder queries
await queryInterface.addIndex("stars", ["userId", "parentId"], {
name: "stars_user_id_parent_id",
});
// Add composite index for filtering folders vs regular stars
await queryInterface.addIndex("stars", ["userId", "isFolder"], {
name: "stars_user_id_is_folder",
});
// Add check constraint to ensure folders don't have documentId or collectionId
await queryInterface.sequelize.query(`
ALTER TABLE stars ADD CONSTRAINT stars_folder_content_check
CHECK (
("isFolder" = true AND "documentId" IS NULL AND "collectionId" IS NULL) OR
("isFolder" = false)
)
`);
},
down: async (queryInterface, Sequelize) => {
// Remove check constraint
await queryInterface.removeConstraint(
"stars",
"stars_folder_content_check"
);
// Remove indexes
await queryInterface.removeIndex("stars", "stars_user_id_is_folder");
await queryInterface.removeIndex("stars", "stars_user_id_parent_id");
await queryInterface.removeIndex("stars", "stars_parent_id");
// Remove columns
await queryInterface.removeColumn("stars", "isFolder");
await queryInterface.removeColumn("stars", "parentId");
},
};
+28
View File
@@ -6,6 +6,9 @@ import {
ForeignKey,
Table,
Length,
HasMany,
BeforeCreate,
BeforeUpdate,
} from "sequelize-typescript";
import Collection from "./Collection";
import Document from "./Document";
@@ -26,6 +29,9 @@ class Star extends IdModel<
@Column
index: string | null;
@Column({ type: DataType.BOOLEAN, allowNull: false, defaultValue: false })
isFolder: boolean;
// associations
@BelongsTo(() => User, "userId")
@@ -48,6 +54,28 @@ class Star extends IdModel<
@ForeignKey(() => Collection)
@Column(DataType.UUID)
collectionId: string | null;
@BelongsTo(() => Star, "parentId")
parent: Star | null;
@ForeignKey(() => Star)
@Column(DataType.UUID)
parentId: string | null;
@HasMany(() => Star, "parentId")
children: Star[];
@BeforeCreate
@BeforeUpdate
static async validateFolderNesting(star: Star) {
// Prevent folders from being nested inside other folders
if (star.isFolder && star.parentId) {
const parent = await Star.findByPk(star.parentId);
if (parent?.isFolder) {
throw new Error("Folders cannot be nested inside other folders");
}
}
}
}
export default Star;
+2
View File
@@ -5,6 +5,8 @@ export default function presentStar(star: Star) {
id: star.id,
documentId: star.documentId,
collectionId: star.collectionId,
parentId: star.parentId,
isFolder: star.isFolder,
index: star.index,
createdAt: star.createdAt,
updatedAt: star.updatedAt,
+32 -2
View File
@@ -13,6 +13,8 @@ export const StarsCreateSchema = BaseSchema.extend({
})
.optional(),
collectionId: z.string().uuid().optional(),
parentId: z.string().uuid().optional(),
isFolder: z.boolean().optional(),
index: z
.string()
.regex(ValidateIndex.regex, {
@@ -21,16 +23,44 @@ export const StarsCreateSchema = BaseSchema.extend({
.optional(),
})
.refine(
(body) => !(isEmpty(body.documentId) && isEmpty(body.collectionId)),
(body) => {
// If isFolder is true, documentId and collectionId must be empty
if (body.isFolder) {
return isEmpty(body.documentId) && isEmpty(body.collectionId);
}
// If not a folder, one of documentId or collectionId is required
return !(isEmpty(body.documentId) && isEmpty(body.collectionId));
},
{
message: "One of documentId or collectionId is required",
}
)
.refine(
(body) => {
// Additional validation: folders cannot have documentId or collectionId
if (
body.isFolder &&
(!isEmpty(body.documentId) || !isEmpty(body.collectionId))
) {
return false;
}
return true;
},
{
message: "Folders cannot have documentId or collectionId",
}
),
});
export type StarsCreateReq = z.infer<typeof StarsCreateSchema>;
export const StarsListSchema = BaseSchema;
export const StarsListSchema = BaseSchema.extend({
body: z
.object({
parentId: z.string().uuid().optional(),
})
.optional(),
});
export type StarsListReq = z.infer<typeof StarsListSchema>;
+30 -4
View File
@@ -25,7 +25,7 @@ router.post(
transaction(),
async (ctx: APIContext<T.StarsCreateReq>) => {
const { transaction } = ctx.state;
const { documentId, collectionId, index } = ctx.input.body;
const { documentId, collectionId, parentId, isFolder, index } = ctx.input.body;
const { user } = ctx.state.auth;
if (documentId) {
@@ -44,11 +44,29 @@ router.post(
authorize(user, "star", collection);
}
// Validate parent folder exists and belongs to user
if (parentId) {
const parentStar = await Star.findOne({
where: {
id: parentId,
userId: user.id,
isFolder: true,
},
transaction,
});
if (!parentStar) {
ctx.throw(400, "Parent folder not found or is not a folder");
}
}
const star = await starCreator({
ctx,
user,
documentId,
collectionId,
parentId,
isFolder: isFolder || false,
index,
});
@@ -66,12 +84,20 @@ router.post(
validate(T.StarsListSchema),
async (ctx: APIContext<T.StarsListReq>) => {
const { user } = ctx.state.auth;
const { parentId } = ctx.input.body || {};
const whereClause: any = {
userId: user.id,
};
// Filter by parentId if provided
if (parentId !== undefined) {
whereClause.parentId = parentId;
}
const [stars, collectionIds] = await Promise.all([
Star.findAll({
where: {
userId: user.id,
},
where: whereClause,
order: [
Sequelize.literal('"star"."index" collate "C"'),
["updatedAt", "DESC"],