Skip to content

Commit f449ba3

Browse files
fix(entities): correct issues found during live testing
- entity-copy: page through the source with the API's page param instead of skip (which the entity data API ignores), fixing an infinite loop that re-inserted records without end; cap --qty at 10000 - entity-data: drop --order/--order-by (the data query ignores order) - entity-list: add working --order/--order-by built as the [field, dir] tuple the SDK expects, validated against orderable fields - entity-create/entity-edit: remove --description; the entity API has no description field, so it was a silent no-op (also drops the stray payload_decoder: null it gated) - fix invalid 'integer' type in help examples (valid type is 'int') - regenerate man-page snapshot for the help text changes
1 parent 7ed0d45 commit f449ba3

14 files changed

Lines changed: 109 additions & 90 deletions

src/commands/entities/entity-copy.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,14 @@ async function entityCopy(options: IOptions) {
5353
const resources = new Resources({ token: config.profileToken, region: config.profileRegion });
5454
const pageSize = options.qty ?? DEFAULT_PAGE;
5555
let total = 0;
56-
let page = 0;
56+
let page = 1;
5757

5858
// Loop until the source is exhausted (page returns fewer than pageSize records).
5959
while (true) {
60-
const batch = await resources.entities
61-
.getEntityData(fromId, { amount: pageSize, skip: page * pageSize })
62-
.catch((error: unknown) => {
63-
const message = error instanceof Error ? error.message : String(error);
64-
return failWith(`Failed to read source entity ${fromId}: ${message}`, "read_failed", useJSON);
65-
});
60+
const batch = await resources.entities.getEntityData(fromId, { amount: pageSize, page }).catch((error: unknown) => {
61+
const message = error instanceof Error ? error.message : String(error);
62+
return failWith(`Failed to read source entity ${fromId}: ${message}`, "read_failed", useJSON);
63+
});
6664

6765
if (!batch || batch.length === 0) {
6866
break;

src/commands/entities/entity-create.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,10 @@ describe("entityCreate", () => {
9494
);
9595
});
9696

97-
test("interactive path uses prompts for description / tags / schema-paste", async () => {
97+
test("interactive path uses prompts for tags / schema-paste", async () => {
9898
requireOrFailMock.mockResolvedValue("interactive-entity");
9999
resourcesInstance.entities.create.mockResolvedValue({ id: "id-2" });
100100
prompts.inject([
101-
"An optional description", // description
102101
"env:prod,team:platform", // tags
103102
JSON.stringify({ email: { type: "string", required: true } }), // schema paste
104103
]);

src/commands/entities/entity-create.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function parseTagsCSV(input: string): TagsObj[] {
3535
/**
3636
* @description Loads the entity definition from `--schema <file>` or
3737
* `--schema-json '<inline>'`. Accepts two shapes:
38-
* 1. A full envelope: `{ name, schema: {...}, tags?, description? }`
38+
* 1. A full envelope: `{ name, schema: {...}, tags? }`
3939
* 2. A bare schema map: `{ field_name: {type: "string", ...}, ... }`
4040
* — wrapped into `{ schema: <parsed> }` so the API receives the columns.
4141
* Detected when none of the values look like an EntityCreateInfo field
@@ -60,7 +60,7 @@ function loadSchemaPayload(options: IOptions): EntityCreateInfo | undefined {
6060
// whole object is the schema. This matches `--add-field`'s bare-map shape
6161
// and avoids silently dropping schema columns when callers omit the
6262
// envelope.
63-
const envelopeKeys = ["name", "schema", "tags", "description", "payload_decoder"] as const;
63+
const envelopeKeys = ["name", "schema", "tags", "payload_decoder"] as const;
6464
const isEnvelope = envelopeKeys.some((key) => key in parsed);
6565
if (!isEnvelope) {
6666
return { schema: parsed } as unknown as EntityCreateInfo;
@@ -89,18 +89,15 @@ async function entityCreate(nameArg: string | undefined, options: IOptions) {
8989
failWith("Schema payload is missing required field: name.", "missing_name", Boolean(options.json));
9090
}
9191
} else {
92-
// Interactive: prompt for name → description → tags → paste schema JSON.
92+
// Interactive: prompt for name → tags → paste schema JSON.
9393
const name = await requireOrFail(nameArg, "name", {
9494
silent: options.silent,
9595
json: options.json,
9696
promptMessage: "Entity name:",
9797
});
98-
let description: string | undefined;
9998
let tags: TagsObj[] | undefined;
10099
let schema: EntityCreateInfo["schema"] | undefined;
101100
if (!options.silent) {
102-
const descAnswer = await prompts({ type: "text", name: "description", message: "Description (optional):" });
103-
description = descAnswer.description ? String(descAnswer.description) : undefined;
104101
const tagAnswer = await prompts({ type: "text", name: "tags", message: "Tags as key:value,key2:value2 (optional):" });
105102
tags = tagAnswer.tags ? parseTagsCSV(String(tagAnswer.tags)) : undefined;
106103
const schemaAnswer = await prompts({
@@ -117,12 +114,7 @@ async function entityCreate(nameArg: string | undefined, options: IOptions) {
117114
}
118115
}
119116
}
120-
payload = { name, ...(description ? { payload_decoder: null } : {}), ...(tags ? { tags } : {}), ...(schema ? { schema } : {}) };
121-
// `description` lives on EntityInfo but not EntityCreateInfo per SDK types;
122-
// keep it on the payload anyway since the API accepts it.
123-
if (description) {
124-
(payload as Record<string, unknown>).description = description;
125-
}
117+
payload = { name, ...(tags ? { tags } : {}), ...(schema ? { schema } : {}) };
126118
}
127119

128120
const created = await resources.entities.create(payload).catch((error: unknown) => {

src/commands/entities/entity-data.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,19 @@ describe("entityData", () => {
6060
expect(resourcesInstance.entities.getEntityData).toHaveBeenCalledWith("ent1", {});
6161
});
6262

63-
test("read mode applies --qty / --skip / --order-by / --order / -q filters", async () => {
63+
test("read mode applies --qty / --skip / -q filters", async () => {
6464
resourcesInstance.entities.getEntityData.mockResolvedValue([]);
6565

6666
const { entityData } = await import("./entity-data.js");
6767
await entityData("ent1", {
6868
qty: 50,
6969
skip: 10,
70-
orderBy: "created_at",
71-
order: "desc",
7270
query: ["status=active", "tier=gold"],
7371
} as never);
7472

7573
expect(resourcesInstance.entities.getEntityData).toHaveBeenCalledWith("ent1", {
7674
amount: 50,
7775
skip: 10,
78-
order: "created_at,desc",
7976
filter: { status: "active", tier: "gold" },
8077
});
8178
});

src/commands/entities/entity-data.ts

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ interface IOptions {
99
environment?: string;
1010
qty?: number;
1111
skip?: number;
12-
orderBy?: string;
13-
order?: "asc" | "desc";
1412
query?: string[];
1513
json?: boolean;
1614
stringify?: boolean;
@@ -134,9 +132,6 @@ async function entityData(idArg: string | undefined, options: IOptions) {
134132
if (options.skip !== undefined) {
135133
queryParams.skip = options.skip;
136134
}
137-
if (options.orderBy) {
138-
queryParams.order = `${options.orderBy},${options.order ?? "asc"}`;
139-
}
140135
const filter = parseQueryFilters(options.query);
141136
if (filter) {
142137
queryParams.filter = filter;
@@ -175,12 +170,10 @@ async function entityData(idArg: string | undefined, options: IOptions) {
175170

176171
case "edit": {
177172
const payload = parseJSON<unknown>(options.edit as string, "--edit", useJSON);
178-
const result = await resources.entities
179-
.editEntityData(id, payload as never)
180-
.catch((error: unknown) => {
181-
const message = error instanceof Error ? error.message : String(error);
182-
return failWith(`Failed to edit entity ${id} data: ${message}`, "edit_failed", useJSON);
183-
});
173+
const result = await resources.entities.editEntityData(id, payload as never).catch((error: unknown) => {
174+
const message = error instanceof Error ? error.message : String(error);
175+
return failWith(`Failed to edit entity ${id} data: ${message}`, "edit_failed", useJSON);
176+
});
184177
if (useJSON) {
185178
process.stdout.write(`${JSON.stringify({ id, edited: true, result })}\n`);
186179
return;
@@ -196,7 +189,10 @@ async function entityData(idArg: string | undefined, options: IOptions) {
196189
if (raw.trim().startsWith("[")) {
197190
ids = parseJSON<string[]>(raw, "--delete", useJSON);
198191
} else {
199-
ids = raw.split(",").map((s) => s.trim()).filter(Boolean);
192+
ids = raw
193+
.split(",")
194+
.map((s) => s.trim())
195+
.filter(Boolean);
200196
}
201197
if (ids.length === 0) {
202198
failWith("--delete requires at least one record id.", "empty_ids", useJSON);
@@ -206,12 +202,10 @@ async function entityData(idArg: string | undefined, options: IOptions) {
206202
infoMSG("Cancelled. No changes made.");
207203
return;
208204
}
209-
const result = await resources.entities
210-
.deleteEntityData(id, { ids })
211-
.catch((error: unknown) => {
212-
const message = error instanceof Error ? error.message : String(error);
213-
return failWith(`Failed to delete entity ${id} records: ${message}`, "delete_failed", useJSON);
214-
});
205+
const result = await resources.entities.deleteEntityData(id, { ids }).catch((error: unknown) => {
206+
const message = error instanceof Error ? error.message : String(error);
207+
return failWith(`Failed to delete entity ${id} records: ${message}`, "delete_failed", useJSON);
208+
});
215209
if (useJSON) {
216210
process.stdout.write(`${JSON.stringify({ id, deleted: ids.length, result })}\n`);
217211
return;
@@ -243,12 +237,10 @@ async function entityData(idArg: string | undefined, options: IOptions) {
243237
// data (verified live against the TagoIO API). Counting via the data
244238
// endpoint keeps `--count` consistent with what `entity-data` itself
245239
// returns. The page-size cap (10000) matches `entity-copy`'s default.
246-
const data = await resources.entities
247-
.getEntityData(id, { amount: 10000 })
248-
.catch((error: unknown) => {
249-
const message = error instanceof Error ? error.message : String(error);
250-
return failWith(`Failed to count entity ${id}: ${message}`, "count_failed", useJSON);
251-
});
240+
const data = await resources.entities.getEntityData(id, { amount: 10000 }).catch((error: unknown) => {
241+
const message = error instanceof Error ? error.message : String(error);
242+
return failWith(`Failed to count entity ${id}: ${message}`, "count_failed", useJSON);
243+
});
252244
const count = Array.isArray(data) ? data.length : 0;
253245
if (useJSON || options.stringify) {
254246
writeJSON({ id, count }, options);

src/commands/entities/entity-edit.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,14 @@ describe("entityEdit", () => {
5959
expect(resourcesInstance.entities.edit).toHaveBeenCalledWith("ent1", { name: "renamed" });
6060
});
6161

62-
test("merges --name and --description into a single patch", async () => {
62+
test("builds the patch from --name", async () => {
6363
resourcesInstance.entities.edit.mockResolvedValue({ message: "ok" });
6464

6565
const { entityEdit } = await import("./entity-edit.js");
66-
await entityEdit("ent1", { name: "renamed", description: "new desc" } as never);
66+
await entityEdit("ent1", { name: "renamed" } as never);
6767

6868
expect(resourcesInstance.entities.edit).toHaveBeenCalledWith("ent1", {
6969
name: "renamed",
70-
description: "new desc",
7170
});
7271
});
7372

@@ -87,7 +86,7 @@ describe("entityEdit", () => {
8786
await expect(entityEdit(undefined, { silent: true, name: "x" } as never)).rejects.toThrow(/Missing required input: id/);
8887
});
8988

90-
test("no-op edit (no --name or --description) errors actionably", async () => {
89+
test("no-op edit (no --name) errors actionably", async () => {
9190
const { entityEdit } = await import("./entity-edit.js");
9291
await expect(entityEdit("ent1", {} as never)).rejects.toThrow(/Nothing to update/);
9392
});

src/commands/entities/entity-edit.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { pickEntityIDFromTagoIO } from "../../prompt/pick-entity-id-from-tagoio.
77
interface IOptions {
88
environment?: string;
99
name?: string;
10-
description?: string;
1110
silent?: boolean;
1211
json?: boolean;
1312
}
@@ -36,19 +35,16 @@ async function entityEdit(idArg: string | undefined, options: IOptions) {
3635
}
3736

3837
// Build the patch from only the flags the user set. Empty patch = no-op.
39-
const patch: Partial<EntityCreateInfo> & { description?: string } = {};
38+
const patch: Partial<EntityCreateInfo> = {};
4039
if (options.name !== undefined) {
4140
patch.name = options.name;
4241
}
43-
if (options.description !== undefined) {
44-
patch.description = options.description;
45-
}
4642

4743
if (Object.keys(patch).length === 0) {
48-
failWith("Nothing to update — pass at least one of --name or --description.", "noop_edit", Boolean(options.json));
44+
failWith("Nothing to update — pass --name.", "noop_edit", Boolean(options.json));
4945
}
5046

51-
await resources.entities.edit(id, patch as Partial<EntityCreateInfo>).catch((error: unknown) => {
47+
await resources.entities.edit(id, patch).catch((error: unknown) => {
5248
const message = error instanceof Error ? error.message : String(error);
5349
failWith(`Failed to edit entity ${id}: ${message}`, "edit_failed", Boolean(options.json));
5450
});

src/commands/entities/entity-info.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ describe("entityInfo", () => {
106106
resourcesInstance.entities.info.mockResolvedValue({
107107
id: "ent1",
108108
name: "Entity One",
109-
description: "desc",
110109
schema: { email: { type: "string" } },
111110
index: { email_idx: { fields: ["email"] } },
112111
created_at: null,

src/commands/entities/entity-info.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ async function entityInfo(idArg: string | undefined, options: IOptions) {
6161
const meta: Record<string, unknown> = {
6262
id: info.id,
6363
name: info.name,
64-
description: (info as { description?: string }).description ?? "",
6564
created_at: info.created_at,
6665
updated_at: info.updated_at,
6766
};

src/commands/entities/entity-list.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,34 @@ describe("entityList", () => {
9797
);
9898
});
9999

100+
test("--order-by + --order builds the [field, direction] tuple the SDK expects", async () => {
101+
getEnvironmentConfigMock.mockReturnValue(makeEnvironmentConfig());
102+
resourcesInstance.entities.list.mockResolvedValue([]);
103+
104+
const { entityList } = await import("./entity-list.js");
105+
await entityList({ tagkey: [], tagvalue: [], orderBy: "created_at", order: "desc" } as never);
106+
107+
expect(resourcesInstance.entities.list).toHaveBeenCalledWith(expect.objectContaining({ orderBy: ["created_at", "desc"] }));
108+
});
109+
110+
test("--order-by defaults to asc when --order is omitted", async () => {
111+
getEnvironmentConfigMock.mockReturnValue(makeEnvironmentConfig());
112+
resourcesInstance.entities.list.mockResolvedValue([]);
113+
114+
const { entityList } = await import("./entity-list.js");
115+
await entityList({ tagkey: [], tagvalue: [], orderBy: "name" } as never);
116+
117+
expect(resourcesInstance.entities.list).toHaveBeenCalledWith(expect.objectContaining({ orderBy: ["name", "asc"] }));
118+
});
119+
120+
test("rejects an --order-by field that is not orderable", async () => {
121+
getEnvironmentConfigMock.mockReturnValue(makeEnvironmentConfig());
122+
123+
const { entityList } = await import("./entity-list.js");
124+
await expect(entityList({ tagkey: [], tagvalue: [], orderBy: "email" } as never)).rejects.toThrow(/Cannot order by "email"/);
125+
expect(resourcesInstance.entities.list).not.toHaveBeenCalled();
126+
});
127+
100128
test("routes a SDK failure through errorHandler with an actionable message", async () => {
101129
getEnvironmentConfigMock.mockReturnValue(makeEnvironmentConfig());
102130
resourcesInstance.entities.list.mockRejectedValue(new Error("Authorization Denied"));

0 commit comments

Comments
 (0)