Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type {
RealtimeEvent,
RealtimeCallback,
SortField,
UpdateManyResult,
} from "./modules/entities.types.js";

export type {
Expand Down
11 changes: 11 additions & 0 deletions src/modules/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
RealtimeEvent,
RealtimeEventType,
SortField,
UpdateManyResult,
} from "./entities.types";
import { RoomsSocket } from "../utils/socket-utils.js";

Expand Down Expand Up @@ -160,6 +161,16 @@ function createEntityHandler<T = any>(
return axios.post(`${baseURL}/bulk`, data);
},

// Update multiple entities matching a query using a MongoDB update operator
async updateMany(query: Partial<T>, data: Record<string, Record<string, any>>): Promise<UpdateManyResult> {
return axios.patch(`${baseURL}/update-many`, { query, data });
},

// Update multiple entities by ID, each with its own update data
async bulkUpdate(data: (Partial<T> & { id: string })[]): Promise<T[]> {
return axios.put(`${baseURL}/bulk`, data);
},

// Import entities from a file
async importEntities(file: File): Promise<ImportResult<T>> {
const formData = new FormData();
Expand Down
93 changes: 93 additions & 0 deletions src/modules/entities.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -379,6 +391,87 @@ export interface EntityHandler<T = any> {
*/
bulkCreate(data: Partial<T>[]): Promise<T[]>;

/**
* 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<T>, data: Record<string, Record<string, any>>): Promise<UpdateManyResult>;

/**
* 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<T> & { id: string })[]): Promise<T[]>;

/**
* Imports records from a file.
*
Expand Down
95 changes: 94 additions & 1 deletion tests/unit/entities.test.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);
});

});
Loading