Skip to content

Commit 3bd687f

Browse files
ayalcursoragent
andcommitted
feat: add entity records CRUD commands
Add `base44 entities records <list|get|create|update|delete>` commands for managing entity records via the CLI. Uses the new admin entities router (/api/apps/{app_id}/admin/entities) which authenticates via AppAdminRouter and bypasses RLS. Includes: - Core API layer (records-api.ts, records-schema.ts) - CLI commands for list, get, create, update, delete - Refactored entities command to support subcommands (push + records) - Integration tests and API mocks Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6ae8cb0 commit 3bd687f

14 files changed

Lines changed: 1003 additions & 11 deletions

File tree

src/cli/commands/entities/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Command } from "commander";
2+
import type { CLIContext } from "@/cli/types.js";
3+
import { getEntitiesPushCommand } from "./push.js";
4+
import { getRecordsCommand } from "./records/index.js";
5+
6+
export function getEntitiesCommand(context: CLIContext): Command {
7+
return new Command("entities")
8+
.description("Manage project entities")
9+
.addCommand(getEntitiesPushCommand(context))
10+
.addCommand(getRecordsCommand(context));
11+
}

src/cli/commands/entities/push.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,9 @@ async function pushEntitiesAction(): Promise<RunCommandResult> {
4242
}
4343

4444
export function getEntitiesPushCommand(context: CLIContext): Command {
45-
return new Command("entities")
46-
.description("Manage project entities")
47-
.addCommand(
48-
new Command("push")
49-
.description("Push local entities to Base44")
50-
.action(async () => {
51-
await runCommand(pushEntitiesAction, { requireAuth: true }, context);
52-
}),
53-
);
45+
return new Command("push")
46+
.description("Push local entity schemas to Base44")
47+
.action(async () => {
48+
await runCommand(pushEntitiesAction, { requireAuth: true }, context);
49+
});
5450
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { log } from "@clack/prompts";
2+
import { Command } from "commander";
3+
import JSON5 from "json5";
4+
import type { CLIContext } from "@/cli/types.js";
5+
import { runCommand, runTask } from "@/cli/utils/index.js";
6+
import type { RunCommandResult } from "@/cli/utils/runCommand.js";
7+
import { InvalidInputError } from "@/core/errors.js";
8+
import { createRecord } from "@/core/resources/entity/index.js";
9+
import { readTextFile } from "@/core/utils/fs.js";
10+
11+
interface CreateRecordCommandOptions {
12+
data?: string;
13+
file?: string;
14+
}
15+
16+
async function parseRecordData(
17+
options: CreateRecordCommandOptions,
18+
): Promise<Record<string, unknown>> {
19+
if (options.data) {
20+
try {
21+
return JSON5.parse(options.data);
22+
} catch {
23+
throw new InvalidInputError(
24+
"Invalid JSON in --data flag. Provide valid JSON.",
25+
{
26+
hints: [
27+
{
28+
message:
29+
'Example: --data \'{"name": "John", "email": "john@example.com"}\'',
30+
},
31+
],
32+
},
33+
);
34+
}
35+
}
36+
37+
if (options.file) {
38+
const content = await readTextFile(options.file);
39+
try {
40+
return JSON5.parse(content);
41+
} catch {
42+
throw new InvalidInputError(
43+
`Invalid JSON in file ${options.file}. Provide a valid JSON/JSONC file.`,
44+
);
45+
}
46+
}
47+
48+
throw new InvalidInputError(
49+
"Provide record data with --data or --file flag",
50+
{
51+
hints: [
52+
{
53+
message:
54+
'Example: --data \'{"name": "John"}\' or --file record.json',
55+
},
56+
],
57+
},
58+
);
59+
}
60+
61+
async function createRecordAction(
62+
entityName: string,
63+
options: CreateRecordCommandOptions,
64+
): Promise<RunCommandResult> {
65+
const data = await parseRecordData(options);
66+
67+
const record = await runTask(
68+
`Creating ${entityName} record...`,
69+
async () => {
70+
return await createRecord(entityName, data);
71+
},
72+
{
73+
successMessage: `Created ${entityName} record`,
74+
errorMessage: `Failed to create ${entityName} record`,
75+
},
76+
);
77+
78+
log.success(`Record created with ID: ${record.id}`);
79+
console.log(JSON.stringify(record, null, 2));
80+
81+
return {};
82+
}
83+
84+
export function getRecordsCreateCommand(context: CLIContext): Command {
85+
return new Command("create")
86+
.description("Create a new entity record")
87+
.argument("<entity-name>", "Name of the entity (e.g. Users, Products)")
88+
.option("-d, --data <json>", "JSON object with record data")
89+
.option("--file <path>", "Read record data from a JSON/JSONC file")
90+
.action(
91+
async (entityName: string, options: CreateRecordCommandOptions) => {
92+
await runCommand(
93+
() => createRecordAction(entityName, options),
94+
{ requireAuth: true },
95+
context,
96+
);
97+
},
98+
);
99+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { confirm, log } from "@clack/prompts";
2+
import { Command } from "commander";
3+
import type { CLIContext } from "@/cli/types.js";
4+
import { CLIExitError } from "@/cli/errors.js";
5+
import { runCommand, runTask } from "@/cli/utils/index.js";
6+
import type { RunCommandResult } from "@/cli/utils/runCommand.js";
7+
import { deleteRecord } from "@/core/resources/entity/index.js";
8+
9+
interface DeleteRecordCommandOptions {
10+
yes?: boolean;
11+
}
12+
13+
async function deleteRecordAction(
14+
entityName: string,
15+
recordId: string,
16+
options: DeleteRecordCommandOptions,
17+
): Promise<RunCommandResult> {
18+
if (!options.yes) {
19+
const confirmed = await confirm({
20+
message: `Delete ${entityName} record ${recordId}?`,
21+
});
22+
23+
if (confirmed !== true) {
24+
throw new CLIExitError(0);
25+
}
26+
}
27+
28+
await runTask(
29+
`Deleting ${entityName} record...`,
30+
async () => {
31+
return await deleteRecord(entityName, recordId);
32+
},
33+
{
34+
successMessage: `Deleted ${entityName} record`,
35+
errorMessage: `Failed to delete ${entityName} record`,
36+
},
37+
);
38+
39+
log.success(`Record ${recordId} deleted`);
40+
41+
return {};
42+
}
43+
44+
export function getRecordsDeleteCommand(context: CLIContext): Command {
45+
return new Command("delete")
46+
.description("Delete an entity record")
47+
.argument("<entity-name>", "Name of the entity (e.g. Users, Products)")
48+
.argument("<record-id>", "ID of the record to delete")
49+
.option("-y, --yes", "Skip confirmation prompt")
50+
.action(
51+
async (
52+
entityName: string,
53+
recordId: string,
54+
options: DeleteRecordCommandOptions,
55+
) => {
56+
await runCommand(
57+
() => deleteRecordAction(entityName, recordId, options),
58+
{ requireAuth: true },
59+
context,
60+
);
61+
},
62+
);
63+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Command } from "commander";
2+
import type { CLIContext } from "@/cli/types.js";
3+
import { runCommand, runTask } from "@/cli/utils/index.js";
4+
import type { RunCommandResult } from "@/cli/utils/runCommand.js";
5+
import { getRecord } from "@/core/resources/entity/index.js";
6+
7+
async function getRecordAction(
8+
entityName: string,
9+
recordId: string,
10+
): Promise<RunCommandResult> {
11+
const record = await runTask(
12+
`Fetching ${entityName} record...`,
13+
async () => {
14+
return await getRecord(entityName, recordId);
15+
},
16+
{
17+
successMessage: `Fetched ${entityName} record`,
18+
errorMessage: `Failed to fetch ${entityName} record`,
19+
},
20+
);
21+
22+
console.log(JSON.stringify(record, null, 2));
23+
24+
return {};
25+
}
26+
27+
export function getRecordsGetCommand(context: CLIContext): Command {
28+
return new Command("get")
29+
.description("Get a single entity record by ID")
30+
.argument("<entity-name>", "Name of the entity (e.g. Users, Products)")
31+
.argument("<record-id>", "ID of the record")
32+
.action(async (entityName: string, recordId: string) => {
33+
await runCommand(
34+
() => getRecordAction(entityName, recordId),
35+
{ requireAuth: true },
36+
context,
37+
);
38+
});
39+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Command } from "commander";
2+
import type { CLIContext } from "@/cli/types.js";
3+
import { getRecordsCreateCommand } from "./create.js";
4+
import { getRecordsDeleteCommand } from "./delete.js";
5+
import { getRecordsGetCommand } from "./get.js";
6+
import { getRecordsListCommand } from "./list.js";
7+
import { getRecordsUpdateCommand } from "./update.js";
8+
9+
export function getRecordsCommand(context: CLIContext): Command {
10+
return new Command("records")
11+
.description("CRUD operations on entity records")
12+
.addCommand(getRecordsListCommand(context))
13+
.addCommand(getRecordsGetCommand(context))
14+
.addCommand(getRecordsCreateCommand(context))
15+
.addCommand(getRecordsUpdateCommand(context))
16+
.addCommand(getRecordsDeleteCommand(context));
17+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { log } from "@clack/prompts";
2+
import { Command } from "commander";
3+
import type { CLIContext } from "@/cli/types.js";
4+
import { runCommand, runTask } from "@/cli/utils/index.js";
5+
import type { RunCommandResult } from "@/cli/utils/runCommand.js";
6+
import { listRecords } from "@/core/resources/entity/index.js";
7+
8+
interface ListRecordsCommandOptions {
9+
filter?: string;
10+
sort?: string;
11+
limit?: string;
12+
skip?: string;
13+
fields?: string;
14+
}
15+
16+
async function listRecordsAction(
17+
entityName: string,
18+
options: ListRecordsCommandOptions,
19+
): Promise<RunCommandResult> {
20+
const records = await runTask(
21+
`Fetching ${entityName} records...`,
22+
async () => {
23+
return await listRecords(entityName, {
24+
filter: options.filter,
25+
sort: options.sort,
26+
limit: options.limit ? Number(options.limit) : 50,
27+
skip: options.skip ? Number(options.skip) : undefined,
28+
fields: options.fields,
29+
});
30+
},
31+
{
32+
successMessage: `Fetched ${entityName} records`,
33+
errorMessage: `Failed to fetch ${entityName} records`,
34+
},
35+
);
36+
37+
log.info(`Found ${records.length} record(s)`);
38+
console.log(JSON.stringify(records, null, 2));
39+
40+
return {};
41+
}
42+
43+
export function getRecordsListCommand(context: CLIContext): Command {
44+
return new Command("list")
45+
.description("List entity records")
46+
.argument("<entity-name>", "Name of the entity (e.g. Users, Products)")
47+
.option("-f, --filter <json>", "JSON query filter")
48+
.option("-s, --sort <field>", "Sort field (prefix with - for descending)")
49+
.option("-l, --limit <n>", "Max records to return", "50")
50+
.option("--skip <n>", "Number of records to skip")
51+
.option("--fields <fields>", "Comma-separated fields to return")
52+
.action(async (entityName: string, options: ListRecordsCommandOptions) => {
53+
await runCommand(
54+
() => listRecordsAction(entityName, options),
55+
{ requireAuth: true },
56+
context,
57+
);
58+
});
59+
}

0 commit comments

Comments
 (0)