Skip to content

Conversation

sosweetham
Copy link
Member

@sosweetham sosweetham commented Sep 2, 2025

Description of change

adds marketplace to metastate

Issue Number

n/a

Type of change

  • New (a change which implements a new feature)

How the change has been tested

n/a

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features
    • Introduces a full Marketplace web app with home browsing, search and category filters, app detail pages with screenshots and reviews, and a 404 page.
    • Adds an Admin Dashboard with secure login and CRUD for apps, including logo uploads and status management.
    • Provides light/dark theming, responsive UI components, pagination, toasts, dialogs, and a protected admin route.
  • Documentation
    • Adds comprehensive README covering architecture, tech stack, and deployment.
  • Chores
    • Sets up server, database schema, storage, and routing.
    • Adds build tooling, Tailwind theming, and project configuration.

@sosweetham sosweetham requested a review from coodos as a code owner September 2, 2025 11:17
Copy link
Contributor

coderabbitai bot commented Sep 2, 2025

Walkthrough

Adds a complete “marketplace” platform: full client app (React/Vite/Tailwind), shared schema, server (Express/Passport/Drizzle/Neon), object storage (GCS) with ACLs, admin dashboard with CRUD and uploads, public pages, extensive UI component library, React Query setup, auth context, build tooling, and configs.

Changes

Cohort / File(s) Summary
Project setup & config
platforms/marketplace/package.json, platforms/marketplace/tsconfig.json, platforms/marketplace/tailwind.config.ts, platforms/marketplace/postcss.config.js, platforms/marketplace/vite.config.ts, platforms/marketplace/drizzle.config.ts, platforms/marketplace/components.json, platforms/marketplace/.gitignore, platforms/marketplace/README.md
Introduces package manifest, TS/Tailwind/PostCSS/Vite/Drizzle configs, shadcn settings, ignore rules, and documentation.
Client entry & shell
platforms/marketplace/client/index.html, .../client/src/main.tsx, .../client/src/App.tsx, .../client/src/index.css
Adds SPA entry HTML, React bootstrap, app router/providers, and theme/token CSS.
Routing helpers & data layer
.../client/src/lib/queryClient.ts, .../client/src/lib/protected-route.tsx, .../client/src/lib/utils.ts
Establishes React Query client/api helpers, a ProtectedRoute for admin gating, and a className utility.
Auth & UX hooks
.../client/src/hooks/use-auth.tsx, .../client/src/hooks/use-toast.ts, .../client/src/hooks/use-mobile.tsx
Adds auth context with login/logout/register, in-memory toast system, and mobile viewport hook.
UI primitives (Radix/shadcn)
.../client/src/components/ui/* (accordion, alert, alert-dialog, aspect-ratio, avatar, badge, breadcrumb, button, calendar, card, checkbox, collapsible, command, context-menu, dialog, drawer, dropdown-menu, form, hover-card, input, input-otp, label, menubar, navigation-menu, pagination, popover, progress, radio-group, resizable, scroll-area, select, separator, sheet, skeleton, slider, switch, table, tabs, textarea, toast, toaster, toggle, toggle-group, tooltip)
Adds a comprehensive suite of styled, typed UI components wrapping Radix/cmdk/Recharts/Embla utilities for consistent UX.
Media uploader
.../client/src/components/ObjectUploader.tsx
Adds Uppy-based modal uploader with S3-compatible PUT flow and completion callback.
Pages
.../client/src/pages/home-page.tsx, .../client/src/pages/app-detail.tsx, .../client/src/pages/admin-dashboard.tsx, .../client/src/pages/auth-page.tsx, .../client/src/pages/not-found.tsx
Implements Home, App Detail (with reviews), Admin Dashboard (CRUD, uploads, pagination), Auth, and 404 pages.
Shared schema
platforms/marketplace/shared/schema.ts
Defines Drizzle ORM schema (users, apps, reviews), relations, and insert schemas/types.
Server core
platforms/marketplace/server/index.ts, .../server/routes.ts, .../server/auth.ts, .../server/db.ts, .../server/storage.ts, .../server/objectStorage.ts, .../server/objectAcl.ts, .../server/vite.ts
Adds Express server with auth (Passport, sessions), DB (Neon/Drizzle), REST routes (public/admin), object storage (GCS) with ACL policies, dev Vite integration, and static serving.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Admin as Admin User
  participant UI as Client (Admin Dashboard)
  participant API as Server (Express)
  participant DB as DB (Drizzle/Neon)

  Admin->>UI: Open /admin
  UI->>API: GET /api/user
  API-->>UI: { user, isAdmin }
  alt not admin
    UI->>UI: Redirect to /admin/auth
    Admin->>UI: Submit login (email, password)
    UI->>API: POST /api/login
    API->>DB: Verify user (scrypt)
    DB-->>API: User
    API-->>UI: 200 OK (session)
    UI->>API: GET /api/user
    API-->>UI: { user, isAdmin: true }
  end
  UI->>API: GET /api/admin/apps
  API->>DB: Select apps
  DB-->>API: Apps
  API-->>UI: Apps list
Loading
sequenceDiagram
  autonumber
  actor Admin as Admin User
  participant UI as Client (Admin Dashboard)
  participant API as Server
  participant OSS as ObjectStorageService
  participant GCS as GCS

  Admin->>UI: Click "Upload Logo"
  UI->>API: POST /api/objects/upload
  API->>OSS: getObjectEntityUploadURL()
  OSS->>GCS: Sign URL (PUT)
  GCS-->>OSS: Signed URL
  OSS-->>API: URL
  API-->>UI: { url }
  UI->>GCS: PUT file to signed URL
  UI->>API: PUT /api/objects/finalize { fileURL }
  API->>OSS: trySetObjectEntityAclPolicy(OWNER+PUBLIC_READ)
  OSS->>GCS: Set metadata ACL
  OSS-->>API: objectPath
  API-->>UI: { objectPath }
  UI->>API: POST /api/admin/apps (logoUrl=objectPath)
  API->>DB: Insert app
  DB-->>API: App
  API-->>UI: 201 Created
Loading
sequenceDiagram
  autonumber
  actor User as Visitor
  participant UI as Client (App Detail)
  participant API as Server
  participant DB as DB

  User->>UI: Open /app/:id
  UI->>API: GET /api/apps/:id
  API->>DB: Fetch app
  DB-->>API: App
  API-->>UI: App
  UI->>API: GET /api/apps/:id/reviews
  API->>DB: Fetch reviews
  DB-->>API: Reviews
  API-->>UI: Reviews
  User->>UI: Submit review
  UI->>API: POST /api/apps/:id/reviews
  API->>DB: Insert review + update average
  DB-->>API: OK
  API-->>UI: 201 Created
  UI->>API: GET /api/apps/:id/reviews (invalidate)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

Suggested labels

enhancement

Poem

A rabbit taps keys with a whiskery grin,
New stalls in the market, let trading begin!
Carousels spin, charts sparkle in light,
Admins upload logos by moon-tinted night.
Auth gates are sturdy, reviews hop in queue—
Ship it! Thump-thump—this burrow’s brand new. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/marketplace

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 65

<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove maximum-scale=1 for accessibility (disables zoom).

This blocks pinch-zoom and can violate WCAG expectations.

-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
🤖 Prompt for AI Agents
In platforms/marketplace/client/index.html around line 5, the meta viewport tag
includes maximum-scale=1 which disables pinch-zoom and harms accessibility;
remove maximum-scale=1 (and any user-scalable=no) from the content attribute so
the viewport allows zooming, leaving something like width=device-width,
initial-scale=1.0 (and optionally add a comment explaining why zoom must remain
enabled).

@@ -0,0 +1,67 @@
import { useState } from "react";
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add useEffect for proper Uppy lifecycle management.

You create an Uppy instance but never close it on unmount, leaking listeners/state across mounts.

-import { useState } from "react";
+import { useState, useEffect } from "react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useState } from "react";
import { useState, useEffect } from "react";
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ObjectUploader.tsx around line 1,
the component creates an Uppy instance but never tears it down, leaking
listeners and state across mounts; wrap Uppy creation inside a useEffect (or
create it once with useRef and initialize in useEffect) and in the effect's
cleanup call uppy.close() (and null out any state/ref if used), ensure the
effect has an appropriate dependency array so an Uppy instance is only created
once per component lifecycle, and update any setState usage to happen inside the
effect after creating Uppy.

Comment on lines +34 to +51
const [uppy] = useState(() =>
new Uppy({
restrictions: {
maxNumberOfFiles,
maxFileSize,
allowedFileTypes: ['image/*'],
},
autoProceed: false,
})
.use(AwsS3, {
shouldUseMultipart: false,
getUploadParameters: onGetUploadParameters,
})
.on("complete", (result) => {
onComplete?.(result);
setShowModal(false);
})
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Close Uppy on unmount and surface upload errors.

Ensure cleanup and basic error propagation; prevents memory leaks and silent failures.

   const [uppy] = useState(() =>
     new Uppy({
       restrictions: {
         maxNumberOfFiles,
         maxFileSize,
         allowedFileTypes: ['image/*'],
       },
       autoProceed: false,
     })
       .use(AwsS3, {
-        shouldUseMultipart: false,
         getUploadParameters: onGetUploadParameters,
       })
       .on("complete", (result) => {
         onComplete?.(result);
         setShowModal(false);
       })
+      .on("upload-error", (_file, error) => {
+        console.error("Upload failed:", error);
+      })
   );
+
+  useEffect(() => {
+    return () => {
+      uppy.close();
+    };
+  }, [uppy]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [uppy] = useState(() =>
new Uppy({
restrictions: {
maxNumberOfFiles,
maxFileSize,
allowedFileTypes: ['image/*'],
},
autoProceed: false,
})
.use(AwsS3, {
shouldUseMultipart: false,
getUploadParameters: onGetUploadParameters,
})
.on("complete", (result) => {
onComplete?.(result);
setShowModal(false);
})
);
const [uppy] = useState(() =>
new Uppy({
restrictions: {
maxNumberOfFiles,
maxFileSize,
allowedFileTypes: ['image/*'],
},
autoProceed: false,
})
.use(AwsS3, {
getUploadParameters: onGetUploadParameters,
})
.on("complete", (result) => {
onComplete?.(result);
setShowModal(false);
})
.on("upload-error", (_file, error) => {
console.error("Upload failed:", error);
})
);
useEffect(() => {
return () => {
uppy.close();
};
}, [uppy]);
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ObjectUploader.tsx around lines
34 to 51, the Uppy instance is created but not cleaned up and upload errors
aren't surfaced; add a useEffect that registers an "error" event handler on uppy
to call an onError callback (or at least console.error) and returns a cleanup
function that removes the handler and closes/destroys the uppy instance
(uppy.close() or uppy.destroy()) on unmount to prevent memory leaks and surface
upload failures to the parent.

Comment on lines +30 to +34
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use forwardRef for ref support and interop with parent components

Forwarding refs is standard for UI primitives (focus, measurements, animations). Add displayName for DevTools.

Apply:

-function Badge({ className, variant, ...props }: BadgeProps) {
-  return (
-    <div className={cn(badgeVariants({ variant }), className)} {...props} />
-  )
-}
+const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
+  ({ className, variant, ...props }, ref) => (
+    <div ref={ref} className={cn(badgeVariants({ variant }), className)} {...props} />
+  )
+)
+Badge.displayName = "Badge"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
({ className, variant, ...props }, ref) => (
<div ref={ref} className={cn(badgeVariants({ variant }), className)} {...props} />
)
)
Badge.displayName = "Badge"
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ui/badge.tsx around lines 30-34,
the Badge component does not forward refs or set a displayName; update it to use
React.forwardRef so it accepts a ref parameter and forwards it to the root div
(e.g., forwardRef<HTMLDivElement, BadgeProps>((props, ref) => { ... })) and pass
ref into the div along with existing props, then set Badge.displayName = "Badge"
for DevTools; ensure the component export remains the same and adjust any type
imports to include ForwardRefRenderFunction/ForwardedRef types as needed.

const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Fix className merge and default button type (prevents lost styles and accidental form submits).

  • Passing className inside buttonVariants({ ... }) is ignored by CVA, so consumer styles never apply.
  • Also default the native button type to "button" when not using asChild.
-const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
-  ({ className, variant, size, asChild = false, ...props }, ref) => {
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
+  ({ className, variant, size, asChild = false, type, ...props }, ref) => {
     const Comp = asChild ? Slot : "button"
     return (
       <Comp
-        className={cn(buttonVariants({ variant, size, className }))}
+        className={cn(buttonVariants({ variant, size }), className)}
         ref={ref}
+        {...(!asChild && { type: type ?? "button" })}
         {...props}
       />
     )
   }
 )

Also applies to: 42-53

🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ui/button.tsx around lines 42 to
53 (specifically line 47), the component currently passes className into
buttonVariants (CVA) which ignores it and causes consumer styles to be lost;
also the native button type isn't defaulted causing accidental form submits.
Remove className from the object passed to buttonVariants, call buttonVariants
with only variant/size/etc and then merge the result with the incoming className
using cn(...), and set the rendered native button's default prop type="button"
when asChild is false and no explicit type prop was provided (preserve provided
type when present). Ensure the final className passed to the element is the
merged value and that asChild handling remains unchanged.

Comment on lines +117 to +136
async updateAppRating(appId: string): Promise<void> {
const result = await db
.select({
avgRating: sql<string>`COALESCE(ROUND(AVG(${reviews.rating}::numeric), 2), 0)::text`,
count: sql<number>`COUNT(*)`,
})
.from(reviews)
.where(eq(reviews.appId, appId));

const { avgRating, count } = result[0];

await db
.update(apps)
.set({
averageRating: avgRating || "0.00",
totalReviews: count || 0,
updatedAt: new Date(),
})
.where(eq(apps.id, appId));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Cast COUNT(*) to int to avoid bigint-as-string pitfalls

Postgres returns COUNT(*) as int8, which pg often surfaces as string. Cast to int to ensure totalReviews receives a number.

-      .select({
-        avgRating: sql<string>`COALESCE(ROUND(AVG(${reviews.rating}::numeric), 2), 0)::text`,
-        count: sql<number>`COUNT(*)`,
-      })
+      .select({
+        avgRating: sql<string>`COALESCE(ROUND(AVG(${reviews.rating}::numeric), 2), 0)::text`,
+        count: sql<number>`COUNT(*)::int`,
+      })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async updateAppRating(appId: string): Promise<void> {
const result = await db
.select({
avgRating: sql<string>`COALESCE(ROUND(AVG(${reviews.rating}::numeric), 2), 0)::text`,
count: sql<number>`COUNT(*)`,
})
.from(reviews)
.where(eq(reviews.appId, appId));
const { avgRating, count } = result[0];
await db
.update(apps)
.set({
averageRating: avgRating || "0.00",
totalReviews: count || 0,
updatedAt: new Date(),
})
.where(eq(apps.id, appId));
}
async updateAppRating(appId: string): Promise<void> {
const result = await db
.select({
avgRating: sql<string>`COALESCE(ROUND(AVG(${reviews.rating}::numeric), 2), 0)::text`,
count: sql<number>`COUNT(*)::int`,
})
.from(reviews)
.where(eq(reviews.appId, appId));
const { avgRating, count } = result[0];
await db
.update(apps)
.set({
averageRating: avgRating || "0.00",
totalReviews: count || 0,
updatedAt: new Date(),
})
.where(eq(apps.id, appId));
}
🤖 Prompt for AI Agents
In platforms/marketplace/server/storage.ts around lines 117 to 136, the SQL
selection uses COUNT(*) which Postgres returns as int8 (bigint) and may be
surfaced as a string; change the COUNT expression to cast to int (e.g.,
CAST(COUNT(*) AS int) or COUNT(*)::int) and update the sql type annotation to
sql<number> so totalReviews is returned as a number, ensuring the subsequent
update uses a numeric count rather than a string.

@@ -0,0 +1,96 @@
import type { Config } from "tailwindcss";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

ESM/CJS mismatch: replace require(...) with ESM imports

This file is ESM (export default … satisfies Config). Using require(...) will fail (“require is not defined”). Import the plugins and reference them directly.

-import type { Config } from "tailwindcss";
+import type { Config } from "tailwindcss";
+import animate from "tailwindcss-animate";
+import typography from "@tailwindcss/typography";
@@
-  plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
+  plugins: [animate, typography],

Also applies to: 95-96

🤖 Prompt for AI Agents
In platforms/marketplace/tailwind.config.ts around lines 1 and 95-96, the file
is ESM but currently uses require(...) for plugins which will fail; replace
those require(...) calls with top-level ESM import statements (import pluginName
from 'plugin-package') and then reference the imported identifiers directly in
the config export, removing any require usage and ensuring the imports are added
to the top of the file.

Comment on lines +5 to +7
"incremental": true,
"tsBuildInfoFile": "./node_modules/typescript/tsbuildinfo",
"noEmit": true,
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

tsBuildInfo path under node_modules is brittle; write to a repo-owned file instead

TypeScript will write the build info even with noEmit. Writing into node_modules can fail in CI/PNPM or read-only FS.

   "compilerOptions": {
     "incremental": true,
-    "tsBuildInfoFile": "./node_modules/typescript/tsbuildinfo",
+    "tsBuildInfoFile": "./.tsbuildinfo",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"incremental": true,
"tsBuildInfoFile": "./node_modules/typescript/tsbuildinfo",
"noEmit": true,
"incremental": true,
"tsBuildInfoFile": "./.tsbuildinfo",
"noEmit": true,
🤖 Prompt for AI Agents
In platforms/marketplace/tsconfig.json around lines 5–7 the tsBuildInfoFile is
pointed at "./node_modules/typescript/tsbuildinfo", which is brittle in CI/PNPM
or read-only filesystems; change tsBuildInfoFile to a repo-owned path (for
example "./.tsbuildinfo" or "./.cache/tsbuildinfo" or "./build/tsbuildinfo")
instead of anything under node_modules, and ensure the chosen directory exists
or is created by the build, adding the tsbuildinfo filename to .gitignore if
desired.

Comment on lines +15 to +21
"moduleResolution": "bundler",
"baseUrl": ".",
"types": ["node", "vite/client"],
"paths": {
"@/*": ["./client/src/*"],
"@shared/*": ["./shared/*"]
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify path alias support in Vite config

Aliases work in TS, but bundler also needs resolve aliases or vite-tsconfig-paths. Ensure the Vite config enables this.

To check:


🏁 Script executed:

#!/bin/bash
# Look for tsconfig-paths plugin or manual resolve.alias
rg -nP -C2 'tsconfigPaths|resolve\s*:\s*\{[^}]*alias' platforms/marketplace

Length of output: 77


🏁 Script executed:

#!/bin/bash
# Locate Vite config files in platforms/marketplace
find platforms/marketplace -maxdepth 3 -type f -iname 'vite.config.*'

# For each found config, print any resolve.alias block
for cfg in $(find platforms/marketplace -maxdepth 3 -type f -iname 'vite.config.*'); do
  echo "=== $cfg ==="
  rg -nP -C3 'resolve\s*:\s*\{[^}]*alias' "$cfg" || echo "No resolve.alias in $cfg"
done

Length of output: 568


Configure path aliases in Vite config
The marketplace’s vite.config.ts currently has no resolve.alias entries or the vite-tsconfig-paths plugin enabled, so your TypeScript aliases (@/*, @shared/*) won’t be recognized at build time. In platforms/marketplace/vite.config.ts, either:

  • Install and register vite-tsconfig-paths:
    import tsconfigPaths from 'vite-tsconfig-paths';
    
    export default defineConfig({
      plugins: [tsconfigPaths(), /* … */],
      // …
    });
  • Or add a matching resolve.alias block:
    import { resolve } from 'path';
    
    export default defineConfig({
      resolve: {
        alias: [
          { find: '@',       replacement: resolve(__dirname, 'client/src') },
          { find: '@shared', replacement: resolve(__dirname, 'shared') },
        ],
      },
      // …
    });

This ensures your TS path aliases work during bundling.

🤖 Prompt for AI Agents
In platforms/marketplace/vite.config.ts (configure to match tsconfig.json paths
referenced at platforms/marketplace/tsconfig.json lines 15-21), the TypeScript
path aliases "@/*" and "@shared/*" are not recognized by Vite; update
vite.config.ts to either register the vite-tsconfig-paths plugin (install it and
add tsconfigPaths() to the plugins array) or add a resolve.alias section that
maps "@" to client/src and "@shared" to shared using absolute paths resolved
from __dirname so the build respects your tsconfig paths.

Comment on lines +6 to +37
export default defineConfig({
plugins: [
react(),
runtimeErrorOverlay(),
...(process.env.NODE_ENV !== "production" &&
process.env.REPL_ID !== undefined
? [
await import("@replit/vite-plugin-cartographer").then((m) =>
m.cartographer(),
),
]
: []),
],
resolve: {
alias: {
"@": path.resolve(import.meta.dirname, "client", "src"),
"@shared": path.resolve(import.meta.dirname, "shared"),
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
},
},
root: path.resolve(import.meta.dirname, "client"),
build: {
outDir: path.resolve(import.meta.dirname, "dist/public"),
emptyOutDir: true,
},
server: {
fs: {
strict: true,
deny: ["**/.*"],
},
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

import.meta.dirname is not standard; fix path resolution and dynamic plugin import

Use fileURLToPath to derive the directory, and move the conditional dynamic import into an async defineConfig factory. Keeps ESM-safe and works across Node environments.

-import { defineConfig } from "vite";
-import react from "@vitejs/plugin-react";
-import path from "path";
-import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
-
-export default defineConfig({
-  plugins: [
-    react(),
-    runtimeErrorOverlay(),
-    ...(process.env.NODE_ENV !== "production" &&
-    process.env.REPL_ID !== undefined
-      ? [
-          await import("@replit/vite-plugin-cartographer").then((m) =>
-            m.cartographer(),
-          ),
-        ]
-      : []),
-  ],
-  resolve: {
-    alias: {
-      "@": path.resolve(import.meta.dirname, "client", "src"),
-      "@shared": path.resolve(import.meta.dirname, "shared"),
-      "@assets": path.resolve(import.meta.dirname, "attached_assets"),
-    },
-  },
-  root: path.resolve(import.meta.dirname, "client"),
-  build: {
-    outDir: path.resolve(import.meta.dirname, "dist/public"),
-    emptyOutDir: true,
-  },
-  server: {
-    fs: {
-      strict: true,
-      deny: ["**/.*"],
-    },
-  },
-});
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import path from "path";
+import { fileURLToPath } from "url";
+import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
+
+export default defineConfig(async () => {
+  const rootDir = path.dirname(fileURLToPath(import.meta.url));
+  const plugins = [react(), runtimeErrorOverlay()];
+
+  if (process.env.NODE_ENV !== "production" && process.env.REPL_ID) {
+    const { cartographer } = await import("@replit/vite-plugin-cartographer");
+    plugins.push(cartographer());
+  }
+
+  return {
+    plugins,
+    resolve: {
+      alias: {
+        "@": path.resolve(rootDir, "client", "src"),
+        "@shared": path.resolve(rootDir, "shared"),
+        "@assets": path.resolve(rootDir, "attached_assets"),
+      },
+    },
+    root: path.resolve(rootDir, "client"),
+    build: {
+      outDir: path.resolve(rootDir, "dist/public"),
+      emptyOutDir: true,
+    },
+    server: {
+      fs: {
+        strict: true,
+        deny: ["**/.*"],
+      },
+    },
+  };
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default defineConfig({
plugins: [
react(),
runtimeErrorOverlay(),
...(process.env.NODE_ENV !== "production" &&
process.env.REPL_ID !== undefined
? [
await import("@replit/vite-plugin-cartographer").then((m) =>
m.cartographer(),
),
]
: []),
],
resolve: {
alias: {
"@": path.resolve(import.meta.dirname, "client", "src"),
"@shared": path.resolve(import.meta.dirname, "shared"),
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
},
},
root: path.resolve(import.meta.dirname, "client"),
build: {
outDir: path.resolve(import.meta.dirname, "dist/public"),
emptyOutDir: true,
},
server: {
fs: {
strict: true,
deny: ["**/.*"],
},
},
});
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
import { fileURLToPath } from "url";
import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
export default defineConfig(async () => {
const rootDir = path.dirname(fileURLToPath(import.meta.url));
const plugins = [react(), runtimeErrorOverlay()];
if (process.env.NODE_ENV !== "production" && process.env.REPL_ID) {
const { cartographer } = await import("@replit/vite-plugin-cartographer");
plugins.push(cartographer());
}
return {
plugins,
resolve: {
alias: {
"@": path.resolve(rootDir, "client", "src"),
"@shared": path.resolve(rootDir, "shared"),
"@assets": path.resolve(rootDir, "attached_assets"),
},
},
root: path.resolve(rootDir, "client"),
build: {
outDir: path.resolve(rootDir, "dist/public"),
emptyOutDir: true,
},
server: {
fs: {
strict: true,
deny: ["**/.*"],
},
},
};
});
🤖 Prompt for AI Agents
In platforms/marketplace/vite.config.ts around lines 6 to 37, replace use of
nonstandard import.meta.dirname by deriving a directory via fileURLToPath(new
URL('.', import.meta.url)) (or equivalent) and update all path.resolve calls to
use that computed dirname; also convert the exported config to an async
defineConfig factory so you can await the conditional dynamic import of
@replit/vite-plugin-cartographer only when NODE_ENV !== "production" and REPL_ID
is set, then include the imported plugin in the plugins array or an empty array
otherwise, keeping the rest of the config (resolve.alias, root, build, server)
unchanged.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment on lines +1 to +6
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"

import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Mark as a Client Component.

This module wraps Radix UI (client-only). Add the "use client" directive to avoid Next.js Server Component import errors.

+ "use client"
+
 import * as React from "react"
 import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
 
 import { cn } from "@/lib/utils"
 import { buttonVariants } from "@/components/ui/button"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ui/alert-dialog.tsx lines 1-6,
this module imports Radix UI (a client-only library) but lacks the "use client"
directive; add a top-line "use client" directive as the first line of the file
so Next.js treats this as a Client Component and prevents Server Component
import errors, keeping the rest of the imports and code unchanged.

Comment on lines +35 to +43
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix ref/prop element type mismatches for AlertTitle/AlertDescription.

Refs and props types don’t match the rendered tags (h5 vs HTMLParagraphElement; div vs HTMLParagraphElement). This breaks type-safety and can confuse consumers.

Apply:

-const AlertTitle = React.forwardRef<
-  HTMLParagraphElement,
-  React.HTMLAttributes<HTMLHeadingElement>
+const AlertTitle = React.forwardRef<
+  HTMLHeadingElement,
+  React.HTMLAttributes<HTMLHeadingElement>
 >(({ className, ...props }, ref) => (
   <h5
     ref={ref}
     className={cn("mb-1 font-medium leading-none tracking-tight", className)}
     {...props}
   />
 ))

-const AlertDescription = React.forwardRef<
-  HTMLParagraphElement,
-  React.HTMLAttributes<HTMLParagraphElement>
+const AlertDescription = React.forwardRef<
+  HTMLDivElement,
+  React.HTMLAttributes<HTMLDivElement>
 >(({ className, ...props }, ref) => (
   <div
     ref={ref}
     className={cn("text-sm [&_p]:leading-relaxed", className)}
     {...props}
   />
 ))

Also applies to: 47-55

Comment on lines +64 to +71
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix a11y: current page should not have role="link" or aria-disabled.

The current page crumb isn’t interactive; keep aria-current="page" only.

   <span
     ref={ref}
-    role="link"
-    aria-disabled="true"
     aria-current="page"
     className={cn("font-normal text-foreground", className)}
     {...props}
   />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
<span
ref={ref}
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ui/breadcrumb.tsx around lines
64–71, the current-page span incorrectly includes role="link" and
aria-disabled="true"; remove those two attributes and keep only
aria-current="page". Also ensure you don't accidentally forward interactive
attributes from the spread props by destructuring role and aria-disabled (e.g.
const { role, 'aria-disabled': ariaDisabled, ...rest } = props) and spreading
rest so the current crumb cannot receive a role or aria-disabled from callers.

Comment on lines +86 to +97
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Don’t hijack Arrow keys inside inputs; support Up/Down for vertical carousels.

Currently Arrow keys are captured everywhere, breaking text inputs and textareas. Add a guard and map keys by orientation.

-    const handleKeyDown = React.useCallback(
-      (event: React.KeyboardEvent<HTMLDivElement>) => {
-        if (event.key === "ArrowLeft") {
-          event.preventDefault()
-          scrollPrev()
-        } else if (event.key === "ArrowRight") {
-          event.preventDefault()
-          scrollNext()
-        }
-      },
-      [scrollPrev, scrollNext]
-    )
+    const handleKeyDown = React.useCallback(
+      (event: React.KeyboardEvent<HTMLDivElement>) => {
+        const target = event.target as HTMLElement | null
+        if (target && target.closest('input, textarea, select, [contenteditable="true"]')) {
+          return
+        }
+        const key = event.key
+        if (orientation === "vertical") {
+          if (key === "ArrowUp") {
+            event.preventDefault()
+            scrollPrev()
+          } else if (key === "ArrowDown") {
+            event.preventDefault()
+            scrollNext()
+          }
+        } else {
+          if (key === "ArrowLeft") {
+            event.preventDefault()
+            scrollPrev()
+          } else if (key === "ArrowRight") {
+            event.preventDefault()
+            scrollNext()
+          }
+        }
+      },
+      [orientation, scrollPrev, scrollNext]
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
const target = event.target as HTMLElement | null
if (target && target.closest('input, textarea, select, [contenteditable="true"]')) {
return
}
const key = event.key
if (orientation === "vertical") {
if (key === "ArrowUp") {
event.preventDefault()
scrollPrev()
} else if (key === "ArrowDown") {
event.preventDefault()
scrollNext()
}
} else {
if (key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
}
},
[orientation, scrollPrev, scrollNext]
)

Comment on lines +107 to +119
React.useEffect(() => {
if (!api) {
return
}

onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)

return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Unsubscribe “reInit” listener to prevent duplicate handlers.

Missing cleanup for the reInit subscription can cause multiple onSelect calls after reinitialization.

     React.useEffect(() => {
       if (!api) {
         return
       }

       onSelect(api)
       api.on("reInit", onSelect)
       api.on("select", onSelect)

       return () => {
-        api?.off("select", onSelect)
+        api?.off("select", onSelect)
+        api?.off("reInit", onSelect)
       }
     }, [api, onSelect])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
api?.off("reInit", onSelect)
}
}, [api, onSelect])
🤖 Prompt for AI Agents
In platforms/marketplace/client/src/components/ui/carousel.tsx around lines 107
to 119, the effect subscribes to both "reInit" and "select" events but only
removes the "select" listener on cleanup, which can leave dangling "reInit"
handlers and cause duplicate onSelect calls after reinitialization; update the
cleanup to unsubscribe both events (e.g., call api?.off("reInit", onSelect) and
api?.off("select", onSelect)) so all handlers added in the effect are removed
when the component unmounts or api changes, keeping the initial onSelect
invocation intact.

Comment on lines +111 to +124
// Object storage routes
app.get("/public-objects/:filePath(*)", async (req, res) => {
const filePath = req.params.filePath;
try {
const file = await objectStorageService.searchPublicObject(filePath);
if (!file) {
return res.status(404).json({ error: "File not found" });
}
objectStorageService.downloadObject(file, res);
} catch (error) {
console.error("Error searching for public object:", error);
return res.status(500).json({ error: "Internal server error" });
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate public ACL in /public-objects/

Ensure only publicly visible files are served; otherwise a misconfigured path could expose private objects.

   app.get("/public-objects/:filePath(*)", async (req, res) => {
     const filePath = req.params.filePath;
     try {
       const file = await objectStorageService.searchPublicObject(filePath);
       if (!file) {
         return res.status(404).json({ error: "File not found" });
       }
-      objectStorageService.downloadObject(file, res);
+      const canRead = await objectStorageService.canAccessObjectEntity({
+        objectFile: file,
+      });
+      if (!canRead) {
+        return res.status(404).json({ error: "File not found" });
+      }
+      objectStorageService.downloadObject(file, res);
     } catch (error) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Object storage routes
app.get("/public-objects/:filePath(*)", async (req, res) => {
const filePath = req.params.filePath;
try {
const file = await objectStorageService.searchPublicObject(filePath);
if (!file) {
return res.status(404).json({ error: "File not found" });
}
objectStorageService.downloadObject(file, res);
} catch (error) {
console.error("Error searching for public object:", error);
return res.status(500).json({ error: "Internal server error" });
}
});
// Object storage routes
app.get("/public-objects/:filePath(*)", async (req, res) => {
const filePath = req.params.filePath;
try {
const file = await objectStorageService.searchPublicObject(filePath);
if (!file) {
return res.status(404).json({ error: "File not found" });
}
const canRead = await objectStorageService.canAccessObjectEntity({
objectFile: file,
});
if (!canRead) {
return res.status(404).json({ error: "File not found" });
}
objectStorageService.downloadObject(file, res);
} catch (error) {
console.error("Error searching for public object:", error);
return res.status(500).json({ error: "Internal server error" });
}
});
🤖 Prompt for AI Agents
In platforms/marketplace/server/routes.ts around lines 111 to 124, the route
serving /public-objects/:filePath(*) currently returns any found file without
verifying its public ACL; update the handler to check the file's public
visibility metadata (e.g., file.isPublic or file.acl/public flag) after
searchPublicObject returns and before calling downloadObject, return 403
Forbidden for non-public files, and log the access denial; also ensure the
filePath is normalized/sanitized if your storage SDK requires it to prevent path
traversal.

Comment on lines +126 to +137
app.get("/objects/:objectPath(*)", async (req, res) => {
try {
const objectFile = await objectStorageService.getObjectEntityFile(req.path);
objectStorageService.downloadObject(objectFile, res);
} catch (error) {
console.error("Error accessing object:", error);
if (error instanceof ObjectNotFoundError) {
return res.sendStatus(404);
}
return res.sendStatus(500);
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Private object leakage: add ACL check before streaming /objects/

The route streams any located object with the server’s credentials, regardless of ACL; a non-owner can fetch private files if they know the path.

   app.get("/objects/:objectPath(*)", async (req, res) => {
     try {
       const objectFile = await objectStorageService.getObjectEntityFile(req.path);
-      objectStorageService.downloadObject(objectFile, res);
+      const canRead = await objectStorageService.canAccessObjectEntity({
+        userId: (req as any).user?.id,
+        objectFile,
+      });
+      if (!canRead) {
+        return res.sendStatus(403);
+      }
+      objectStorageService.downloadObject(objectFile, res);
     } catch (error) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.get("/objects/:objectPath(*)", async (req, res) => {
try {
const objectFile = await objectStorageService.getObjectEntityFile(req.path);
objectStorageService.downloadObject(objectFile, res);
} catch (error) {
console.error("Error accessing object:", error);
if (error instanceof ObjectNotFoundError) {
return res.sendStatus(404);
}
return res.sendStatus(500);
}
});
app.get("/objects/:objectPath(*)", async (req, res) => {
try {
const objectFile = await objectStorageService.getObjectEntityFile(req.path);
const canRead = await objectStorageService.canAccessObjectEntity({
userId: (req as any).user?.id,
objectFile,
});
if (!canRead) {
return res.sendStatus(403);
}
objectStorageService.downloadObject(objectFile, res);
} catch (error) {
console.error("Error accessing object:", error);
if (error instanceof ObjectNotFoundError) {
return res.sendStatus(404);
}
return res.sendStatus(500);
}
});
🤖 Prompt for AI Agents
In platforms/marketplace/server/routes.ts around lines 126 to 137, the route
currently streams any located object without checking ACLs; before calling
downloadObject, retrieve the object's ACL/metadata and enforce authorization for
req.user (or authenticated identity) — if the object is private and the
requester is not the owner or in an allowed list, return 403 and do not stream;
implement this by calling an authorization helper (e.g.,
objectStorageService.isAuthorizedToRead or object.metadata.isPublic) after
getObjectEntityFile and before downloadObject, and ensure unauthenticated
requests are treated as unauthorized for private objects.

Comment on lines +7 to +14
export const users = pgTable("users", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
username: text("username").notNull().unique(),
email: text("email").notNull().unique(),
password: text("password").notNull(),
isAdmin: boolean("is_admin").default(false).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use UUID column type and defaultRandom().

Aligns DB type with generator, removes reliance on pgcrypto casts, and improves type-safety.

-import { pgTable, text, varchar, integer, decimal, timestamp, boolean } from "drizzle-orm/pg-core";
+import { pgTable, text, varchar, integer, decimal, timestamp, boolean, uuid, pgEnum, uniqueIndex, check } from "drizzle-orm/pg-core";

 export const users = pgTable("users", {
-  id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
+  id: uuid("id").primaryKey().defaultRandom(),
   username: text("username").notNull().unique(),
   email: text("email").notNull().unique(),
   password: text("password").notNull(),
   isAdmin: boolean("is_admin").default(false).notNull(),
-  createdAt: timestamp("created_at").defaultNow().notNull(),
+  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const users = pgTable("users", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
username: text("username").notNull().unique(),
email: text("email").notNull().unique(),
password: text("password").notNull(),
isAdmin: boolean("is_admin").default(false).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
import { pgTable, text, varchar, integer, decimal, timestamp, boolean, uuid, pgEnum, uniqueIndex, check } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
username: text("username").notNull().unique(),
email: text("email").notNull().unique(),
password: text("password").notNull(),
isAdmin: boolean("is_admin").default(false).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
🤖 Prompt for AI Agents
In platforms/marketplace/shared/schema.ts around lines 7 to 14, the users.id
column uses varchar with a raw SQL default gen_random_uuid(); change it to use
the UUID column type and the library's defaultRandom() helper (e.g.,
uuid("id").primaryKey().defaultRandom()) so the DB type matches the generator
and you avoid pgcrypto casts; also update any imports if your schema library
requires a uuid column helper instead of varchar.

💡 Verification agent

🧩 Analysis chain

Verify pgcrypto extension or use defaultRandom().

If sticking with gen_random_uuid(), ensure pgcrypto is enabled; otherwise defaultRandom() avoids this dependency.

Run:

Also applies to: 16-30, 32-39


🏁 Script executed:

#!/usr/bin/env bash
# If migrations exist, check for CREATE EXTENSION pgcrypto; else confirm use of defaultRandom()
fd -a 'drizzle|migrations' platforms/marketplace -td -E node_modules | xargs -I{} rg -n "CREATE EXTENSION\s+pgcrypto|gen_random_uuid|defaultRandom" -S -n -C2 {} || true

Length of output: 166


🏁 Script executed:

#!/usr/bin/env bash
# Search entire repo for pgcrypto extension usage, defaultRandom, and gen_random_uuid
rg -n "CREATE EXTENSION\s+pgcrypto" -C2 .
rg -n "defaultRandom" -C2 .
rg -n "gen_random_uuid" -C2 .

Length of output: 1385


Ensure pgcrypto is enabled or switch to defaultRandom()
The sql\gen_random_uuid()`defaults inplatforms/marketplace/shared/schema.ts(lines 8, 17, 33) require PostgreSQL’spgcryptoextension. Add a migration such asCREATE EXTENSION IF NOT EXISTS pgcrypto;before creating these tables, or replacedefault(sql`gen_random_uuid()`)with Drizzle ORM’sdefaultRandom()` to avoid the dependency.

🤖 Prompt for AI Agents
In platforms/marketplace/shared/schema.ts around lines 7 to 14 (and other id
defaults at ~lines 17 and 33), the use of default(sql`gen_random_uuid()`)
requires the pgcrypto extension; either add a migration that runs CREATE
EXTENSION IF NOT EXISTS pgcrypto; before creating these tables, or replace those
default(sql`gen_random_uuid()`) calls with Drizzle ORM’s defaultRandom() on the
id columns so UUIDs are generated without relying on pgcrypto; pick one approach
and consistently apply it to all id columns.

Comment on lines +16 to +30
export const apps = pgTable("apps", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
name: text("name").notNull(),
description: text("description").notNull(),
fullDescription: text("full_description"),
category: text("category").notNull(),
link: text("link").notNull(),
logoUrl: text("logo_url"),
screenshots: text("screenshots").array(),
status: text("status").default("active").notNull(), // active, pending, inactive
averageRating: decimal("average_rating", { precision: 3, scale: 2 }).default("0"),
totalReviews: integer("total_reviews").default(0).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

App table: tighten types, add enum for status, ensure updatedAt semantics.

  • Use UUID for id.
  • Enforce status via enum.
  • Ensure averageRating is not null.
  • Make timestamps timezone-aware and auto-update updatedAt in app-layer.
-export const apps = pgTable("apps", {
-  id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
+export const appStatusEnum = pgEnum("app_status", ["active", "pending", "inactive"]);
+
+export const apps = pgTable("apps", {
+  id: uuid("id").primaryKey().defaultRandom(),
   name: text("name").notNull(),
   description: text("description").notNull(),
   fullDescription: text("full_description"),
   category: text("category").notNull(),
   link: text("link").notNull(),
   logoUrl: text("logo_url"),
   screenshots: text("screenshots").array(),
-  status: text("status").default("active").notNull(), // active, pending, inactive
-  averageRating: decimal("average_rating", { precision: 3, scale: 2 }).default("0"),
+  status: appStatusEnum("status").default("active").notNull(),
+  averageRating: decimal("average_rating", { precision: 3, scale: 2 }).default("0").notNull(),
   totalReviews: integer("total_reviews").default(0).notNull(),
-  createdAt: timestamp("created_at").defaultNow().notNull(),
-  updatedAt: timestamp("updated_at").defaultNow().notNull(),
+  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
+  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
 }, (t) => ({
+  // optional: unique app names or links
+  // nameUniqueIdx: uniqueIndex("apps_name_unique_idx").on(t.name),
+  // linkUniqueIdx: uniqueIndex("apps_link_unique_idx").on(t.link),
 }));

Note: For Postgres, updatedAt won’t auto-update on UPDATE; ensure the ORM layer sets it in mutations (e.g., updatedAt = new Date()).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const apps = pgTable("apps", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
name: text("name").notNull(),
description: text("description").notNull(),
fullDescription: text("full_description"),
category: text("category").notNull(),
link: text("link").notNull(),
logoUrl: text("logo_url"),
screenshots: text("screenshots").array(),
status: text("status").default("active").notNull(), // active, pending, inactive
averageRating: decimal("average_rating", { precision: 3, scale: 2 }).default("0"),
totalReviews: integer("total_reviews").default(0).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export const appStatusEnum = pgEnum("app_status", ["active", "pending", "inactive"]);
export const apps = pgTable("apps", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
description: text("description").notNull(),
fullDescription: text("full_description"),
category: text("category").notNull(),
link: text("link").notNull(),
logoUrl: text("logo_url"),
screenshots: text("screenshots").array(),
status: appStatusEnum("status").default("active").notNull(),
averageRating: decimal("average_rating", { precision: 3, scale: 2 }).default("0").notNull(),
totalReviews: integer("total_reviews").default(0).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
}, (t) => ({
// optional: unique app names or links
// nameUniqueIdx: uniqueIndex("apps_name_unique_idx").on(t.name),
// linkUniqueIdx: uniqueIndex("apps_link_unique_idx").on(t.link),
}));
🤖 Prompt for AI Agents
In platforms/marketplace/shared/schema.ts around lines 16 to 30, tighten the
apps table types: change id to a UUID column (use the DB/ORM uuid type and
default gen_random_uuid()), replace the freeform status text with a Postgres
enum (define and use a pgEnum like "app_status" with values
"active","pending","inactive"), mark averageRating as not null and keep a
numeric/decimal default of 0, change createdAt/updatedAt to timezone-aware
timestamps (timestamptz) and keep defaultNow() for createdAt, and ensure
updatedAt is set to defaultNow() but rely on the application/ORM layer to assign
updatedAt = new Date() on updates (document or implement update middleware/hooks
accordingly).

Comment on lines +32 to +39
export const reviews = pgTable("reviews", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
appId: varchar("app_id").notNull().references(() => apps.id, { onDelete: "cascade" }),
username: text("username").notNull(),
rating: integer("rating").notNull(), // 1-5
comment: text("comment"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Review table: use UUID FK, add rating CHECK and single-review per user constraint.

Improves integrity and prevents out-of-range ratings or duplicate reviews.

-export const reviews = pgTable("reviews", {
-  id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
-  appId: varchar("app_id").notNull().references(() => apps.id, { onDelete: "cascade" }),
+export const reviews = pgTable("reviews", {
+  id: uuid("id").primaryKey().defaultRandom(),
+  appId: uuid("app_id").notNull().references(() => apps.id, { onDelete: "cascade" }),
   username: text("username").notNull(),
   rating: integer("rating").notNull(), // 1-5
   comment: text("comment"),
-  createdAt: timestamp("created_at").defaultNow().notNull(),
-});
+  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
+}, (t) => ({
+  ratingRangeCk: check("reviews_rating_range_ck", sql`${t.rating} >= 1 AND ${t.rating} <= 5`),
+  oneReviewPerUserIdx: uniqueIndex("reviews_app_username_unique_idx").on(t.appId, t.username),
+}));

Optional: prefer userId referencing users.id instead of denormalized username for referential integrity.

Also applies to: 46-51

🤖 Prompt for AI Agents
In platforms/marketplace/shared/schema.ts around lines 32-39 (and similarly
46-51), the reviews table needs stronger integrity: change appId to a UUID
foreign key referencing apps.id (use the UUID/uuidv4 column type consistent with
apps), add a CHECK constraint on rating to enforce BETWEEN 1 AND 5, and add a
uniqueness constraint to prevent duplicate reviews per user (either UNIQUE on
(app_id, username) if keeping username, or preferably replace username with
userId referencing users.id and add UNIQUE(app_id, user_id)). Update both
occurrences accordingly and preserve createdAt.defaultNow().notNull().

@coodos coodos merged commit 28952b8 into main Sep 5, 2025
1 of 4 checks passed
@coodos coodos deleted the feat/marketplace branch September 5, 2025 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants