diff --git a/src/index.ts b/src/index.ts index b95308b..ec58617 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,7 @@ export type { RealtimeEvent, RealtimeCallback, SortField, + UpdateManyResult, } from "./modules/entities.types.js"; export type { diff --git a/src/modules/entities.ts b/src/modules/entities.ts index 1edeeb4..c03be02 100644 --- a/src/modules/entities.ts +++ b/src/modules/entities.ts @@ -9,6 +9,7 @@ import { RealtimeEvent, RealtimeEventType, SortField, + UpdateManyResult, } from "./entities.types"; import { RoomsSocket } from "../utils/socket-utils.js"; @@ -160,6 +161,16 @@ function createEntityHandler( return axios.post(`${baseURL}/bulk`, data); }, + // Update multiple entities matching a query using a MongoDB update operator + async updateMany(query: Partial, data: Record>): Promise { + return axios.patch(`${baseURL}/update-many`, { query, data }); + }, + + // Update multiple entities by ID, each with its own update data + async bulkUpdate(data: (Partial & { id: string })[]): Promise { + return axios.put(`${baseURL}/bulk`, data); + }, + // Import entities from a file async importEntities(file: File): Promise> { const formData = new FormData(); diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index 613d554..c0e9a66 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -44,6 +44,18 @@ export interface DeleteManyResult { deleted: number; } +/** + * Result returned when updating multiple entities via a query. + */ +export interface UpdateManyResult { + /** Whether the operation was successful. */ + success: boolean; + /** Number of entities that were updated. */ + updated: number; + /** Whether there are more entities matching the query that were not updated in this batch. When `true`, call `updateMany` again with the same query to update the next batch. */ + has_more: boolean; +} + /** * Result returned when importing entities from a file. * @@ -379,6 +391,87 @@ export interface EntityHandler { */ bulkCreate(data: Partial[]): Promise; + /** + * Updates multiple records matching a query using a MongoDB update operator. + * + * Applies the same update operation to all records matching the query. + * The `data` parameter must contain one or more MongoDB update operators + * (e.g., `$set`, `$inc`, `$push`). Multiple operators can be combined in a + * single call, but each field may only appear in one operator. + * + * Results are batched in groups of up to 500 — when `has_more` is `true` + * in the response, call `updateMany` again with the same query to update + * the next batch. + * + * @param query - Query object to filter which records to update. Records matching all + * specified criteria will be updated. + * @param data - Update operation object containing one or more MongoDB update operators. + * Each field may only appear in one operator per call. + * Supported operators: `$set`, `$rename`, `$unset`, `$inc`, `$mul`, `$min`, `$max`, + * `$currentDate`, `$addToSet`, `$push`, `$pull`. + * @returns Promise resolving to the update result. + * + * @example + * ```typescript + * // Set status to 'archived' for all completed records + * const result = await base44.entities.MyEntity.updateMany( + * { status: 'completed' }, + * { $set: { status: 'archived' } } + * ); + * console.log(`Updated ${result.updated} records`); + * ``` + * + * @example + * ```typescript + * // Combine multiple operators in a single call + * const result = await base44.entities.MyEntity.updateMany( + * { category: 'sales' }, + * { $set: { status: 'done' }, $inc: { view_count: 1 } } + * ); + * ``` + * + * @example + * ```typescript + * // Handle batched updates for large datasets + * let hasMore = true; + * let totalUpdated = 0; + * while (hasMore) { + * const result = await base44.entities.MyEntity.updateMany( + * { status: 'pending' }, + * { $set: { status: 'processed' } } + * ); + * totalUpdated += result.updated; + * hasMore = result.has_more; + * } + * ``` + */ + updateMany(query: Partial, data: Record>): Promise; + + /** + * Updates multiple records in a single request, each with its own update data. + * + * Unlike `updateMany` which applies the same update to all matching records, + * `bulkUpdate` allows different updates for each record. Each item in the + * array must include an `id` field identifying which record to update. + * + * **Note:** Maximum 500 items per request. + * + * @param data - Array of update objects (max 500). Each object must have an `id` field + * and any number of fields to update. + * @returns Promise resolving to an array of updated records. + * + * @example + * ```typescript + * // Update multiple records with different data + * const updated = await base44.entities.MyEntity.bulkUpdate([ + * { id: 'entity-1', status: 'paid', amount: 999 }, + * { id: 'entity-2', status: 'cancelled' }, + * { id: 'entity-3', name: 'Renamed Item' } + * ]); + * ``` + */ + bulkUpdate(data: (Partial & { id: string })[]): Promise; + /** * Imports records from a file. * diff --git a/tests/unit/entities.test.ts b/tests/unit/entities.test.ts index 2a94e40..e655a05 100644 --- a/tests/unit/entities.test.ts +++ b/tests/unit/entities.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest"; import nock from "nock"; import { createClient } from "../../src/index.ts"; -import type { DeleteResult } from "../../src/modules/entities.types.ts"; +import type { DeleteResult, UpdateManyResult } from "../../src/modules/entities.types.ts"; /** * Todo entity type for testing. @@ -201,4 +201,97 @@ describe("Entities Module", () => { expect(scope.isDone()).toBe(true); }); + test("updateMany() should send query and data to correct endpoint", async () => { + const mockResult: UpdateManyResult = { + success: true, + updated: 3, + has_more: false, + }; + + // Mock the API response + scope + .patch(`/api/apps/${appId}/entities/Todo/update-many`, { + query: { completed: false }, + data: { $set: { completed: true } }, + }) + .reply(200, mockResult); + + // Call the API + const result = await base44.entities.Todo.updateMany( + { completed: false }, + { $set: { completed: true } } + ); + + // Verify the response + expect(result.success).toBe(true); + expect(result.updated).toBe(3); + expect(result.has_more).toBe(false); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test("updateMany() should handle has_more response", async () => { + const mockResult: UpdateManyResult = { + success: true, + updated: 500, + has_more: true, + }; + + // Mock the API response + scope + .patch(`/api/apps/${appId}/entities/Todo/update-many`, { + query: {}, + data: { $inc: { view_count: 1 } }, + }) + .reply(200, mockResult); + + // Call the API + const result = await base44.entities.Todo.updateMany( + {}, + { $inc: { view_count: 1 } } + ); + + // Verify the response + expect(result.success).toBe(true); + expect(result.updated).toBe(500); + expect(result.has_more).toBe(true); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test("bulkUpdate() should send array of updates to correct endpoint", async () => { + const updatePayload = [ + { id: "1", title: "Updated Task 1", completed: true }, + { id: "2", title: "Updated Task 2" }, + ]; + const mockResponse: Todo[] = [ + { id: "1", title: "Updated Task 1", completed: true }, + { id: "2", title: "Updated Task 2", completed: false }, + ]; + + // Mock the API response + scope + .put( + `/api/apps/${appId}/entities/Todo/bulk`, + updatePayload as nock.RequestBodyMatcher + ) + .reply(200, mockResponse); + + // Call the API + const result = await base44.entities.Todo.bulkUpdate(updatePayload); + + // Verify the response + expect(result).toHaveLength(2); + expect(result[0].id).toBe("1"); + expect(result[0].title).toBe("Updated Task 1"); + expect(result[0].completed).toBe(true); + expect(result[1].id).toBe("2"); + expect(result[1].title).toBe("Updated Task 2"); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + });