finishing touches on move to GadgetId from ObjectId
This commit is contained in:
parent
3f28680a44
commit
64c4304f42
36
AGENTS.md
36
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
|
- **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.
|
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
|
||||||
|
|||||||
111
docs/gadget-id.md
Normal file
111
docs/gadget-id.md
Normal file
@ -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<IMyInterface>({
|
||||||
|
_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
|
||||||
@ -32,4 +32,37 @@ strict, noUnusedLocals, noUnusedParameters, noUncheckedIndexedAccess all enabled
|
|||||||
## Architecture
|
## Architecture
|
||||||
- Backend: Express 5 + Socket.io + Mongoose + Redis sessions
|
- Backend: Express 5 + Socket.io + Mongoose + Redis sessions
|
||||||
- Frontend: React 19 + Vite 8 + Tailwind CSS 4
|
- Frontend: React 19 + Vite 8 + Tailwind CSS 4
|
||||||
- Entry points: `src/web-app.ts` (backend), `frontend/src/main.tsx` (frontend)
|
- 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<IMyEntity>({
|
||||||
|
_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.
|
||||||
@ -75,18 +75,10 @@ class ProviderController extends DtpController {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
this.log.error("failed to get provider", { error: err.message });
|
this.log.error("failed to get provider", { error: err.message });
|
||||||
|
res.status(400).json({
|
||||||
if (err.message.includes("Cast to ObjectId failed")) {
|
success: false,
|
||||||
res.status(400).json({
|
message: "Invalid provider ID",
|
||||||
success: false,
|
});
|
||||||
message: "Invalid provider ID",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: err.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 mockSocket = createMockSocket("code-socket-chat2");
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: mockSocket,
|
socket: mockSocket,
|
||||||
@ -299,7 +299,7 @@ describe("SocketService Session Indexing", () => {
|
|||||||
const found1 = SocketService.getCodeSessionByChatSessionId(chatSessionId);
|
const found1 = SocketService.getCodeSessionByChatSessionId(chatSessionId);
|
||||||
expect(found1).toBe(mockCodeSession);
|
expect(found1).toBe(mockCodeSession);
|
||||||
|
|
||||||
// Test with ObjectId
|
// Test with same ID again
|
||||||
const found2 = SocketService.getCodeSessionByChatSessionId(
|
const found2 = SocketService.getCodeSessionByChatSessionId(
|
||||||
mockChatSession._id,
|
mockChatSession._id,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -27,4 +27,25 @@ pnpm start # Run built code (dist/gadget-drone.js)
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
None configured (`pnpm test` exits with error).
|
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.
|
||||||
@ -26,8 +26,7 @@ export * from "./messages/drone.ts";
|
|||||||
export * from "./messages/socket.ts";
|
export * from "./messages/socket.ts";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Utilities - re-export mongoose Types for ObjectId usage without requiring
|
* Utilities - re-export types for cross-package usage
|
||||||
* drone to have mongoose as a direct dependency
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { Types } from "mongoose";
|
export { Types } from "mongoose";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user