From 9f2702ca3cb57b888cdb857fb81cf0acb6bbddfc Mon Sep 17 00:00:00 2001 From: Kfir Stri Date: Sun, 11 Jan 2026 16:01:58 +0200 Subject: [PATCH 1/4] feat(login): implement login flow with http (#10) * fix issue with error throwing, add refresh mechanism * added refresh logic fixes * fix package version issue and add a userInfo method * actual call the getUserInfo service * added userInfo call and log * small ui changes * get user info with access token * tiny changes * fix issue with correct way to use ky --- src/cli/commands/entities/push.ts | 56 +++++++++++++++++++ src/cli/index.ts | 4 ++ .../config/{resource.ts => baseResource.ts} | 1 + src/core/config/index.ts | 2 +- src/core/config/project.ts | 8 +-- src/core/consts.ts | 8 +++ src/core/resources/entity/api.ts | 36 ++++++++++++ src/core/resources/entity/index.ts | 1 + src/core/resources/entity/resource.ts | 4 +- src/core/resources/entity/schema.ts | 7 +++ src/core/resources/function/resource.ts | 3 +- src/core/utils/httpClient.ts | 21 ++++++- 12 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 src/cli/commands/entities/push.ts rename src/core/config/{resource.ts => baseResource.ts} (64%) create mode 100644 src/core/resources/entity/api.ts diff --git a/src/cli/commands/entities/push.ts b/src/cli/commands/entities/push.ts new file mode 100644 index 00000000..0072589f --- /dev/null +++ b/src/cli/commands/entities/push.ts @@ -0,0 +1,56 @@ +import { Command } from "commander"; +import { log } from "@clack/prompts"; +import { pushEntities } from "@core/resources/entity/index.js"; +import { readProjectConfig } from "@core/index.js"; +import { runCommand, runTask } from "../../utils/index.js"; + +async function pushEntitiesAction(): Promise { + const { entities } = await readProjectConfig(); + + if (entities.length === 0) { + log.warn("No entities found in project"); + return; + } + + log.info(`Found ${entities.length} entities to push`); + + const result = await runTask( + "Pushing entities to Base44", + async () => { + return await pushEntities(entities); + }, + { + successMessage: "Entities pushed successfully", + errorMessage: "Failed to push entities", + } + ); + + // Print the results + if (result.created.length > 0) { + log.success(`Created: ${result.created.join(", ")}`); + } + if (result.updated.length > 0) { + log.success(`Updated: ${result.updated.join(", ")}`); + } + if (result.deleted.length > 0) { + log.warn(`Deleted: ${result.deleted.join(", ")}`); + } + + if ( + result.created.length === 0 && + result.updated.length === 0 && + result.deleted.length === 0 + ) { + log.info("No changes detected"); + } +} + +export const entitiesPushCommand = new Command("entities") + .description("Manage project entities") + .addCommand( + new Command("push") + .description("Push local entities to Base44") + .action(async () => { + await runCommand(pushEntitiesAction); + }) + ); diff --git a/src/cli/index.ts b/src/cli/index.ts index b7ee129d..b510f283 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -5,6 +5,7 @@ import { loginCommand } from "./commands/auth/login.js"; import { whoamiCommand } from "./commands/auth/whoami.js"; import { logoutCommand } from "./commands/auth/logout.js"; import { showProjectCommand } from "./commands/project/show-project.js"; +import { entitiesPushCommand } from "./commands/entities/push.js"; import packageJson from "../../package.json"; const program = new Command(); @@ -24,5 +25,8 @@ program.addCommand(logoutCommand); // Register project commands program.addCommand(showProjectCommand); +// Register entities commands +program.addCommand(entitiesPushCommand); + // Parse command line arguments program.parse(); diff --git a/src/core/config/resource.ts b/src/core/config/baseResource.ts similarity index 64% rename from src/core/config/resource.ts rename to src/core/config/baseResource.ts index da19f94b..b3f3f5a4 100644 --- a/src/core/config/resource.ts +++ b/src/core/config/baseResource.ts @@ -1,3 +1,4 @@ export interface Resource { readAll: (dir: string) => Promise; + push: (items: T[]) => Promise; } diff --git a/src/core/config/index.ts b/src/core/config/index.ts index efda2993..78ea40db 100644 --- a/src/core/config/index.ts +++ b/src/core/config/index.ts @@ -1,3 +1,3 @@ -export type * from "./resource.js"; +export type * from "./baseResource.js"; export * from "./project.js"; export * from "./app.js"; diff --git a/src/core/config/project.ts b/src/core/config/project.ts index 5e8ef247..0902e733 100644 --- a/src/core/config/project.ts +++ b/src/core/config/project.ts @@ -10,8 +10,8 @@ import type { FunctionConfig } from "../resources/function/index.js"; export const ProjectConfigSchema = z.looseObject({ name: z.string().min(1, "Project name cannot be empty"), - entitySrc: z.string().default("./entities"), - functionSrc: z.string().default("./functions"), + entitiesDir: z.string().default("./entities"), + functionsDir: z.string().default("./functions"), }); export type ProjectConfig = z.infer; @@ -88,8 +88,8 @@ export async function readProjectConfig( const configDir = dirname(configPath); const [entities, functions] = await Promise.all([ - entityResource.readAll(join(configDir, project.entitySrc)), - functionResource.readAll(join(configDir, project.functionSrc)), + entityResource.readAll(join(configDir, project.entitiesDir)), + functionResource.readAll(join(configDir, project.functionsDir)), ]); return { diff --git a/src/core/consts.ts b/src/core/consts.ts index a7909326..b1be0ee5 100644 --- a/src/core/consts.ts +++ b/src/core/consts.ts @@ -28,3 +28,11 @@ const DEFAULT_API_URL = "https://app.base44.com"; export function getBase44ApiUrl(): string { return process.env.BASE44_API_URL || DEFAULT_API_URL; } + +export function getAppId(): string { + const appId = process.env.BASE44_CLIENT_ID; + if (!appId) { + throw new Error("BASE44_CLIENT_ID environment variable is not set"); + } + return appId; +} diff --git a/src/core/resources/entity/api.ts b/src/core/resources/entity/api.ts new file mode 100644 index 00000000..9ad3820e --- /dev/null +++ b/src/core/resources/entity/api.ts @@ -0,0 +1,36 @@ +import { getAppClient } from "@core/utils"; +import { SyncEntitiesResponseSchema } from "./schema.js"; +import type { SyncEntitiesResponse, Entity } from "./schema.js"; + +const appClient = getAppClient(); + +export async function pushEntities( + entities: Entity[] +): Promise { + const schemaSyncPayload = entities.reduce((acc, current) => { + return { + ...acc, + [current.name]: current, + }; + }, {}); + + const response = await appClient.put("entities-schemas/sync-all", { + json: { + entityNameToSchema: schemaSyncPayload, + }, + throwHttpErrors: false, + }); + + if (!response.ok) { + const errorJson: { message: string } = await response.json(); + if (response.status === 428) { + throw new Error(`Failed to delete entity: ${errorJson.message}`); + } + + throw new Error(`Error occurred while syncing entities ${errorJson.message}`); + } + + const result = SyncEntitiesResponseSchema.parse(await response.json()); + + return result; +} diff --git a/src/core/resources/entity/index.ts b/src/core/resources/entity/index.ts index c061bbba..a13b1ced 100644 --- a/src/core/resources/entity/index.ts +++ b/src/core/resources/entity/index.ts @@ -1,3 +1,4 @@ export * from "./schema.js"; export * from "./config.js"; export * from "./resource.js"; +export * from "./api.js"; \ No newline at end of file diff --git a/src/core/resources/entity/resource.ts b/src/core/resources/entity/resource.ts index 0323a437..80b010f9 100644 --- a/src/core/resources/entity/resource.ts +++ b/src/core/resources/entity/resource.ts @@ -1,7 +1,9 @@ -import type { Resource } from "@core/config/resource.js"; +import type { Resource } from "@core/config/baseResource.js"; import type { Entity } from "./schema.js"; import { readAllEntities } from "./config.js"; +import { pushEntities } from "./api.js"; export const entityResource: Resource = { readAll: readAllEntities, + push: pushEntities, }; diff --git a/src/core/resources/entity/schema.ts b/src/core/resources/entity/schema.ts index d615c11a..2a75fea9 100644 --- a/src/core/resources/entity/schema.ts +++ b/src/core/resources/entity/schema.ts @@ -34,3 +34,10 @@ export type EntityProperty = z.infer; export type EntityPolicies = z.infer; export type Entity = z.infer; +export const SyncEntitiesResponseSchema = z.object({ + created: z.array(z.string()), + updated: z.array(z.string()), + deleted: z.array(z.string()), +}); + +export type SyncEntitiesResponse = z.infer; diff --git a/src/core/resources/function/resource.ts b/src/core/resources/function/resource.ts index 9382f230..b6b8e4d9 100644 --- a/src/core/resources/function/resource.ts +++ b/src/core/resources/function/resource.ts @@ -1,7 +1,8 @@ -import type { Resource } from "@core/config/resource.js"; +import type { Resource } from "@core/config/baseResource.js"; import type { FunctionConfig } from "./schema.js"; import { readAllFunctions } from "./config.js"; export const functionResource: Resource = { readAll: readAllFunctions, + push: async () => {}, // noop }; diff --git a/src/core/utils/httpClient.ts b/src/core/utils/httpClient.ts index f16bdb4a..fff85c76 100644 --- a/src/core/utils/httpClient.ts +++ b/src/core/utils/httpClient.ts @@ -1,6 +1,6 @@ import ky from "ky"; import type { KyRequest, KyResponse, NormalizedOptions } from "ky"; -import { getBase44ApiUrl } from "../consts.js"; +import { getAppId, getBase44ApiUrl } from "../consts.js"; import { readAuth, refreshAndSaveTokens, @@ -42,7 +42,7 @@ async function handleUnauthorized( }); } -const httpClient = ky.create({ +const base44Client = ky.create({ prefixUrl: getBase44ApiUrl(), headers: { "User-Agent": "Base44 CLI", @@ -72,4 +72,19 @@ const httpClient = ky.create({ }, }); -export default httpClient; +let _appClient: typeof base44Client | null = null; + +/** + * Returns an HTTP client scoped to the current app. + * Lazily initialized on first call (app ID must be set via BASE44_CLIENT_ID env var). + */ +function getAppClient() { + if (!_appClient) { + _appClient = base44Client.extend({ + prefixUrl: new URL(`/api/apps/${getAppId()}/`, getBase44ApiUrl()).href, + }); + } + return _appClient; +} + +export { base44Client, getAppClient }; From 05daacae3860a06a8690add6a52f7d1b94854702 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Sun, 11 Jan 2026 16:20:44 +0200 Subject: [PATCH 2/4] move the client init into the function --- src/core/resources/entity/api.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/resources/entity/api.ts b/src/core/resources/entity/api.ts index 9ad3820e..55046ced 100644 --- a/src/core/resources/entity/api.ts +++ b/src/core/resources/entity/api.ts @@ -2,11 +2,10 @@ import { getAppClient } from "@core/utils"; import { SyncEntitiesResponseSchema } from "./schema.js"; import type { SyncEntitiesResponse, Entity } from "./schema.js"; -const appClient = getAppClient(); - export async function pushEntities( entities: Entity[] ): Promise { + const appClient = getAppClient(); const schemaSyncPayload = entities.reduce((acc, current) => { return { ...acc, @@ -27,7 +26,9 @@ export async function pushEntities( throw new Error(`Failed to delete entity: ${errorJson.message}`); } - throw new Error(`Error occurred while syncing entities ${errorJson.message}`); + throw new Error( + `Error occurred while syncing entities ${errorJson.message}` + ); } const result = SyncEntitiesResponseSchema.parse(await response.json()); From b0ea6b62d95bad60314056ac8cb3832eaa8f54b9 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Sun, 11 Jan 2026 16:22:35 +0200 Subject: [PATCH 3/4] dont lazy load the appclient --- src/core/config/baseResource.ts | 2 +- src/core/resources/entity/index.ts | 2 +- src/core/resources/function/resource.ts | 1 - src/core/utils/httpClient.ts | 12 +++--------- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/core/config/baseResource.ts b/src/core/config/baseResource.ts index b3f3f5a4..139ce292 100644 --- a/src/core/config/baseResource.ts +++ b/src/core/config/baseResource.ts @@ -1,4 +1,4 @@ export interface Resource { readAll: (dir: string) => Promise; - push: (items: T[]) => Promise; + push?: (items: T[]) => Promise; } diff --git a/src/core/resources/entity/index.ts b/src/core/resources/entity/index.ts index a13b1ced..3ff8a353 100644 --- a/src/core/resources/entity/index.ts +++ b/src/core/resources/entity/index.ts @@ -1,4 +1,4 @@ export * from "./schema.js"; export * from "./config.js"; export * from "./resource.js"; -export * from "./api.js"; \ No newline at end of file +export * from "./api.js"; diff --git a/src/core/resources/function/resource.ts b/src/core/resources/function/resource.ts index b6b8e4d9..144dcee8 100644 --- a/src/core/resources/function/resource.ts +++ b/src/core/resources/function/resource.ts @@ -4,5 +4,4 @@ import { readAllFunctions } from "./config.js"; export const functionResource: Resource = { readAll: readAllFunctions, - push: async () => {}, // noop }; diff --git a/src/core/utils/httpClient.ts b/src/core/utils/httpClient.ts index fff85c76..bddd8376 100644 --- a/src/core/utils/httpClient.ts +++ b/src/core/utils/httpClient.ts @@ -72,19 +72,13 @@ const base44Client = ky.create({ }, }); -let _appClient: typeof base44Client | null = null; - /** * Returns an HTTP client scoped to the current app. - * Lazily initialized on first call (app ID must be set via BASE44_CLIENT_ID env var). */ function getAppClient() { - if (!_appClient) { - _appClient = base44Client.extend({ - prefixUrl: new URL(`/api/apps/${getAppId()}/`, getBase44ApiUrl()).href, - }); - } - return _appClient; + return base44Client.extend({ + prefixUrl: new URL(`/api/apps/${getAppId()}/`, getBase44ApiUrl()).href, + }); } export { base44Client, getAppClient }; From 7bd3a758ca2eeedbbd0585448eef324fa03b6e08 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Sun, 11 Jan 2026 16:30:03 +0200 Subject: [PATCH 4/4] performance fix for entity api --- src/core/resources/entity/api.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/core/resources/entity/api.ts b/src/core/resources/entity/api.ts index 55046ced..296818b8 100644 --- a/src/core/resources/entity/api.ts +++ b/src/core/resources/entity/api.ts @@ -1,4 +1,4 @@ -import { getAppClient } from "@core/utils"; +import { getAppClient } from "@core/utils/index.js"; import { SyncEntitiesResponseSchema } from "./schema.js"; import type { SyncEntitiesResponse, Entity } from "./schema.js"; @@ -6,12 +6,9 @@ export async function pushEntities( entities: Entity[] ): Promise { const appClient = getAppClient(); - const schemaSyncPayload = entities.reduce((acc, current) => { - return { - ...acc, - [current.name]: current, - }; - }, {}); + const schemaSyncPayload = Object.fromEntries( + entities.map((entity) => [entity.name, entity]) + ); const response = await appClient.put("entities-schemas/sync-all", { json: {