From b7241db83b1168696e7eeacafb7122ccde0c7a20 Mon Sep 17 00:00:00 2001 From: danshw Date: Sun, 15 Mar 2026 15:53:07 +0200 Subject: [PATCH 1/2] feat(sdk): added support for bulk operations on entities --- src/index.ts | 1 + src/modules/entities.ts | 11 ++++ src/modules/entities.types.ts | 88 ++++++++++++++++++++++++++++++++ tests/unit/entities.test.ts | 95 ++++++++++++++++++++++++++++++++++- 4 files changed, 194 insertions(+), 1 deletion(-) 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..eb6c07c 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,82 @@ 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 exactly one MongoDB update operator + * (e.g., `$set`, `$inc`, `$push`). + * + * Results are batched — 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 exactly one MongoDB update operator. + * 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 + * // Increment a counter on all matching records + * const result = await base44.entities.MyEntity.updateMany( + * { category: 'sales' }, + * { $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. + * + * @param data - Array of update objects. 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); + }); + }); From 8c2cfcaff82e25f63fce1d2d5a6aa5e88cd6bd3b Mon Sep 17 00:00:00 2001 From: danshw Date: Sun, 15 Mar 2026 16:02:56 +0200 Subject: [PATCH 2/2] feat(sdk): added support for bulk operations on entities --- src/modules/entities.types.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index eb6c07c..c0e9a66 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -395,15 +395,18 @@ export interface EntityHandler { * 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 exactly one MongoDB update operator - * (e.g., `$set`, `$inc`, `$push`). + * 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 — when `has_more` is `true` in the response, call - * `updateMany` again with the same query to update the next batch. + * 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 exactly one MongoDB update operator. + * @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. @@ -420,10 +423,10 @@ export interface EntityHandler { * * @example * ```typescript - * // Increment a counter on all matching records + * // Combine multiple operators in a single call * const result = await base44.entities.MyEntity.updateMany( * { category: 'sales' }, - * { $inc: { view_count: 1 } } + * { $set: { status: 'done' }, $inc: { view_count: 1 } } * ); * ``` * @@ -451,7 +454,9 @@ export interface EntityHandler { * `bulkUpdate` allows different updates for each record. Each item in the * array must include an `id` field identifying which record to update. * - * @param data - Array of update objects. Each object must have an `id` field + * **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. *