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 63% rename from src/core/config/resource.ts rename to src/core/config/baseResource.ts index da19f94b..139ce292 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..296818b8 --- /dev/null +++ b/src/core/resources/entity/api.ts @@ -0,0 +1,34 @@ +import { getAppClient } from "@core/utils/index.js"; +import { SyncEntitiesResponseSchema } from "./schema.js"; +import type { SyncEntitiesResponse, Entity } from "./schema.js"; + +export async function pushEntities( + entities: Entity[] +): Promise { + const appClient = getAppClient(); + const schemaSyncPayload = Object.fromEntries( + entities.map((entity) => [entity.name, entity]) + ); + + 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..3ff8a353 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"; 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..144dcee8 100644 --- a/src/core/resources/function/resource.ts +++ b/src/core/resources/function/resource.ts @@ -1,4 +1,4 @@ -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"; diff --git a/src/core/utils/httpClient.ts b/src/core/utils/httpClient.ts index f16bdb4a..bddd8376 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,13 @@ const httpClient = ky.create({ }, }); -export default httpClient; +/** + * Returns an HTTP client scoped to the current app. + */ +function getAppClient() { + return base44Client.extend({ + prefixUrl: new URL(`/api/apps/${getAppId()}/`, getBase44ApiUrl()).href, + }); +} + +export { base44Client, getAppClient };