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
56 changes: 56 additions & 0 deletions src/cli/commands/entities/push.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
})
);
4 changes: 4 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export interface Resource<T> {
readAll: (dir: string) => Promise<T[]>;
push?: (items: T[]) => Promise<unknown>;
}
2 changes: 1 addition & 1 deletion src/core/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export type * from "./resource.js";
export type * from "./baseResource.js";
export * from "./project.js";
export * from "./app.js";
8 changes: 4 additions & 4 deletions src/core/config/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof ProjectConfigSchema>;
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions src/core/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
34 changes: 34 additions & 0 deletions src/core/resources/entity/api.ts
Original file line number Diff line number Diff line change
@@ -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<SyncEntitiesResponse> {
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;
}
1 change: 1 addition & 0 deletions src/core/resources/entity/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./schema.js";
export * from "./config.js";
export * from "./resource.js";
export * from "./api.js";
4 changes: 3 additions & 1 deletion src/core/resources/entity/resource.ts
Original file line number Diff line number Diff line change
@@ -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<Entity> = {
readAll: readAllEntities,
push: pushEntities,
};
7 changes: 7 additions & 0 deletions src/core/resources/entity/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,10 @@ export type EntityProperty = z.infer<typeof EntityPropertySchema>;
export type EntityPolicies = z.infer<typeof EntityPoliciesSchema>;
export type Entity = z.infer<typeof EntitySchema>;

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<typeof SyncEntitiesResponseSchema>;
2 changes: 1 addition & 1 deletion src/core/resources/function/resource.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
15 changes: 12 additions & 3 deletions src/core/utils/httpClient.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -42,7 +42,7 @@ async function handleUnauthorized(
});
}

const httpClient = ky.create({
const base44Client = ky.create({
prefixUrl: getBase44ApiUrl(),
headers: {
"User-Agent": "Base44 CLI",
Expand Down Expand Up @@ -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 };