From cd90b22885eb67013a91da045e6e477ca4de2ed1 Mon Sep 17 00:00:00 2001 From: Oz Sayag Date: Tue, 27 Jan 2026 13:39:54 +0200 Subject: [PATCH 1/7] Refactor entities module to support generic entity types and improve type safety --- src/modules/entities.ts | 47 ++++---- src/modules/entities.types.ts | 56 +++++++--- tests/unit/entities.test.js | 173 ----------------------------- tests/unit/entities.test.ts | 197 ++++++++++++++++++++++++++++++++++ 4 files changed, 264 insertions(+), 209 deletions(-) delete mode 100644 tests/unit/entities.test.js create mode 100644 tests/unit/entities.test.ts diff --git a/src/modules/entities.ts b/src/modules/entities.ts index 9cfc6bd..3e703e0 100644 --- a/src/modules/entities.ts +++ b/src/modules/entities.ts @@ -1,5 +1,7 @@ import { AxiosInstance } from "axios"; import { + DeleteManyResult, + DeleteResult, EntitiesModule, EntityHandler, RealtimeCallback, @@ -54,12 +56,12 @@ export function createEntitiesModule( * Parses the realtime message data and extracts event information. * @internal */ -function parseRealtimeMessage(dataStr: string): RealtimeEvent | null { +function parseRealtimeMessage(dataStr: string): RealtimeEvent | null { try { const parsed = JSON.parse(dataStr); return { type: parsed.type as RealtimeEventType, - data: parsed.data, + data: parsed.data as T, id: parsed.id || parsed.data?.id, timestamp: parsed.timestamp || new Date().toISOString(), }; @@ -79,17 +81,22 @@ function parseRealtimeMessage(dataStr: string): RealtimeEvent | null { * @returns Entity handler with CRUD methods * @internal */ -function createEntityHandler( +function createEntityHandler( axios: AxiosInstance, appId: string, entityName: string, getSocket: () => ReturnType -): EntityHandler { +): EntityHandler { const baseURL = `/apps/${appId}/entities/${entityName}`; return { // List entities with optional pagination and sorting - async list(sort: string, limit: number, skip: number, fields: string[]) { + async list( + sort?: string, + limit?: number, + skip?: number, + fields?: string[] + ): Promise { const params: Record = {}; if (sort) params.sort = sort; if (limit) params.limit = limit; @@ -102,12 +109,12 @@ function createEntityHandler( // Filter entities based on query async filter( - query: Record, - sort: string, - limit: number, - skip: number, - fields: string[] - ) { + query: Partial, + sort?: string, + limit?: number, + skip?: number, + fields?: string[] + ): Promise { const params: Record = { q: JSON.stringify(query), }; @@ -122,37 +129,37 @@ function createEntityHandler( }, // Get entity by ID - async get(id: string) { + async get(id: string): Promise { return axios.get(`${baseURL}/${id}`); }, // Create new entity - async create(data: Record) { + async create(data: Partial): Promise { return axios.post(baseURL, data); }, // Update entity by ID - async update(id: string, data: Record) { + async update(id: string, data: Partial): Promise { return axios.put(`${baseURL}/${id}`, data); }, // Delete entity by ID - async delete(id: string) { + async delete(id: string): Promise { return axios.delete(`${baseURL}/${id}`); }, // Delete multiple entities based on query - async deleteMany(query: Record) { + async deleteMany(query: Partial): Promise { return axios.delete(baseURL, { data: query }); }, // Create multiple entities in a single request - async bulkCreate(data: Record[]) { + async bulkCreate(data: Partial[]): Promise { return axios.post(`${baseURL}/bulk`, data); }, // Import entities from a file - async importEntities(file: File) { + async importEntities(file: File): Promise { const formData = new FormData(); formData.append("file", file, file.name); @@ -164,14 +171,14 @@ function createEntityHandler( }, // Subscribe to realtime updates - subscribe(callback: RealtimeCallback): () => void { + subscribe(callback: RealtimeCallback): () => void { const room = `entities:${appId}:${entityName}`; // Get the socket and subscribe to the room const socket = getSocket(); const unsubscribe = socket.subscribeToRoom(room, { update_model: (msg) => { - const event = parseRealtimeMessage(msg.data); + const event = parseRealtimeMessage(msg.data); if (!event) { return; } diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index 8666db0..be3812a 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -5,12 +5,14 @@ export type RealtimeEventType = "create" | "update" | "delete"; /** * Payload received when a realtime event occurs. + * + * @typeParam T - The entity type for the data field. Defaults to `any`. */ -export interface RealtimeEvent { +export interface RealtimeEvent { /** The type of change that occurred */ type: RealtimeEventType; /** The entity data */ - data: any; + data: T; /** The unique identifier of the affected entity */ id: string; /** ISO 8601 timestamp of when the event occurred */ @@ -19,15 +21,37 @@ export interface RealtimeEvent { /** * Callback function invoked when a realtime event occurs. + * + * @typeParam T - The entity type for the event data. Defaults to `any`. + */ +export type RealtimeCallback = (event: RealtimeEvent) => void; + +/** + * Result returned when deleting a single entity. + */ +export interface DeleteResult { + /** Whether the deletion was successful */ + success: boolean; +} + +/** + * Result returned when deleting multiple entities. */ -export type RealtimeCallback = (event: RealtimeEvent) => void; +export interface DeleteManyResult { + /** Whether the deletion was successful */ + success: boolean; + /** Number of entities that were deleted */ + deleted: number; +} /** * Entity handler providing CRUD operations for a specific entity type. * * Each entity in the app gets a handler with these methods for managing data. + * + * @typeParam T - The entity type. Defaults to `any` for backward compatibility. */ -export interface EntityHandler { +export interface EntityHandler { /** * Lists records with optional pagination and sorting. * @@ -72,7 +96,7 @@ export interface EntityHandler { limit?: number, skip?: number, fields?: string[] - ): Promise; + ): Promise; /** * Filters records based on a query. @@ -132,12 +156,12 @@ export interface EntityHandler { * ``` */ filter( - query: Record, + query: Partial, sort?: string, limit?: number, skip?: number, fields?: string[] - ): Promise; + ): Promise; /** * Gets a single record by ID. @@ -154,7 +178,7 @@ export interface EntityHandler { * console.log(record.name); * ``` */ - get(id: string): Promise; + get(id: string): Promise; /** * Creates a new record. @@ -175,7 +199,7 @@ export interface EntityHandler { * console.log('Created record with ID:', newRecord.id); * ``` */ - create(data: Record): Promise; + create(data: Partial): Promise; /** * Updates an existing record. @@ -205,7 +229,7 @@ export interface EntityHandler { * }); * ``` */ - update(id: string, data: Record): Promise; + update(id: string, data: Partial): Promise; /** * Deletes a single record by ID. @@ -219,10 +243,10 @@ export interface EntityHandler { * ```typescript * // Delete a record * const result = await base44.entities.MyEntity.delete('entity-123'); - * console.log('Deleted:', result); + * console.log('Deleted:', result.success); * ``` */ - delete(id: string): Promise; + delete(id: string): Promise; /** * Deletes multiple records matching a query. @@ -244,7 +268,7 @@ export interface EntityHandler { * console.log('Deleted:', result); * ``` */ - deleteMany(query: Record): Promise; + deleteMany(query: Partial): Promise; /** * Creates multiple records in a single request. @@ -265,7 +289,7 @@ export interface EntityHandler { * ]); * ``` */ - bulkCreate(data: Record[]): Promise; + bulkCreate(data: Partial[]): Promise; /** * Imports records from a file. @@ -315,7 +339,7 @@ export interface EntityHandler { * unsubscribe(); * ``` */ - subscribe(callback: RealtimeCallback): () => void; + subscribe(callback: RealtimeCallback): () => void; } /** @@ -364,5 +388,5 @@ export interface EntitiesModule { * base44.entities.AnotherEntity * ``` */ - [entityName: string]: EntityHandler; + [entityName: string]: EntityHandler; } diff --git a/tests/unit/entities.test.js b/tests/unit/entities.test.js deleted file mode 100644 index d75b374..0000000 --- a/tests/unit/entities.test.js +++ /dev/null @@ -1,173 +0,0 @@ -import { describe, test, expect, beforeEach, afterEach } from 'vitest'; -import nock from 'nock'; -import { createClient } from '../../src/index.ts'; - -describe('Entities Module', () => { - let base44; - let scope; - const appId = 'test-app-id'; - const serverUrl = 'https://api.base44.com'; - - beforeEach(() => { - // Create a new client for each test - base44 = createClient({ - serverUrl, - appId, - }); - - // Create a nock scope for mocking API calls - scope = nock(serverUrl); - - // Enable request debugging for Nock - nock.disableNetConnect(); - nock.emitter.on('no match', (req) => { - console.log(`Nock: No match for ${req.method} ${req.path}`); - console.log('Headers:', req.getHeaders()); - }); - }); - - afterEach(() => { - // Clean up any pending mocks - nock.cleanAll(); - nock.emitter.removeAllListeners('no match'); - nock.enableNetConnect(); - }); - - test('list() should fetch entities with correct parameters', async () => { - // Mock the API response - scope.get(`/api/apps/${appId}/entities/Todo`) - .query(true) // Accept any query parameters - .reply(200, { - items: [ - { id: '1', title: 'Task 1', completed: false }, - { id: '2', title: 'Task 2', completed: true } - ], - total: 2 - }); - - // Call the API - const result = await base44.entities.Todo.list('title', 10, 0, ['id', 'title']); - - // Verify the response - expect(result.items).toHaveLength(2); - expect(result.items[0].title).toBe('Task 1'); - expect(result.total).toBe(2); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); - }); - - test('filter() should send correct query parameters', async () => { - const filterQuery = { completed: true }; - - // Mock the API response - scope.get(`/api/apps/${appId}/entities/Todo`) - .query(query => { - // Verify the query contains our filter - const parsedQ = JSON.parse(query.q); - return parsedQ.completed === true; - }) - .reply(200, { - items: [ - { id: '2', title: 'Task 2', completed: true } - ], - total: 1 - }); - - // Call the API - const result = await base44.entities.Todo.filter(filterQuery); - - // Verify the response - expect(result.items).toHaveLength(1); - expect(result.items[0].completed).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); - }); - - test('get() should fetch a single entity', async () => { - const todoId = '123'; - - // Mock the API response - scope.get(`/api/apps/${appId}/entities/Todo/${todoId}`) - .reply(200, { - id: todoId, - title: 'Get milk', - completed: false - }); - - // Call the API - const todo = await base44.entities.Todo.get(todoId); - - // Verify the response - expect(todo.id).toBe(todoId); - expect(todo.title).toBe('Get milk'); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); - }); - - test('create() should send correct data', async () => { - const newTodo = { - title: 'New task', - completed: false - }; - - // Mock the API response - scope.post(`/api/apps/${appId}/entities/Todo`, newTodo) - .reply(201, { - id: '123', - ...newTodo - }); - - // Call the API - const todo = await base44.entities.Todo.create(newTodo); - - // Verify the response - expect(todo.id).toBe('123'); - expect(todo.title).toBe('New task'); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); - }); - - test('update() should send correct data', async () => { - const todoId = '123'; - const updates = { - title: 'Updated task', - completed: true - }; - - // Mock the API response - scope.put(`/api/apps/${appId}/entities/Todo/${todoId}`, updates) - .reply(200, { - id: todoId, - ...updates - }); - - // Call the API - const todo = await base44.entities.Todo.update(todoId, updates); - - // Verify the response - expect(todo.id).toBe(todoId); - expect(todo.title).toBe('Updated task'); - expect(todo.completed).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); - }); - - test('delete() should call correct endpoint', async () => { - const todoId = '123'; - - // Mock the API response - scope.delete(`/api/apps/${appId}/entities/Todo/${todoId}`) - .reply(204); - - // Call the API - await base44.entities.Todo.delete(todoId); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); - }); -}); \ No newline at end of file diff --git a/tests/unit/entities.test.ts b/tests/unit/entities.test.ts new file mode 100644 index 0000000..fec0f31 --- /dev/null +++ b/tests/unit/entities.test.ts @@ -0,0 +1,197 @@ +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"; + +/** + * Todo entity type for testing. + */ +interface Todo { + id: string; + title: string; + completed: boolean; +} + +describe("Entities Module", () => { + let base44: ReturnType; + let scope: nock.Scope; + const appId = "test-app-id"; + const serverUrl = "https://api.base44.com"; + + beforeEach(() => { + // Create a new client for each test + base44 = createClient({ + serverUrl, + appId, + }); + + // Create a nock scope for mocking API calls + scope = nock(serverUrl); + + // Enable request debugging for Nock + nock.disableNetConnect(); + nock.emitter.on("no match", (req) => { + console.log(`Nock: No match for ${req.method} ${req.path}`); + console.log("Headers:", req.getHeaders()); + }); + }); + + afterEach(() => { + // Clean up any pending mocks + nock.cleanAll(); + nock.emitter.removeAllListeners("no match"); + nock.enableNetConnect(); + }); + + test("list() should fetch entities with correct parameters", async () => { + const mockTodos: Todo[] = [ + { id: "1", title: "Task 1", completed: false }, + { id: "2", title: "Task 2", completed: true }, + ]; + + // Mock the API response + scope + .get(`/api/apps/${appId}/entities/Todo`) + .query(true) // Accept any query parameters + .reply(200, mockTodos); + + // Call the API + const result = await base44.entities.Todo.list("title", 10, 0, [ + "id", + "title", + ]); + + // Verify the response + expect(result).toHaveLength(2); + expect(result[0].title).toBe("Task 1"); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test("filter() should send correct query parameters", async () => { + const filterQuery: Partial = { completed: true }; + const mockTodos: Todo[] = [{ id: "2", title: "Task 2", completed: true }]; + + // Mock the API response + scope + .get(`/api/apps/${appId}/entities/Todo`) + .query((query) => { + // Verify the query contains our filter + const parsedQ = JSON.parse(query.q as string); + return parsedQ.completed === true; + }) + .reply(200, mockTodos); + + // Call the API + const result = await base44.entities.Todo.filter(filterQuery); + + // Verify the response + expect(result).toHaveLength(1); + expect(result[0].completed).toBe(true); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test("get() should fetch a single entity", async () => { + const todoId = "123"; + const mockTodo: Todo = { + id: todoId, + title: "Get milk", + completed: false, + }; + + // Mock the API response + scope.get(`/api/apps/${appId}/entities/Todo/${todoId}`).reply(200, mockTodo); + + // Call the API + const todo = await base44.entities.Todo.get(todoId); + + // Verify the response + expect(todo.id).toBe(todoId); + expect(todo.title).toBe("Get milk"); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test("create() should send correct data", async () => { + const newTodo: Partial = { + title: "New task", + completed: false, + }; + const createdTodo: Todo = { + id: "123", + title: "New task", + completed: false, + }; + + // Mock the API response + scope + .post(`/api/apps/${appId}/entities/Todo`, newTodo as nock.RequestBodyMatcher) + .reply(201, createdTodo); + + // Call the API + const todo = await base44.entities.Todo.create(newTodo); + + // Verify the response + expect(todo.id).toBe("123"); + expect(todo.title).toBe("New task"); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test("update() should send correct data", async () => { + const todoId = "123"; + const updates: Partial = { + title: "Updated task", + completed: true, + }; + const updatedTodo: Todo = { + id: todoId, + title: "Updated task", + completed: true, + }; + + // Mock the API response + scope + .put( + `/api/apps/${appId}/entities/Todo/${todoId}`, + updates as nock.RequestBodyMatcher + ) + .reply(200, updatedTodo); + + // Call the API + const todo = await base44.entities.Todo.update(todoId, updates); + + // Verify the response + expect(todo.id).toBe(todoId); + expect(todo.title).toBe("Updated task"); + expect(todo.completed).toBe(true); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test("delete() should call correct endpoint and return DeleteResult", async () => { + const todoId = "123"; + const deleteResult: DeleteResult = { success: true }; + + // Mock the API response + scope + .delete(`/api/apps/${appId}/entities/Todo/${todoId}`) + .reply(200, deleteResult); + + // Call the API + const result = await base44.entities.Todo.delete(todoId); + + // Verify the response matches DeleteResult type + expect(result.success).toBe(true); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + +}); From 4b25c61471cd9cfeb3df16c82156d0090b78bd4b Mon Sep 17 00:00:00 2001 From: Oz Sayag Date: Tue, 27 Jan 2026 13:42:39 +0200 Subject: [PATCH 2/7] Extend EntitiesModule with typed Todo handler for improved type safety in entities tests --- tests/unit/entities.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/entities.test.ts b/tests/unit/entities.test.ts index fec0f31..904587f 100644 --- a/tests/unit/entities.test.ts +++ b/tests/unit/entities.test.ts @@ -12,6 +12,13 @@ interface Todo { completed: boolean; } +// Declaration merging: extend EntitiesModule with typed Todo handler +declare module "../../src/modules/entities.types.ts" { + interface EntitiesModule { + Todo: EntityHandler; + } +} + describe("Entities Module", () => { let base44: ReturnType; let scope: nock.Scope; From 07267141b477c94a2e83099fa4a3feafc3a5fa0a Mon Sep 17 00:00:00 2001 From: Oz Sayag Date: Wed, 28 Jan 2026 13:25:25 +0200 Subject: [PATCH 3/7] Enhance entity handling with typed sorting and import results - Introduced `SortField` type for improved sorting parameter handling in list and filter methods. - Updated `importEntities` method to return a structured `ImportResult` type, providing detailed import status and output. - Modified list and filter methods to support generic field selection, enhancing type safety and flexibility. --- src/modules/entities.ts | 20 ++++++----- src/modules/entities.types.ts | 66 ++++++++++++++++++++++++++++------- 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/modules/entities.ts b/src/modules/entities.ts index 3e703e0..1edeeb4 100644 --- a/src/modules/entities.ts +++ b/src/modules/entities.ts @@ -4,9 +4,11 @@ import { DeleteResult, EntitiesModule, EntityHandler, + ImportResult, RealtimeCallback, RealtimeEvent, RealtimeEventType, + SortField, } from "./entities.types"; import { RoomsSocket } from "../utils/socket-utils.js"; @@ -91,12 +93,12 @@ function createEntityHandler( return { // List entities with optional pagination and sorting - async list( - sort?: string, + async list( + sort?: SortField, limit?: number, skip?: number, - fields?: string[] - ): Promise { + fields?: K[] + ): Promise[]> { const params: Record = {}; if (sort) params.sort = sort; if (limit) params.limit = limit; @@ -108,13 +110,13 @@ function createEntityHandler( }, // Filter entities based on query - async filter( + async filter( query: Partial, - sort?: string, + sort?: SortField, limit?: number, skip?: number, - fields?: string[] - ): Promise { + fields?: K[] + ): Promise[]> { const params: Record = { q: JSON.stringify(query), }; @@ -159,7 +161,7 @@ function createEntityHandler( }, // Import entities from a file - async importEntities(file: File): Promise { + async importEntities(file: File): Promise> { const formData = new FormData(); formData.append("file", file, file.name); diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index be3812a..7460bd0 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -44,6 +44,42 @@ export interface DeleteManyResult { deleted: number; } +/** + * Result returned when importing entities from a file. + * + * @typeParam T - The entity type for imported records. Defaults to `any`. + */ +export interface ImportResult { + /** Status of the import operation */ + status: "success" | "error"; + /** Details message, e.g., "Successfully imported 3 entities with RLS enforcement" */ + details: string | null; + /** Array of created entity objects when successful, or null on error */ + output: T[] | null; +} + +/** + * Sort field type for entity queries. + * + * Supports ascending (no prefix or `'+'`) and descending (`'-'`) sorting. + * + * @typeParam T - The entity type to derive sortable fields from. + * + * @example + * ```typescript + * // Ascending sort (default) + * 'created_date' + * '+created_date' + * + * // Descending sort + * '-created_date' + * ``` + */ +export type SortField = + | (keyof T & string) + | `+${keyof T & string}` + | `-${keyof T & string}`; + /** * Entity handler providing CRUD operations for a specific entity type. * @@ -60,11 +96,12 @@ export interface EntityHandler { * * **Note:** The maximum limit is 5,000 items per request. * + * @typeParam K - The fields to include in the response. Defaults to all fields. * @param sort - Sort parameter, such as `'-created_date'` for descending. Defaults to `'-created_date'`. * @param limit - Maximum number of results to return. Defaults to `50`. * @param skip - Number of results to skip for pagination. Defaults to `0`. * @param fields - Array of field names to include in the response. Defaults to all fields. - * @returns Promise resolving to an array of records. + * @returns Promise resolving to an array of records with selected fields. * * @example * ```typescript @@ -91,12 +128,12 @@ export interface EntityHandler { * const fields = await base44.entities.MyEntity.list('-created_date', 10, 0, ['name', 'status']); * ``` */ - list( - sort?: string, + list( + sort?: SortField, limit?: number, skip?: number, - fields?: string[] - ): Promise; + fields?: K[] + ): Promise[]>; /** * Filters records based on a query. @@ -106,6 +143,7 @@ export interface EntityHandler { * * **Note:** The maximum limit is 5,000 items per request. * + * @typeParam K - The fields to include in the response. Defaults to all fields. * @param query - Query object with field-value pairs. Each key should be a field name * from your entity schema, and each value is the criteria to match. Records matching all * specified criteria are returned. Field names are case-sensitive. @@ -113,7 +151,7 @@ export interface EntityHandler { * @param limit - Maximum number of results to return. Defaults to `50`. * @param skip - Number of results to skip for pagination. Defaults to `0`. * @param fields - Array of field names to include in the response. Defaults to all fields. - * @returns Promise resolving to an array of filtered records. + * @returns Promise resolving to an array of filtered records with selected fields. * * @example * ```typescript @@ -155,13 +193,13 @@ export interface EntityHandler { * ); * ``` */ - filter( + filter( query: Partial, - sort?: string, + sort?: SortField, limit?: number, skip?: number, - fields?: string[] - ): Promise; + fields?: K[] + ): Promise[]>; /** * Gets a single record by ID. @@ -298,7 +336,7 @@ export interface EntityHandler { * The file format should match your entity structure. Requires a browser environment and can't be used in the backend. * * @param file - File object to import. - * @returns Promise resolving to the import result. + * @returns Promise resolving to the import result containing status, details, and created records. * * @example * ```typescript @@ -307,12 +345,14 @@ export interface EntityHandler { * const file = event.target.files?.[0]; * if (file) { * const result = await base44.entities.MyEntity.importEntities(file); - * console.log(`Imported ${result.count} records`); + * if (result.status === 'success' && result.output) { + * console.log(`Imported ${result.output.length} records`); + * } * } * }; * ``` */ - importEntities(file: File): Promise; + importEntities(file: File): Promise>; /** * Subscribes to realtime updates for all records of this entity type. From 375a874cb263eea46d2fcc4af3b4916ee28f4924 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 15:05:18 +0200 Subject: [PATCH 4/7] add support to augment the types of the sdk --- src/index.ts | 7 +++- src/modules/agents.types.ts | 39 +++++++++++++++++++--- src/modules/entities.types.ts | 60 +++++++++++++++++++++++++--------- src/modules/functions.types.ts | 35 ++++++++++++++++++-- 4 files changed, 119 insertions(+), 22 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9ea7892..ea845eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,6 +36,7 @@ export * from "./types.js"; export type { EntitiesModule, EntityHandler, + EntityTypeRegistry, RealtimeEventType, RealtimeEvent, RealtimeCallback, @@ -71,10 +72,14 @@ export type { CreateFileSignedUrlResult, } from "./modules/integrations.types.js"; -export type { FunctionsModule } from "./modules/functions.types.js"; +export type { + FunctionsModule, + FunctionNameRegistry, +} from "./modules/functions.types.js"; export type { AgentsModule, + AgentNameRegistry, AgentConversation, AgentMessage, AgentMessageReasoning, diff --git a/src/modules/agents.types.ts b/src/modules/agents.types.ts index b437eb2..14fb155 100644 --- a/src/modules/agents.types.ts +++ b/src/modules/agents.types.ts @@ -2,6 +2,37 @@ import { AxiosInstance } from "axios"; import { RoomsSocket } from "../utils/socket-utils.js"; import { ModelFilterParams } from "../types.js"; +/** + * Registry of agent names. + * + * This interface is designed to be augmented by generated type declaration files. + * When augmented, it enables autocomplete for agent names in methods like `createConversation`. + * + * @example + * ```typescript + * // In your generated types.d.ts file: + * declare module '@base44/sdk' { + * interface AgentNameRegistry { + * support_agent: true; + * sales_bot: true; + * } + * } + * + * // Then in your code: + * await base44.agents.createConversation({ + * agent_name: 'support_agent' // ✅ Autocomplete shows: 'support_agent' | 'sales_bot' + * }); + * ``` + */ +export interface AgentNameRegistry {} + +/** + * Agent name type - uses registry keys if augmented, otherwise falls back to string. + */ +export type AgentName = keyof AgentNameRegistry extends never + ? string + : keyof AgentNameRegistry; + /** * Reasoning information for an agent message. * @@ -134,8 +165,8 @@ export interface AgentMessage { * Parameters for creating a new conversation. */ export interface CreateConversationParams { - /** The name of the agent to create a conversation with. */ - agent_name: string; + /** The name of the agent to create a conversation with. When AgentNameRegistry is augmented, provides autocomplete. */ + agent_name: AgentName; /** Optional metadata to attach to the conversation. */ metadata?: Record; } @@ -359,7 +390,7 @@ export interface AgentsModule { * Generates a URL that users can use to connect with the agent through WhatsApp. * The URL includes authentication if a token is available. * - * @param agentName - The name of the agent. + * @param agentName - The name of the agent. When AgentNameRegistry is augmented, provides autocomplete. * @returns WhatsApp connection URL. * * @example @@ -370,5 +401,5 @@ export interface AgentsModule { * // User can open this URL to start a WhatsApp conversation * ``` */ - getWhatsAppConnectURL(agentName: string): string; + getWhatsAppConnectURL(agentName: AgentName): string; } diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index 7460bd0..37e460d 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -80,6 +80,31 @@ export type SortField = | `+${keyof T & string}` | `-${keyof T & string}`; +/** + * Registry mapping entity names to their TypeScript types. + * + * This interface is designed to be augmented by generated type declaration files. + * When augmented, it enables type-safe entity access throughout your application. + * + * @example + * ```typescript + * // In your generated types.d.ts file: + * declare module '@base44/sdk' { + * interface EntityTypeRegistry { + * Task: { title: string; completed: boolean }; + * User: { email: string; name: string }; + * } + * } + * + * // Then in your code: + * const task = await base44.entities.Task.create({ + * title: 'Buy groceries', // ✅ Type-checked + * completed: false + * }); + * ``` + */ +export interface EntityTypeRegistry {} + /** * Entity handler providing CRUD operations for a specific entity type. * @@ -382,6 +407,22 @@ export interface EntityHandler { subscribe(callback: RealtimeCallback): () => void; } +/** + * Typed entities module - maps registry keys to typed handlers. + * Only used when EntityTypeRegistry is augmented. + */ +type TypedEntitiesModule = { + [K in keyof EntityTypeRegistry]: EntityHandler; +}; + +/** + * Dynamic entities module - allows any entity name with untyped handler. + * Used as fallback and for entities not in the registry. + */ +type DynamicEntitiesModule = { + [entityName: string]: EntityHandler; +}; + /** * Entities module for managing app data. * @@ -391,6 +432,9 @@ export interface EntityHandler { * Entities are accessed dynamically using the pattern: * `base44.entities.EntityName.method()` * + * When {@link EntityTypeRegistry} is augmented (via generated types.d.ts), + * entity access becomes type-safe with autocomplete and type checking. + * * This module is available to use with a client in all three authentication modes: * * - **Anonymous or User authentication** (`base44.entities`): Access is scoped to the current user's permissions. Anonymous users can only access public entities, while authenticated users can access entities they have permission to view or modify. @@ -415,18 +459,4 @@ export interface EntityHandler { * const allUsers = await base44.asServiceRole.entities.User.list(); * ``` */ -export interface EntitiesModule { - /** - * Access any entity by name. - * - * Use this to access entities defined in the app. - * - * @example - * ```typescript - * // Access entities dynamically - * base44.entities.MyEntity - * base44.entities.AnotherEntity - * ``` - */ - [entityName: string]: EntityHandler; -} +export type EntitiesModule = TypedEntitiesModule & DynamicEntitiesModule; diff --git a/src/modules/functions.types.ts b/src/modules/functions.types.ts index cdc91a2..033af98 100644 --- a/src/modules/functions.types.ts +++ b/src/modules/functions.types.ts @@ -1,3 +1,34 @@ +/** + * Registry of function names. + * + * This interface is designed to be augmented by generated type declaration files. + * When augmented, it enables autocomplete for function names in the `invoke` method. + * + * @example + * ```typescript + * // In your generated types.d.ts file: + * declare module '@base44/sdk' { + * interface FunctionNameRegistry { + * calculateTotal: true; + * processImage: true; + * } + * } + * + * // Then in your code: + * await base44.functions.invoke('calculateTotal', { ... }); + * // ^^^^^^^^^^^^^^^^ + * // ✅ Autocomplete shows: 'calculateTotal' | 'processImage' + * ``` + */ +export interface FunctionNameRegistry {} + +/** + * Function name type - uses registry keys if augmented, otherwise falls back to string. + */ +export type FunctionName = keyof FunctionNameRegistry extends never + ? string + : keyof FunctionNameRegistry; + /** * Functions module for invoking custom backend functions. * @@ -17,7 +48,7 @@ export interface FunctionsModule { * the result. If any parameter is a `File` object, the request will automatically be * sent as `multipart/form-data`. Otherwise, it will be sent as JSON. * - * @param functionName - The name of the function to invoke. + * @param functionName - The name of the function to invoke. When FunctionNameRegistry is augmented, provides autocomplete. * @param data - An object containing named parameters for the function. * @returns Promise resolving to the function's response. The `data` property contains the data returned by the function, if there is any. * @@ -46,5 +77,5 @@ export interface FunctionsModule { * }; * ``` */ - invoke(functionName: string, data: Record): Promise; + invoke(functionName: FunctionName, data?: Record): Promise; } From e606f6d0edaa45e7568fca93ca4486216cae4829 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 15:15:10 +0200 Subject: [PATCH 5/7] cleanups --- src/modules/agents.types.ts | 26 ++----------- src/modules/entities.types.ts | 67 ++++++---------------------------- src/modules/functions.types.ts | 24 ++---------- 3 files changed, 18 insertions(+), 99 deletions(-) diff --git a/src/modules/agents.types.ts b/src/modules/agents.types.ts index 14fb155..0be5bae 100644 --- a/src/modules/agents.types.ts +++ b/src/modules/agents.types.ts @@ -4,30 +4,12 @@ import { ModelFilterParams } from "../types.js"; /** * Registry of agent names. - * - * This interface is designed to be augmented by generated type declaration files. - * When augmented, it enables autocomplete for agent names in methods like `createConversation`. - * - * @example - * ```typescript - * // In your generated types.d.ts file: - * declare module '@base44/sdk' { - * interface AgentNameRegistry { - * support_agent: true; - * sales_bot: true; - * } - * } - * - * // Then in your code: - * await base44.agents.createConversation({ - * agent_name: 'support_agent' // ✅ Autocomplete shows: 'support_agent' | 'sales_bot' - * }); - * ``` + * Augmented by `base44 types generate` to enable autocomplete for agent names. */ export interface AgentNameRegistry {} /** - * Agent name type - uses registry keys if augmented, otherwise falls back to string. + * Agent name type - uses registry keys if augmented, otherwise string. */ export type AgentName = keyof AgentNameRegistry extends never ? string @@ -165,7 +147,7 @@ export interface AgentMessage { * Parameters for creating a new conversation. */ export interface CreateConversationParams { - /** The name of the agent to create a conversation with. When AgentNameRegistry is augmented, provides autocomplete. */ + /** The name of the agent to create a conversation with. */ agent_name: AgentName; /** Optional metadata to attach to the conversation. */ metadata?: Record; @@ -390,7 +372,7 @@ export interface AgentsModule { * Generates a URL that users can use to connect with the agent through WhatsApp. * The URL includes authentication if a token is available. * - * @param agentName - The name of the agent. When AgentNameRegistry is augmented, provides autocomplete. + * @param agentName - The name of the agent. * @returns WhatsApp connection URL. * * @example diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index 37e460d..7a044f8 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -5,9 +5,7 @@ export type RealtimeEventType = "create" | "update" | "delete"; /** * Payload received when a realtime event occurs. - * - * @typeParam T - The entity type for the data field. Defaults to `any`. - */ +g */ export interface RealtimeEvent { /** The type of change that occurred */ type: RealtimeEventType; @@ -21,8 +19,6 @@ export interface RealtimeEvent { /** * Callback function invoked when a realtime event occurs. - * - * @typeParam T - The entity type for the event data. Defaults to `any`. */ export type RealtimeCallback = (event: RealtimeEvent) => void; @@ -46,34 +42,18 @@ export interface DeleteManyResult { /** * Result returned when importing entities from a file. - * - * @typeParam T - The entity type for imported records. Defaults to `any`. */ export interface ImportResult { /** Status of the import operation */ status: "success" | "error"; - /** Details message, e.g., "Successfully imported 3 entities with RLS enforcement" */ + /** Details message */ details: string | null; /** Array of created entity objects when successful, or null on error */ output: T[] | null; } /** - * Sort field type for entity queries. - * - * Supports ascending (no prefix or `'+'`) and descending (`'-'`) sorting. - * - * @typeParam T - The entity type to derive sortable fields from. - * - * @example - * ```typescript - * // Ascending sort (default) - * 'created_date' - * '+created_date' - * - * // Descending sort - * '-created_date' - * ``` + * Sort field for entity queries. Prefix with `-` for descending order. */ export type SortField = | (keyof T & string) @@ -82,26 +62,7 @@ export type SortField = /** * Registry mapping entity names to their TypeScript types. - * - * This interface is designed to be augmented by generated type declaration files. - * When augmented, it enables type-safe entity access throughout your application. - * - * @example - * ```typescript - * // In your generated types.d.ts file: - * declare module '@base44/sdk' { - * interface EntityTypeRegistry { - * Task: { title: string; completed: boolean }; - * User: { email: string; name: string }; - * } - * } - * - * // Then in your code: - * const task = await base44.entities.Task.create({ - * title: 'Buy groceries', // ✅ Type-checked - * completed: false - * }); - * ``` + * Augmented by `base44 types generate` to enable type-safe entity access. */ export interface EntityTypeRegistry {} @@ -109,8 +70,6 @@ export interface EntityTypeRegistry {} * Entity handler providing CRUD operations for a specific entity type. * * Each entity in the app gets a handler with these methods for managing data. - * - * @typeParam T - The entity type. Defaults to `any` for backward compatibility. */ export interface EntityHandler { /** @@ -121,12 +80,11 @@ export interface EntityHandler { * * **Note:** The maximum limit is 5,000 items per request. * - * @typeParam K - The fields to include in the response. Defaults to all fields. * @param sort - Sort parameter, such as `'-created_date'` for descending. Defaults to `'-created_date'`. * @param limit - Maximum number of results to return. Defaults to `50`. * @param skip - Number of results to skip for pagination. Defaults to `0`. * @param fields - Array of field names to include in the response. Defaults to all fields. - * @returns Promise resolving to an array of records with selected fields. + * @returns Promise resolving to an array of records. * * @example * ```typescript @@ -168,7 +126,6 @@ export interface EntityHandler { * * **Note:** The maximum limit is 5,000 items per request. * - * @typeParam K - The fields to include in the response. Defaults to all fields. * @param query - Query object with field-value pairs. Each key should be a field name * from your entity schema, and each value is the criteria to match. Records matching all * specified criteria are returned. Field names are case-sensitive. @@ -176,7 +133,7 @@ export interface EntityHandler { * @param limit - Maximum number of results to return. Defaults to `50`. * @param skip - Number of results to skip for pagination. Defaults to `0`. * @param fields - Array of field names to include in the response. Defaults to all fields. - * @returns Promise resolving to an array of filtered records with selected fields. + * @returns Promise resolving to an array of filtered records. * * @example * ```typescript @@ -328,7 +285,7 @@ export interface EntityHandler { * status: 'completed', * priority: 'low' * }); - * console.log('Deleted:', result); + * console.log('Deleted:', result.deleted); * ``` */ deleteMany(query: Partial): Promise; @@ -361,7 +318,7 @@ export interface EntityHandler { * The file format should match your entity structure. Requires a browser environment and can't be used in the backend. * * @param file - File object to import. - * @returns Promise resolving to the import result containing status, details, and created records. + * @returns Promise resolving to the import result. * * @example * ```typescript @@ -370,8 +327,8 @@ export interface EntityHandler { * const file = event.target.files?.[0]; * if (file) { * const result = await base44.entities.MyEntity.importEntities(file); - * if (result.status === 'success' && result.output) { - * console.log(`Imported ${result.output.length} records`); + * if (result.status === 'success') { + * console.log(`Imported ${result.output?.length} records`); * } * } * }; @@ -409,7 +366,6 @@ export interface EntityHandler { /** * Typed entities module - maps registry keys to typed handlers. - * Only used when EntityTypeRegistry is augmented. */ type TypedEntitiesModule = { [K in keyof EntityTypeRegistry]: EntityHandler; @@ -417,7 +373,6 @@ type TypedEntitiesModule = { /** * Dynamic entities module - allows any entity name with untyped handler. - * Used as fallback and for entities not in the registry. */ type DynamicEntitiesModule = { [entityName: string]: EntityHandler; @@ -432,7 +387,7 @@ type DynamicEntitiesModule = { * Entities are accessed dynamically using the pattern: * `base44.entities.EntityName.method()` * - * When {@link EntityTypeRegistry} is augmented (via generated types.d.ts), + * When {@link EntityTypeRegistry} is augmented (via `base44 types generate`), * entity access becomes type-safe with autocomplete and type checking. * * This module is available to use with a client in all three authentication modes: diff --git a/src/modules/functions.types.ts b/src/modules/functions.types.ts index 033af98..e047485 100644 --- a/src/modules/functions.types.ts +++ b/src/modules/functions.types.ts @@ -1,29 +1,11 @@ /** * Registry of function names. - * - * This interface is designed to be augmented by generated type declaration files. - * When augmented, it enables autocomplete for function names in the `invoke` method. - * - * @example - * ```typescript - * // In your generated types.d.ts file: - * declare module '@base44/sdk' { - * interface FunctionNameRegistry { - * calculateTotal: true; - * processImage: true; - * } - * } - * - * // Then in your code: - * await base44.functions.invoke('calculateTotal', { ... }); - * // ^^^^^^^^^^^^^^^^ - * // ✅ Autocomplete shows: 'calculateTotal' | 'processImage' - * ``` + * Augmented by `base44 types generate` to enable autocomplete for function names. */ export interface FunctionNameRegistry {} /** - * Function name type - uses registry keys if augmented, otherwise falls back to string. + * Function name type - uses registry keys if augmented, otherwise string. */ export type FunctionName = keyof FunctionNameRegistry extends never ? string @@ -48,7 +30,7 @@ export interface FunctionsModule { * the result. If any parameter is a `File` object, the request will automatically be * sent as `multipart/form-data`. Otherwise, it will be sent as JSON. * - * @param functionName - The name of the function to invoke. When FunctionNameRegistry is augmented, provides autocomplete. + * @param functionName - The name of the function to invoke. * @param data - An object containing named parameters for the function. * @returns Promise resolving to the function's response. The `data` property contains the data returned by the function, if there is any. * From 24b39a46b3991462b8aec72e3a0e843e039d5f28 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Thu, 5 Feb 2026 15:23:04 +0200 Subject: [PATCH 6/7] align comments --- src/modules/agents.types.ts | 2 +- src/modules/entities.types.ts | 45 +++++++++++++++++++++++++--------- src/modules/functions.types.ts | 2 +- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/modules/agents.types.ts b/src/modules/agents.types.ts index 0be5bae..8b71cc4 100644 --- a/src/modules/agents.types.ts +++ b/src/modules/agents.types.ts @@ -4,7 +4,7 @@ import { ModelFilterParams } from "../types.js"; /** * Registry of agent names. - * Augmented by `base44 types generate` to enable autocomplete for agent names. + * Augment this interface to enable autocomplete for agent names. */ export interface AgentNameRegistry {} diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index 7a044f8..a9c3b16 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -5,7 +5,9 @@ export type RealtimeEventType = "create" | "update" | "delete"; /** * Payload received when a realtime event occurs. -g */ + * + * @typeParam T - The entity type for the data field. Defaults to `any`. + */ export interface RealtimeEvent { /** The type of change that occurred */ type: RealtimeEventType; @@ -19,6 +21,8 @@ export interface RealtimeEvent { /** * Callback function invoked when a realtime event occurs. + * + * @typeParam T - The entity type for the event data. Defaults to `any`. */ export type RealtimeCallback = (event: RealtimeEvent) => void; @@ -42,18 +46,34 @@ export interface DeleteManyResult { /** * Result returned when importing entities from a file. + * + * @typeParam T - The entity type for imported records. Defaults to `any`. */ export interface ImportResult { /** Status of the import operation */ status: "success" | "error"; - /** Details message */ + /** Details message, e.g., "Successfully imported 3 entities with RLS enforcement" */ details: string | null; /** Array of created entity objects when successful, or null on error */ output: T[] | null; } /** - * Sort field for entity queries. Prefix with `-` for descending order. + * Sort field type for entity queries. + * + * Supports ascending (no prefix or `'+'`) and descending (`'-'`) sorting. + * + * @typeParam T - The entity type to derive sortable fields from. + * + * @example + * ```typescript + * // Ascending sort (default) + * 'created_date' + * '+created_date' + * + * // Descending sort + * '-created_date' + * ``` */ export type SortField = | (keyof T & string) @@ -62,7 +82,7 @@ export type SortField = /** * Registry mapping entity names to their TypeScript types. - * Augmented by `base44 types generate` to enable type-safe entity access. + * Augment this interface to enable type-safe entity access. */ export interface EntityTypeRegistry {} @@ -70,6 +90,8 @@ export interface EntityTypeRegistry {} * Entity handler providing CRUD operations for a specific entity type. * * Each entity in the app gets a handler with these methods for managing data. + * + * @typeParam T - The entity type. Defaults to `any` for backward compatibility. */ export interface EntityHandler { /** @@ -80,11 +102,12 @@ export interface EntityHandler { * * **Note:** The maximum limit is 5,000 items per request. * + * @typeParam K - The fields to include in the response. Defaults to all fields. * @param sort - Sort parameter, such as `'-created_date'` for descending. Defaults to `'-created_date'`. * @param limit - Maximum number of results to return. Defaults to `50`. * @param skip - Number of results to skip for pagination. Defaults to `0`. * @param fields - Array of field names to include in the response. Defaults to all fields. - * @returns Promise resolving to an array of records. + * @returns Promise resolving to an array of records with selected fields. * * @example * ```typescript @@ -126,6 +149,7 @@ export interface EntityHandler { * * **Note:** The maximum limit is 5,000 items per request. * + * @typeParam K - The fields to include in the response. Defaults to all fields. * @param query - Query object with field-value pairs. Each key should be a field name * from your entity schema, and each value is the criteria to match. Records matching all * specified criteria are returned. Field names are case-sensitive. @@ -133,7 +157,7 @@ export interface EntityHandler { * @param limit - Maximum number of results to return. Defaults to `50`. * @param skip - Number of results to skip for pagination. Defaults to `0`. * @param fields - Array of field names to include in the response. Defaults to all fields. - * @returns Promise resolving to an array of filtered records. + * @returns Promise resolving to an array of filtered records with selected fields. * * @example * ```typescript @@ -318,7 +342,7 @@ export interface EntityHandler { * The file format should match your entity structure. Requires a browser environment and can't be used in the backend. * * @param file - File object to import. - * @returns Promise resolving to the import result. + * @returns Promise resolving to the import result containing status, details, and created records. * * @example * ```typescript @@ -327,8 +351,8 @@ export interface EntityHandler { * const file = event.target.files?.[0]; * if (file) { * const result = await base44.entities.MyEntity.importEntities(file); - * if (result.status === 'success') { - * console.log(`Imported ${result.output?.length} records`); + * if (result.status === 'success' && result.output) { + * console.log(`Imported ${result.output.length} records`); * } * } * }; @@ -387,9 +411,6 @@ type DynamicEntitiesModule = { * Entities are accessed dynamically using the pattern: * `base44.entities.EntityName.method()` * - * When {@link EntityTypeRegistry} is augmented (via `base44 types generate`), - * entity access becomes type-safe with autocomplete and type checking. - * * This module is available to use with a client in all three authentication modes: * * - **Anonymous or User authentication** (`base44.entities`): Access is scoped to the current user's permissions. Anonymous users can only access public entities, while authenticated users can access entities they have permission to view or modify. diff --git a/src/modules/functions.types.ts b/src/modules/functions.types.ts index e047485..95dd123 100644 --- a/src/modules/functions.types.ts +++ b/src/modules/functions.types.ts @@ -1,6 +1,6 @@ /** * Registry of function names. - * Augmented by `base44 types generate` to enable autocomplete for function names. + * Augment this interface to enable autocomplete for function names. */ export interface FunctionNameRegistry {} From a1ebbbda4ad106c5a84205ed2f276e752990dc8e Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Sun, 8 Feb 2026 00:36:47 +0200 Subject: [PATCH 7/7] update entities and function TypeRegistry in tests --- tests/unit/entities.test.ts | 6 +++--- tests/unit/functions.test.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/unit/entities.test.ts b/tests/unit/entities.test.ts index 904587f..2a94e40 100644 --- a/tests/unit/entities.test.ts +++ b/tests/unit/entities.test.ts @@ -12,10 +12,10 @@ interface Todo { completed: boolean; } -// Declaration merging: extend EntitiesModule with typed Todo handler +// Module augmentation: register Todo type in EntityTypeRegistry declare module "../../src/modules/entities.types.ts" { - interface EntitiesModule { - Todo: EntityHandler; + interface EntityTypeRegistry { + Todo: Todo; } } diff --git a/tests/unit/functions.test.ts b/tests/unit/functions.test.ts index 3cd7d59..f886d30 100644 --- a/tests/unit/functions.test.ts +++ b/tests/unit/functions.test.ts @@ -2,6 +2,15 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest"; import nock from "nock"; import { createClient } from "../../src/index.ts"; +// Module augmentation: register function names in FunctionNameRegistry +declare module "../../src/modules/functions.types.ts" { + interface FunctionNameRegistry { + sendNotification: true; + processOrder: true; + generateReport: true; + } +} + describe("Functions Module", () => { let base44: ReturnType; let scope;