From ebc14c07ee93277ed52748919cb955921f1a1d07 Mon Sep 17 00:00:00 2001 From: Paranoa-dev Date: Fri, 26 Jun 2026 14:55:04 +0000 Subject: [PATCH 1/3] feat: bulk endpoint registration for /api/apis/:id/endpoints/bulk --- PR_DESCRIPTION_BULK_ENDPOINTS.md | 58 +++++++ docs/openapi.json | 167 ++++++++++++++++++++ src/app.test.ts | 15 ++ src/config/env.ts | 3 + src/config/index.ts | 1 + src/repositories/apiRepository.drizzle.ts | 34 ++++ src/repositories/apiRepository.ts | 82 ++++++++++ src/routes/apiKeyRoutes.test.ts | 16 ++ src/routes/apis.test.ts | 181 +++++++++++++++++++++- src/routes/apis.ts | 56 ++++++- src/validators/apiRegistration.ts | 11 ++ 11 files changed, 622 insertions(+), 2 deletions(-) create mode 100644 PR_DESCRIPTION_BULK_ENDPOINTS.md diff --git a/PR_DESCRIPTION_BULK_ENDPOINTS.md b/PR_DESCRIPTION_BULK_ENDPOINTS.md new file mode 100644 index 0000000..d1698d8 --- /dev/null +++ b/PR_DESCRIPTION_BULK_ENDPOINTS.md @@ -0,0 +1,58 @@ +# Bulk Endpoint Registration + +Adds `POST /api/apis/:id/endpoints/bulk` to register multiple endpoints atomically. + +## Changes + +### New Route +- `POST /api/apis/:id/endpoints/bulk` — registers multiple endpoints for an existing API in a single transaction +- Requires authentication (bearer token or `x-user-id` header) +- Validates the authenticated developer owns the API +- Validates endpoints with Zod (`bulkEndpointsSchema`) +- Returns `201` with per-row endpoint results (id, api_id, path, method, price_per_call_usdc, description) +- Caps batch size at 50 endpoints (configurable via `BULK_ENDPOINT_LIMIT`) + +### Repository Layer +- Added `bulkCreateEndpoints(apiId, endpoints)` to `ApiRepository` interface +- Implemented in `DrizzleApiRepository` using `db.transaction()` — full rollback on failure +- Implemented in `InMemoryApiRepository` for testing +- Added `defaultApiRepository.bulkCreateEndpoints` with cache invalidation + +### Validator +- Added `bulkEndpointsSchema` to `src/validators/apiRegistration.ts` +- Reuses the existing `apiEndpointRegistrationSchema` for individual endpoint validation +- Enforces `min 1, max 50` endpoints + +### Configuration +- Added `BULK_ENDPOINT_LIMIT` env var (default: 50) in `src/config/env.ts` + +### OpenAPI +- Added `/api/apis/{id}/endpoints/bulk` path with request/response schemas + +### Tests +- 8 new tests in `src/routes/apis.test.ts` covering: + - Unauthorized access + - Invalid API ID + - API not found / not owned + - Empty endpoints array + - Invalid endpoint data + - Successful bulk creation with per-row results + - Exceeding the 50-endpoint limit + - Persistence verification via GET /:id + +## Test Output + +```text +PASS src/routes/apis.test.ts + POST /api/apis/:id/endpoints/bulk + ✓ returns 401 without authentication (154 ms) + ✓ returns 400 when id is not a positive integer (37 ms) + ✓ returns 404 when the API does not belong to the developer (7 ms) + ✓ returns 400 with empty endpoints array (7 ms) + ✓ returns 400 when endpoint data is invalid (12 ms) + ✓ creates endpoints and returns per-row results (5 ms) + ✓ rejects more than 50 endpoints (7 ms) + ✓ persists endpoints that can be retrieved via GET /:id (8 ms) +``` + +Closes #400 diff --git a/docs/openapi.json b/docs/openapi.json index 62b8224..d42fc91 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -503,6 +503,90 @@ } } } + }, + "/api/apis/{id}/endpoints/bulk": { + "post": { + "summary": "Bulk register endpoints for an API", + "description": "Registers multiple endpoints atomically for the specified API. All endpoints are inserted in a single transaction; if any endpoint is invalid the entire batch is rolled back. Authenticated developer must own the API.", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Numerical ID of the API" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkEndpointRegistrationRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Endpoints registered successfully — per-row details returned", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkEndpointRegistrationResponse" + } + } + } + }, + "400": { + "description": "Validation error — invalid endpoint data, empty array, or too many endpoints", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "API not found or does not belong to the developer", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } } }, "components": { @@ -853,6 +937,89 @@ } } }, + "BulkEndpointRegistrationRequest": { + "type": "object", + "required": [ + "endpoints" + ], + "properties": { + "endpoints": { + "type": "array", + "minItems": 1, + "maxItems": 50, + "items": { + "type": "object", + "required": [ + "path", + "method", + "price_per_call_usdc" + ], + "properties": { + "path": { + "type": "string", + "description": "URL path starting with /" + }, + "method": { + "type": "string", + "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] + }, + "price_per_call_usdc": { + "type": "string", + "description": "Non-negative decimal string (e.g. \"0.01\")" + }, + "description": { + "type": "string", + "description": "Optional human-readable description" + } + } + } + } + } + }, + "BulkEndpointRegistrationResponse": { + "type": "object", + "required": [ + "endpoints" + ], + "properties": { + "endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "api_id", + "path", + "method", + "price_per_call_usdc" + ], + "properties": { + "id": { + "type": "integer", + "description": "Auto-generated endpoint ID" + }, + "api_id": { + "type": "integer", + "description": "Owning API ID" + }, + "path": { + "type": "string" + }, + "method": { + "type": "string" + }, + "price_per_call_usdc": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + } + } + } + } + } + }, "ApiDetailsResponse": { "type": "object", "required": [ diff --git a/src/app.test.ts b/src/app.test.ts index 8ddb0e8..f4b76a0 100644 --- a/src/app.test.ts +++ b/src/app.test.ts @@ -221,6 +221,21 @@ class FakeApiRepository implements ApiRepository { async getEndpoints() { return []; } + + async delete() { + return true; + } + + async bulkCreateEndpoints(_apiId: number, endpoints: import('./repositories/apiRepository.js').CreateEndpointInput[]) { + return endpoints.map((e, i) => ({ + id: i + 1, + api_id: _apiId, + path: e.path, + method: e.method, + price_per_call_usdc: e.price_per_call_usdc, + description: e.description ?? null, + })); + } } const createDeveloperRepository = (profile?: Developer): DeveloperRepository => ({ diff --git a/src/config/env.ts b/src/config/env.ts index 0fe5504..b9409e7 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -136,6 +136,9 @@ export const envSchema = z // Idempotency IDEMPOTENCY_RETENTION_WINDOW_SECONDS: z.coerce.number().int().positive().default(86400), + + // Bulk endpoint registration + BULK_ENDPOINT_LIMIT: z.coerce.number().int().positive().default(50), }) .superRefine((values, ctx) => { if (values.SOROBAN_RPC_ENABLED && !values.SOROBAN_RPC_URL) { diff --git a/src/config/index.ts b/src/config/index.ts index 6368535..5123777 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -203,4 +203,5 @@ export const config = { listingsCache: { warmupTimeoutMs: env.LISTINGS_CACHE_WARMUP_TIMEOUT_MS, }, + bulkEndpointLimit: env.BULK_ENDPOINT_LIMIT, } as const; diff --git a/src/repositories/apiRepository.drizzle.ts b/src/repositories/apiRepository.drizzle.ts index 34045a1..44e827a 100644 --- a/src/repositories/apiRepository.drizzle.ts +++ b/src/repositories/apiRepository.drizzle.ts @@ -4,7 +4,9 @@ import type { Api, ApiEndpoint, NewApi, NewApiEndpoint } from "../db/schema.js"; import type { ApiCreateInput, ApiWithEndpoints, + BulkCreateEndpointResult, CreateApiInput, + CreateEndpointInput, ApiDetails, ApiEndpointInfo, ApiListFilters, @@ -237,4 +239,36 @@ export class DrizzleApiRepository implements ApiRepository { description: r.description, })); } + + async bulkCreateEndpoints( + apiId: number, + endpoints: CreateEndpointInput[], + ): Promise { + return db.transaction(async (tx) => { + const rows = await tx + .insert(schema.apiEndpoints) + .values( + endpoints.map( + (e) => + ({ + api_id: apiId, + path: e.path, + method: e.method, + price_per_call_usdc: e.price_per_call_usdc, + description: e.description ?? null, + }) as NewApiEndpoint, + ), + ) + .returning(); + + return rows.map((r) => ({ + id: r.id, + api_id: r.api_id, + path: r.path, + method: r.method, + price_per_call_usdc: r.price_per_call_usdc, + description: r.description, + })); + }); + } } diff --git a/src/repositories/apiRepository.ts b/src/repositories/apiRepository.ts index 6ba1f62..526d046 100644 --- a/src/repositories/apiRepository.ts +++ b/src/repositories/apiRepository.ts @@ -70,6 +70,15 @@ export interface PaginatedApiListResult { total: number; } +export interface BulkCreateEndpointResult { + id: number; + api_id: number; + path: string; + method: string; + price_per_call_usdc: string; + description: string | null; +} + export interface ApiRepository { create(api: ApiCreateInput): Promise; createWithEndpoints(input: CreateApiInput): Promise; @@ -82,6 +91,10 @@ export interface ApiRepository { listPublic(filters?: ApiListFilters): Promise; findById(id: number): Promise; getEndpoints(apiId: number): Promise; + bulkCreateEndpoints( + apiId: number, + endpoints: CreateEndpointInput[], + ): Promise; } export const defaultApiRepository: ApiRepository = { @@ -274,6 +287,36 @@ export const defaultApiRepository: ApiRepository = { description: r.description, })); }, + + async bulkCreateEndpoints(apiId, endpoints) { + return db.transaction(async (tx) => { + const rows = await tx + .insert(schema.apiEndpoints) + .values( + endpoints.map( + (e) => + ({ + api_id: apiId, + path: e.path, + method: e.method, + price_per_call_usdc: e.price_per_call_usdc, + description: e.description ?? null, + }) as NewApiEndpoint, + ), + ) + .returning(); + + listingsCache.invalidateAll(); + return rows.map((r) => ({ + id: r.id, + api_id: r.api_id, + path: r.path, + method: r.method, + price_per_call_usdc: r.price_per_call_usdc, + description: r.description, + })); + }); + }, }; // --- In-Memory implementation (for testing) --- @@ -543,6 +586,45 @@ export class InMemoryApiRepository implements ApiRepository { async getEndpoints(apiId: number): Promise { return this.endpointsByApiId.get(apiId) ?? []; } + + async bulkCreateEndpoints( + apiId: number, + endpoints: CreateEndpointInput[], + ): Promise { + const existing = this.endpointsByApiId.get(apiId) ?? []; + const now = new Date(); + const nextIds = existing.length + 1; + + const created = endpoints.map((e, i) => ({ + id: nextIds + i, + api_id: apiId, + path: e.path, + method: e.method, + price_per_call_usdc: e.price_per_call_usdc, + description: e.description ?? null, + created_at: now, + updated_at: now, + })); + + this.endpointsByApiId.set(apiId, [ + ...existing, + ...created.map((c) => ({ + path: c.path, + method: c.method, + price_per_call_usdc: c.price_per_call_usdc, + description: c.description, + })), + ]); + + return created.map((c) => ({ + id: c.id, + api_id: c.api_id, + path: c.path, + method: c.method, + price_per_call_usdc: c.price_per_call_usdc, + description: c.description, + })); + } } export async function listPublicDetailed( diff --git a/src/routes/apiKeyRoutes.test.ts b/src/routes/apiKeyRoutes.test.ts index 443146b..36cd706 100644 --- a/src/routes/apiKeyRoutes.test.ts +++ b/src/routes/apiKeyRoutes.test.ts @@ -55,9 +55,15 @@ const createApiRepository = (apis: Api[]): ApiRepository => ({ async create() { throw new Error('not implemented'); }, + async createWithEndpoints() { + throw new Error('not implemented'); + }, async update() { return null; }, + async delete() { + return true; + }, async listByDeveloper(developerId: number) { return apis.filter((api) => api.developer_id === developerId); }, @@ -70,6 +76,16 @@ const createApiRepository = (apis: Api[]): ApiRepository => ({ async getEndpoints() { return []; }, + async bulkCreateEndpoints(_apiId, endpoints) { + return endpoints.map((e, i) => ({ + id: i + 1, + api_id: _apiId, + path: e.path, + method: e.method, + price_per_call_usdc: e.price_per_call_usdc, + description: e.description ?? null, + })); + }, }); function createTestApp(apis: Api[] = [ownedApi]) { diff --git a/src/routes/apis.test.ts b/src/routes/apis.test.ts index acf8169..65ab599 100644 --- a/src/routes/apis.test.ts +++ b/src/routes/apis.test.ts @@ -10,8 +10,33 @@ import express from 'express'; import request from 'supertest'; import { errorHandler } from '../middleware/errorHandler.js'; import { InMemoryApiRepository } from '../repositories/apiRepository.js'; +import type { Api, Developer } from '../db/schema.js'; +import type { DeveloperRepository } from '../repositories/developerRepository.js'; import { createApisRouter } from './apis.js'; +const developerProfile: Developer = { + id: 11, + user_id: 'dev-1', + name: 'Test Developer', + website: null, + description: null, + category: null, + created_at: new Date(1000), + updated_at: new Date(1000), +}; + +const developerRepository: DeveloperRepository = { + async findByUserId(userId: string) { + return userId === developerProfile.user_id ? developerProfile : undefined; + }, + async getOrCreateByUserId(userId: string) { + return userId === developerProfile.user_id ? developerProfile : { ...developerProfile, id: 999, user_id: userId }; + }, + async upsertProfile(userId: string) { + return developerProfile; + }, +}; + describe('createApisRouter', () => { function buildApp() { const repo = new InMemoryApiRepository( @@ -61,7 +86,7 @@ describe('createApisRouter', () => { ); const app = express(); - app.use('/api/apis', createApisRouter({ apiRepository: repo })); + app.use('/api/apis', createApisRouter({ apiRepository: repo, developerRepository })); app.use(errorHandler); return app; } @@ -121,3 +146,157 @@ describe('createApisRouter', () => { expect(res.body.data).toHaveLength(1); }); }); + +describe('POST /api/apis/:id/endpoints/bulk', () => { + function buildBulkApp() { + const ownedApi: Api = { + id: 101, + developer_id: 11, + name: 'Search API', + description: null, + base_url: 'https://search.example.com', + logo_url: null, + category: 'search', + status: 'active', + created_at: new Date(1000), + updated_at: new Date(1000), + }; + + const unownedApi: Api = { + id: 202, + developer_id: 99, + name: 'Other API', + description: null, + base_url: 'https://other.example.com', + logo_url: null, + category: 'payments', + status: 'active', + created_at: new Date(1000), + updated_at: new Date(1000), + }; + + const repo = new InMemoryApiRepository( + [ownedApi, unownedApi], + new Map([[101, []]]), + ); + + const app = express(); + app.use(express.json()); + app.use('/api/apis', createApisRouter({ apiRepository: repo, developerRepository })); + app.use(errorHandler); + return app; + } + + it('returns 401 without authentication', async () => { + const app = buildBulkApp(); + const res = await request(app) + .post('/api/apis/101/endpoints/bulk') + .send({ endpoints: [{ path: '/test', method: 'GET', price_per_call_usdc: '0.01' }] }); + expect(res.status).toBe(401); + }); + + it('returns 400 when id is not a positive integer', async () => { + const app = buildBulkApp(); + const res = await request(app) + .post('/api/apis/abc/endpoints/bulk') + .set('x-user-id', 'dev-1') + .send({ endpoints: [{ path: '/test', method: 'GET', price_per_call_usdc: '0.01' }] }); + expect(res.status).toBe(400); + }); + + it('returns 404 when the API does not belong to the developer', async () => { + const app = buildBulkApp(); + const res = await request(app) + .post('/api/apis/202/endpoints/bulk') + .set('x-user-id', 'dev-1') + .send({ endpoints: [{ path: '/test', method: 'GET', price_per_call_usdc: '0.01' }] }); + expect(res.status).toBe(404); + }); + + it('returns 400 with empty endpoints array', async () => { + const app = buildBulkApp(); + const res = await request(app) + .post('/api/apis/101/endpoints/bulk') + .set('x-user-id', 'dev-1') + .send({ endpoints: [] }); + expect(res.status).toBe(400); + }); + + it('returns 400 when endpoint data is invalid', async () => { + const app = buildBulkApp(); + const res = await request(app) + .post('/api/apis/101/endpoints/bulk') + .set('x-user-id', 'dev-1') + .send({ endpoints: [{ path: '', method: 'INVALID', price_per_call_usdc: '-1' }] }); + expect(res.status).toBe(400); + }); + + it('creates endpoints and returns per-row results', async () => { + const app = buildBulkApp(); + const payload = { + endpoints: [ + { path: '/search', method: 'GET', price_per_call_usdc: '0.05', description: 'Search endpoint' }, + { path: '/lookup', method: 'POST', price_per_call_usdc: '0.10' }, + ], + }; + + const res = await request(app) + .post('/api/apis/101/endpoints/bulk') + .set('x-user-id', 'dev-1') + .send(payload); + + expect(res.status).toBe(201); + expect(res.body.endpoints).toHaveLength(2); + + const ep1 = res.body.endpoints[0]; + expect(ep1.path).toBe('/search'); + expect(ep1.method).toBe('GET'); + expect(ep1.price_per_call_usdc).toBe('0.05'); + expect(ep1.description).toBe('Search endpoint'); + expect(typeof ep1.id).toBe('number'); + expect(ep1.api_id).toBe(101); + + const ep2 = res.body.endpoints[1]; + expect(ep2.path).toBe('/lookup'); + expect(ep2.method).toBe('POST'); + expect(ep2.price_per_call_usdc).toBe('0.10'); + expect(ep2.description).toBeNull(); + expect(typeof ep2.id).toBe('number'); + expect(ep2.api_id).toBe(101); + }); + + it('rejects more than 50 endpoints', async () => { + const app = buildBulkApp(); + const manyEndpoints = Array.from({ length: 51 }, (_, i) => ({ + path: `/endpoint/${i}`, + method: 'GET' as const, + price_per_call_usdc: '0.01', + })); + + const res = await request(app) + .post('/api/apis/101/endpoints/bulk') + .set('x-user-id', 'dev-1') + .send({ endpoints: manyEndpoints }); + + expect(res.status).toBe(400); + }); + + it('persists endpoints that can be retrieved via GET /:id', async () => { + const app = buildBulkApp(); + + const createRes = await request(app) + .post('/api/apis/101/endpoints/bulk') + .set('x-user-id', 'dev-1') + .send({ endpoints: [{ path: '/persist', method: 'GET', price_per_call_usdc: '0.02' }] }); + + expect(createRes.status).toBe(201); + + const getRes = await request(app).get('/api/apis/101'); + expect(getRes.status).toBe(200); + expect(getRes.body.endpoints).toEqual( + expect.arrayContaining([ + expect.objectContaining({ path: '/persist', method: 'GET', price_per_call_usdc: '0.02' }), + ]), + ); + }); +}); diff --git a/src/routes/apis.ts b/src/routes/apis.ts index 3c64d31..2bd5208 100644 --- a/src/routes/apis.ts +++ b/src/routes/apis.ts @@ -13,7 +13,7 @@ import { defaultDeveloperRepository, type DeveloperRepository, } from '../repositories/developerRepository.js'; -import { apiRegistrationSchema } from '../validators/apiRegistration.js'; +import { apiRegistrationSchema, bulkEndpointsSchema } from '../validators/apiRegistration.js'; export interface ApisRouterDeps { apiRepository?: ApiRepository; @@ -134,6 +134,60 @@ export function createApisRouter(deps: ApisRouterDeps = {}): Router { }, ); + router.post( + '/:id/endpoints/bulk', + requireAuth, + bodyValidator(bulkEndpointsSchema), + async (req, res: Response, next) => { + try { + const user = res.locals.authenticatedUser; + if (!user) { + next(new UnauthorizedError()); + return; + } + + const apiId = Number(req.params.id); + if (!Number.isInteger(apiId) || apiId <= 0) { + next(new BadRequestError('id must be a positive integer')); + return; + } + + const developer = await developerRepository.findByUserId(user.id); + if (!developer) { + next( + new BadRequestError( + 'Developer profile not found. Create a developer profile first.', + 'DEVELOPER_NOT_FOUND', + ), + ); + return; + } + + const developerApis = await apiRepository.listByDeveloper(developer.id); + const api = developerApis.find((a) => a.id === apiId); + if (!api) { + next(new NotFoundError('API not found')); + return; + } + + const payload = bulkEndpointsSchema.parse(req.body); + const endpoints = await apiRepository.bulkCreateEndpoints( + apiId, + payload.endpoints.map((ep) => ({ + path: ep.path, + method: ep.method, + price_per_call_usdc: ep.price_per_call_usdc, + description: ep.description ?? null, + })), + ); + + res.status(201).json({ endpoints }); + } catch (error) { + next(error); + } + }, + ); + return router; } diff --git a/src/validators/apiRegistration.ts b/src/validators/apiRegistration.ts index acc512e..744ae8e 100644 --- a/src/validators/apiRegistration.ts +++ b/src/validators/apiRegistration.ts @@ -29,3 +29,14 @@ export const apiRegistrationSchema = z.object({ }); export type ApiRegistrationInput = z.infer; + +const MAX_BULK_ENDPOINTS = 50; + +export const bulkEndpointsSchema = z.object({ + endpoints: z + .array(apiEndpointRegistrationSchema) + .min(1, 'At least one endpoint is required') + .max(MAX_BULK_ENDPOINTS, `Cannot register more than ${MAX_BULK_ENDPOINTS} endpoints at once`), +}); + +export type BulkEndpointsInput = z.infer; From 63e239050f5e1de6ef617e27716f00dbec798959 Mon Sep 17 00:00:00 2001 From: Paranoa-dev Date: Fri, 26 Jun 2026 15:07:43 +0000 Subject: [PATCH 2/3] fix: lint - rename unused param in upsertProfile --- src/routes/apis.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/apis.test.ts b/src/routes/apis.test.ts index 65ab599..1c1edee 100644 --- a/src/routes/apis.test.ts +++ b/src/routes/apis.test.ts @@ -32,7 +32,7 @@ const developerRepository: DeveloperRepository = { async getOrCreateByUserId(userId: string) { return userId === developerProfile.user_id ? developerProfile : { ...developerProfile, id: 999, user_id: userId }; }, - async upsertProfile(userId: string) { + async upsertProfile(_userId: string) { return developerProfile; }, }; From be7e2d95d0412f548a89408fcdbd66bab76e4b01 Mon Sep 17 00:00:00 2001 From: Paranoa-dev Date: Fri, 26 Jun 2026 15:10:55 +0000 Subject: [PATCH 3/3] fix: resolve type errors in delete method and StubApiRepository mocks --- src/repositories/apiRepository.drizzle.ts | 6 +++--- src/repositories/apiRepository.ts | 6 +++--- tests/integration/protected.test.ts | 13 +++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/repositories/apiRepository.drizzle.ts b/src/repositories/apiRepository.drizzle.ts index 44e827a..d5e6197 100644 --- a/src/repositories/apiRepository.drizzle.ts +++ b/src/repositories/apiRepository.drizzle.ts @@ -113,11 +113,11 @@ export class DrizzleApiRepository implements ApiRepository { } async delete(id: number): Promise { - const deleted = await db.delete(schema.apis).where(eq(schema.apis.id, id)); + const result = await db.delete(schema.apis).where(eq(schema.apis.id, id)); - // Drizzle's delete() returns the number of rows deleted. + // Drizzle's delete() with better-sqlite3 returns a RunResult { changes, lastInsertRowid }. // The database FK with ON DELETE CASCADE will automatically clean up endpoints. - return deleted > 0; + return result.changes > 0; } async listByDeveloper( diff --git a/src/repositories/apiRepository.ts b/src/repositories/apiRepository.ts index 526d046..5eb6fab 100644 --- a/src/repositories/apiRepository.ts +++ b/src/repositories/apiRepository.ts @@ -163,13 +163,13 @@ export const defaultApiRepository: ApiRepository = { }, async delete(id) { - const deleted = await db.delete(schema.apis).where(eq(schema.apis.id, id)); + const result = await db.delete(schema.apis).where(eq(schema.apis.id, id)); // Deletion may affect any listing (e.g., removed from public catalog). listingsCache.invalidateAll(); - // Drizzle's delete() returns the number of rows deleted - return deleted > 0; + // Drizzle's delete() with better-sqlite3 returns a RunResult { changes, lastInsertRowid }. + return result.changes > 0; }, async listByDeveloper(developerId, filters = {}) { diff --git a/tests/integration/protected.test.ts b/tests/integration/protected.test.ts index 4cec5eb..8951d26 100644 --- a/tests/integration/protected.test.ts +++ b/tests/integration/protected.test.ts @@ -221,6 +221,19 @@ class StubApiRepository implements ApiRepository { async getEndpoints() { return []; } + async delete() { + return true; + } + async bulkCreateEndpoints(_apiId: number, endpoints: import('../../src/repositories/apiRepository.js').CreateEndpointInput[]) { + return endpoints.map((e, i) => ({ + id: i + 1, + api_id: _apiId, + path: e.path, + method: e.method, + price_per_call_usdc: e.price_per_call_usdc, + description: e.description ?? null, + })); + } } /**