Skip to content
Open
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ The CLI will guide you through project setup. For step-by-step tutorials, see th
| [`connectors pull`](https://docs.base44.com/developers/references/cli/commands/connectors-pull) | Pull connectors from Base44 to local files |
| [`connectors push`](https://docs.base44.com/developers/references/cli/commands/connectors-push) | Push local connectors to Base44 |
| [`entities push`](https://docs.base44.com/developers/references/cli/commands/entities-push) | Push local entities to Base44 |
| [`entities records list`](https://docs.base44.com/developers/references/cli/commands/entities-records) | List entity records |
| [`entities records get`](https://docs.base44.com/developers/references/cli/commands/entities-records) | Get a single entity record by ID |
| [`entities records create`](https://docs.base44.com/developers/references/cli/commands/entities-records) | Create a new entity record |
| [`entities records update`](https://docs.base44.com/developers/references/cli/commands/entities-records) | Update an entity record |
| [`entities records delete`](https://docs.base44.com/developers/references/cli/commands/entities-records) | Delete an entity record |
| [`functions deploy`](https://docs.base44.com/developers/references/cli/commands/functions-deploy) | Deploy local functions to Base44 |
| [`site deploy`](https://docs.base44.com/developers/references/cli/commands/site-deploy) | Deploy built site files to Base44 hosting |
| [`site open`](https://docs.base44.com/developers/references/cli/commands/site-open) | Open the published site in your browser |
Expand Down
11 changes: 11 additions & 0 deletions src/cli/commands/entities/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Command } from "commander";
import type { CLIContext } from "@/cli/types.js";
import { getEntitiesPushCommand } from "./push.js";
import { getRecordsCommand } from "./records/index.js";

export function getEntitiesCommand(context: CLIContext): Command {
return new Command("entities")
.description("Manage project entities")
.addCommand(getEntitiesPushCommand(context))
.addCommand(getRecordsCommand(context));
}
14 changes: 5 additions & 9 deletions src/cli/commands/entities/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,9 @@ async function pushEntitiesAction(): Promise<RunCommandResult> {
}

export function getEntitiesPushCommand(context: CLIContext): Command {
return new Command("entities")
.description("Manage project entities")
.addCommand(
new Command("push")
.description("Push local entities to Base44")
.action(async () => {
await runCommand(pushEntitiesAction, { requireAuth: true }, context);
}),
);
return new Command("push")
.description("Push local entity schemas to Base44")
.action(async () => {
await runCommand(pushEntitiesAction, { requireAuth: true }, context);
});
}
52 changes: 52 additions & 0 deletions src/cli/commands/entities/records/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { log } from "@clack/prompts";
import { Command } from "commander";
import type { CLIContext } from "@/cli/types.js";
import { runCommand, runTask } from "@/cli/utils/index.js";
import type { RunCommandResult } from "@/cli/utils/runCommand.js";
import { createRecord } from "@/core/resources/entity/index.js";
import { parseRecordData } from "./parseRecordData.js";

interface CreateRecordCommandOptions {
data?: string;
file?: string;
}

async function createRecordAction(
entityName: string,
options: CreateRecordCommandOptions,
): Promise<RunCommandResult> {
const data = await parseRecordData(
options,
'{"name": "John", "email": "john@example.com"}',
);

const record = await runTask(
`Creating ${entityName} record...`,
async () => {
return await createRecord(entityName, data);
},
{
successMessage: `Created ${entityName} record`,
errorMessage: `Failed to create ${entityName} record`,
},
);

log.info(JSON.stringify(record, null, 2));

return { outroMessage: `Record created with ID: ${record.id}` };
}

export function getRecordsCreateCommand(context: CLIContext): Command {
return new Command("create")
.description("Create a new entity record")
.argument("<entity-name>", "Name of the entity (e.g. Users, Products)")
.option("-d, --data <json>", "JSON object with record data")
.option("--file <path>", "Read record data from a JSON/JSONC file")
.action(async (entityName: string, options: CreateRecordCommandOptions) => {
await runCommand(
() => createRecordAction(entityName, options),
{ requireAuth: true },
context,
);
});
}
61 changes: 61 additions & 0 deletions src/cli/commands/entities/records/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { confirm } from "@clack/prompts";
import { Command } from "commander";
import { CLIExitError } from "@/cli/errors.js";
import type { CLIContext } from "@/cli/types.js";
import { runCommand, runTask } from "@/cli/utils/index.js";
import type { RunCommandResult } from "@/cli/utils/runCommand.js";
import { deleteRecord } from "@/core/resources/entity/index.js";

interface DeleteRecordCommandOptions {
yes?: boolean;
}

async function deleteRecordAction(
entityName: string,
recordId: string,
options: DeleteRecordCommandOptions,
): Promise<RunCommandResult> {
if (!options.yes) {
const confirmed = await confirm({
message: `Delete ${entityName} record ${recordId}?`,
});

if (confirmed !== true) {
throw new CLIExitError(0);
}
}

await runTask(
`Deleting ${entityName} record...`,
async () => {
return await deleteRecord(entityName, recordId);
},
{
successMessage: `Deleted ${entityName} record`,
errorMessage: `Failed to delete ${entityName} record`,
},
);

return { outroMessage: `Record ${recordId} deleted` };
}

export function getRecordsDeleteCommand(context: CLIContext): Command {
return new Command("delete")
.description("Delete an entity record")
.argument("<entity-name>", "Name of the entity (e.g. Users, Products)")
.argument("<record-id>", "ID of the record to delete")
.option("-y, --yes", "Skip confirmation prompt")
.action(
async (
entityName: string,
recordId: string,
options: DeleteRecordCommandOptions,
) => {
await runCommand(
() => deleteRecordAction(entityName, recordId, options),
{ requireAuth: true },
context,
);
},
);
}
40 changes: 40 additions & 0 deletions src/cli/commands/entities/records/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { log } from "@clack/prompts";
import { Command } from "commander";
import type { CLIContext } from "@/cli/types.js";
import { runCommand, runTask } from "@/cli/utils/index.js";
import type { RunCommandResult } from "@/cli/utils/runCommand.js";
import { getRecord } from "@/core/resources/entity/index.js";

async function getRecordAction(
entityName: string,
recordId: string,
): Promise<RunCommandResult> {
const record = await runTask(
`Fetching ${entityName} record...`,
async () => {
return await getRecord(entityName, recordId);
},
{
successMessage: `Fetched ${entityName} record`,
errorMessage: `Failed to fetch ${entityName} record`,
},
);

log.info(JSON.stringify(record, null, 2));

return {};
}

export function getRecordsGetCommand(context: CLIContext): Command {
return new Command("get")
.description("Get a single entity record by ID")
.argument("<entity-name>", "Name of the entity (e.g. Users, Products)")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want any validation for this thing? I guess the backend will throw if entity does not exists

.argument("<record-id>", "ID of the record")
.action(async (entityName: string, recordId: string) => {
await runCommand(
() => getRecordAction(entityName, recordId),
{ requireAuth: true },
context,
);
});
}
17 changes: 17 additions & 0 deletions src/cli/commands/entities/records/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Command } from "commander";
import type { CLIContext } from "@/cli/types.js";
import { getRecordsCreateCommand } from "./create.js";
import { getRecordsDeleteCommand } from "./delete.js";
import { getRecordsGetCommand } from "./get.js";
import { getRecordsListCommand } from "./list.js";
import { getRecordsUpdateCommand } from "./update.js";

export function getRecordsCommand(context: CLIContext): Command {
return new Command("records")
.description("CRUD operations on entity records")
.addCommand(getRecordsListCommand(context))
.addCommand(getRecordsGetCommand(context))
.addCommand(getRecordsCreateCommand(context))
.addCommand(getRecordsUpdateCommand(context))
.addCommand(getRecordsDeleteCommand(context));
}
67 changes: 67 additions & 0 deletions src/cli/commands/entities/records/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { log } from "@clack/prompts";
import { Command } from "commander";
import type { CLIContext } from "@/cli/types.js";
import { runCommand, runTask } from "@/cli/utils/index.js";
import type { RunCommandResult } from "@/cli/utils/runCommand.js";
import { listRecords } from "@/core/resources/entity/index.js";

interface ListRecordsCommandOptions {
filter?: string;
sort?: string;
limit?: string;
skip?: string;
fields?: string;
}

async function listRecordsAction(
entityName: string,
options: ListRecordsCommandOptions,
): Promise<RunCommandResult> {
const records = await runTask(
`Fetching ${entityName} records...`,
async () => {
return await listRecords(entityName, {
filter: options.filter,
sort: options.sort,
limit: options.limit ? Number(options.limit) : 50,
skip: options.skip ? Number(options.skip) : undefined,
fields: options.fields,
});
},
{
successMessage: `Fetched ${entityName} records`,
errorMessage: `Failed to fetch ${entityName} records`,
},
);

log.info(JSON.stringify(records, null, 2));

return { outroMessage: `Found ${records.length} ${entityName} record(s)` };
}

export function getRecordsListCommand(context: CLIContext): Command {
return new Command("list")
.description("List entity records")
.argument("<entity-name>", "Name of the entity (e.g. Users, Products)")
.option(
"-f, --filter <json>",
'JSON filter object (e.g. \'{"status":"active"}\' or \'{"age":{"$gt":18}}\')',
)
.option(
"-s, --sort <field>",
"Sort field name, prefix with - for descending (e.g. -created_date)",
)
.option("-l, --limit <n>", "Max number of records to return", "50")
.option("--skip <n>", "Number of records to skip (for pagination)")
.option(
"--fields <fields>",
"Comma-separated list of fields to return (e.g. id,name,email)",
)
.action(async (entityName: string, options: ListRecordsCommandOptions) => {
await runCommand(
() => listRecordsAction(entityName, options),
{ requireAuth: true },
context,
);
});
}
56 changes: 56 additions & 0 deletions src/cli/commands/entities/records/parseRecordData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import JSON5 from "json5";
import { InvalidInputError } from "@/core/errors.js";
import { readJsonFile } from "@/core/utils/fs.js";

interface RecordDataOptions {
data?: string;
file?: string;
}

export async function parseRecordData(
options: RecordDataOptions,
exampleHint?: string,
): Promise<Record<string, unknown>> {
if (options.data && options.file) {
throw new InvalidInputError(
"Cannot use both --data and --file. Choose one.",
{
hints: [
{
message: `Pass --data for inline JSON or --file for a JSON file path, not both.`,
},
],
},
);
}

if (options.data) {
try {
return JSON5.parse(options.data);
} catch {
throw new InvalidInputError(
"Invalid JSON in --data flag. Provide valid JSON.",
exampleHint
? { hints: [{ message: `Example: --data '${exampleHint}'` }] }
: undefined,
);
}
}

if (options.file) {
return readJsonFile(options.file) as Promise<Record<string, unknown>>;
}

throw new InvalidInputError(
"Provide record data with --data or --file flag",
exampleHint
? {
hints: [
{
message: `Example: --data '${exampleHint}' or --file record.json`,
},
],
}
: undefined,
);
}
57 changes: 57 additions & 0 deletions src/cli/commands/entities/records/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { log } from "@clack/prompts";
import { Command } from "commander";
import type { CLIContext } from "@/cli/types.js";
import { runCommand, runTask } from "@/cli/utils/index.js";
import type { RunCommandResult } from "@/cli/utils/runCommand.js";
import { updateRecord } from "@/core/resources/entity/index.js";
import { parseRecordData } from "./parseRecordData.js";

interface UpdateRecordCommandOptions {
data?: string;
file?: string;
}

async function updateRecordAction(
entityName: string,
recordId: string,
options: UpdateRecordCommandOptions,
): Promise<RunCommandResult> {
const data = await parseRecordData(options, '{"status": "active"}');

const record = await runTask(
`Updating ${entityName} record...`,
async () => {
return await updateRecord(entityName, recordId, data);
},
{
successMessage: `Updated ${entityName} record`,
errorMessage: `Failed to update ${entityName} record`,
},
);

log.info(JSON.stringify(record, null, 2));

return { outroMessage: `Record ${recordId} updated` };
}

export function getRecordsUpdateCommand(context: CLIContext): Command {
return new Command("update")
.description("Update an entity record")
.argument("<entity-name>", "Name of the entity (e.g. Users, Products)")
.argument("<record-id>", "ID of the record to update")
.option("-d, --data <json>", "JSON object with fields to update")
.option("--file <path>", "Read update data from a JSON/JSONC file")
.action(
async (
entityName: string,
recordId: string,
options: UpdateRecordCommandOptions,
) => {
await runCommand(
() => updateRecordAction(entityName, recordId, options),
{ requireAuth: true },
context,
);
},
);
}
Loading
Loading