diff --git a/AGENTS.md b/AGENTS.md index 7fe655a..e51daef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,3 +57,39 @@ When adding a new feature or service, determine its scope: - **Web-only** (Express routes, Mongoose models, session management) → goes in gadget-code If code is needed by both consumer packages, it belongs in `@gadget/ai`. Do not copy-paste shared logic across packages. + +## GadgetId + +All entity IDs use the `GadgetId` type (a string alias) defined in `@gadget/api`. This eliminates MongoDB `ObjectId` conversion throughout the codebase. + +### Interfaces + +Always specify `{ _id: GadgetId }` in interfaces: + +```ts +import { GadgetId } from "@gadget/api"; + +interface IMyEntity { + _id: GadgetId; + // ... other fields +} +``` + +### Mongoose Schemas + +Always use this pattern for `_id`: + +```ts +_id: { type: String, default: () => nanoid() } +``` + +**Never add `unique: true` or `required: true` to `_id`**: +- `unique: true` causes duplicate index errors at startup (MongoDB auto-creates a unique index on `_id`) +- `required: true` causes save failures because the default is assigned AFTER validation + +### Rules + +1. All `_id` fields are `GadgetId` (string) +2. No `ObjectId` usage in application code +3. No `createFromHexString()` or `toHexString()` conversions +4. Never `unique` or `required` on `_id` in schemas diff --git a/docs/gadget-id.md b/docs/gadget-id.md new file mode 100644 index 0000000..2d3a624 --- /dev/null +++ b/docs/gadget-id.md @@ -0,0 +1,111 @@ +# GadgetId + +## Overview + +GadgetId is a string-based identifier type used throughout the Gadget monorepo for all entity IDs. It replaces MongoDB's native `ObjectId` type with a simple, portable string that works consistently across all packages without requiring Mongoose or MongoDB-specific conversions. + +## Type Definition + +```ts +// packages/api/src/lib/gadget-id.ts +export type GadgetId = string; +``` + +## Why GadgetId? + +- **Consistency**: All packages use the same ID type regardless of whether they use Mongoose +- **Simplicity**: No `createFromHexString()` or `toHexString()` conversions needed +- **Portability**: Works seamlessly from backend to frontend to drone workers +- **Type safety**: Compile-time checking ensures IDs are always strings + +## Usage + +### Interfaces + +Always use `GadgetId` for `_id` fields in interfaces: + +```ts +import { GadgetId } from "@gadget/api"; + +interface IUser { + _id: GadgetId; + email: string; + displayName: string; +} +``` + +### Mongoose Schemas + +Use the following pattern for all schemas: + +```ts +import { Schema, model } from "mongoose"; +import { GadgetId } from "@gadget/api"; +import { nanoid } from "nanoid"; + +const MySchema = new Schema({ + _id: { type: String, default: () => nanoid() }, + // ... other fields +}); +``` + +### Important Schema Rules + +**Do NOT add `unique: true` or `required: true` to the `_id` field:** + +- MongoDB automatically creates a unique index on `_id`; specifying `unique: true` causes duplicate index errors at startup +- MongoDB handles `_id` validation automatically; specifying `required: true` causes save failures because the default is assigned AFTER validation + +Correct: +```ts +_id: { type: String, default: () => nanoid() } +``` + +Incorrect: +```ts +// DON'T do this - breaks everything +_id: { type: String, default: () => nanoid(), unique: true, required: true } +``` + +### Creating Documents + +```ts +const thing = new Thing({ /* other fields */ }); +await thing.save(); // Works - MongoDB handles _id automatically +``` + +### Passing IDs to Functions + +IDs are strings - just pass them directly: + +```ts +const userId: GadgetId = user._id; +await userService.getById(userId); // No conversion needed +``` + +## GadgetId vs ObjectId + +| Operation | ObjectId | GadgetId | +|-----------|----------|----------| +| Type | `ObjectId` | `string` | +| Creation | `new ObjectId()` | `nanoid()` (via schema default) | +| String conversion | `id.toHexString()` | Not needed | +| Parsing from string | `createFromHexString(str)` | Not needed | +| Mongoose casting | Automatic with `Schema.Types.ObjectId` | Automatic with `type: String` | + +## Importing GadgetId + +```ts +// From @gadget/api (recommended for most code) +import { GadgetId } from "@gadget/api"; + +// From packages/api directly (for @gadget/api consumers) +import { GadgetId } from "../../../packages/api/dist/lib/gadget-id.js"; +``` + +## Rules + +1. **Never use `ObjectId` directly** in application code +2. **Never import `ObjectId` from Mongoose** in service/controller code +3. **Always use `GadgetId`** for `_id` fields in interfaces and function parameters +4. **Never add `unique` or `required`** to `_id` in Mongoose schemas \ No newline at end of file diff --git a/gadget-code/AGENTS.md b/gadget-code/AGENTS.md index 546047d..12cf310 100644 --- a/gadget-code/AGENTS.md +++ b/gadget-code/AGENTS.md @@ -32,4 +32,37 @@ strict, noUnusedLocals, noUnusedParameters, noUncheckedIndexedAccess all enabled ## Architecture - Backend: Express 5 + Socket.io + Mongoose + Redis sessions - Frontend: React 19 + Vite 8 + Tailwind CSS 4 -- Entry points: `src/web-app.ts` (backend), `frontend/src/main.tsx` (frontend) \ No newline at end of file +- Entry points: `src/web-app.ts` (backend), `frontend/src/main.tsx` (frontend) + +## GadgetId + +All entity IDs use `GadgetId` (a string alias) from `@gadget/api`. Never use `ObjectId`. + +### Interfaces + +```ts +import { GadgetId } from "@gadget/api"; + +interface IMyEntity { + _id: GadgetId; + // ... +} +``` + +### Mongoose Schemas + +```ts +import { nanoid } from "nanoid"; + +const MySchema = new Schema({ + _id: { type: String, default: () => nanoid() }, + // ... +}); +``` + +### Critical: Never add `unique` or `required` to `_id` + +- `unique: true` → duplicate index error at startup (MongoDB auto-creates unique index on `_id`) +- `required: true` → save fails because default is assigned AFTER validation + +See `docs/gadget-id.md` for full documentation. \ No newline at end of file diff --git a/gadget-code/src/controllers/api/v1/provider.ts b/gadget-code/src/controllers/api/v1/provider.ts index 21803e6..e1a7146 100644 --- a/gadget-code/src/controllers/api/v1/provider.ts +++ b/gadget-code/src/controllers/api/v1/provider.ts @@ -75,18 +75,10 @@ class ProviderController extends DtpController { } catch (error) { const err = error as Error; this.log.error("failed to get provider", { error: err.message }); - - if (err.message.includes("Cast to ObjectId failed")) { - res.status(400).json({ - success: false, - message: "Invalid provider ID", - }); - } else { - res.status(500).json({ - success: false, - message: err.message, - }); - } + res.status(400).json({ + success: false, + message: "Invalid provider ID", + }); } } } diff --git a/gadget-code/tests/socket-service.test.ts b/gadget-code/tests/socket-service.test.ts index 82fddc4..b1cbed0 100644 --- a/gadget-code/tests/socket-service.test.ts +++ b/gadget-code/tests/socket-service.test.ts @@ -281,7 +281,7 @@ describe("SocketService Session Indexing", () => { ); }); - it("should handle chatSessionId as string or ObjectId", () => { + it("should handle chatSessionId as string", () => { const mockSocket = createMockSocket("code-socket-chat2"); const mockCodeSession = { socket: mockSocket, @@ -299,7 +299,7 @@ describe("SocketService Session Indexing", () => { const found1 = SocketService.getCodeSessionByChatSessionId(chatSessionId); expect(found1).toBe(mockCodeSession); - // Test with ObjectId + // Test with same ID again const found2 = SocketService.getCodeSessionByChatSessionId( mockChatSession._id, ); diff --git a/gadget-drone/AGENTS.md b/gadget-drone/AGENTS.md index 6f19db0..aaf4210 100644 --- a/gadget-drone/AGENTS.md +++ b/gadget-drone/AGENTS.md @@ -27,4 +27,25 @@ pnpm start # Run built code (dist/gadget-drone.js) ## Tests -None configured (`pnpm test` exits with error). \ No newline at end of file +None configured (`pnpm test` exits with error). + +## GadgetId + +All entity IDs use `GadgetId` (a string alias) from `@gadget/api`: + +```ts +import { GadgetId } from "@gadget/api"; + +// Use GadgetId for all ID fields +const registrationId: GadgetId = "abc123..."; +``` + +**Schema `_id` pattern (for Mongoose models):** + +```ts +_id: { type: String, default: () => nanoid() } +``` + +**Never add `unique: true` or `required: true` to `_id`** — MongoDB handles this automatically. + +See `docs/gadget-id.md` for full documentation. \ No newline at end of file diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index dd4f6a3..9bd986d 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -26,8 +26,7 @@ export * from "./messages/drone.ts"; export * from "./messages/socket.ts"; /* - * Utilities - re-export mongoose Types for ObjectId usage without requiring - * drone to have mongoose as a direct dependency + * Utilities - re-export types for cross-package usage */ export { Types } from "mongoose";