Build
Work with entity data
Worked read, query, save, batch, and delete examples through Entity Service.
Pikbase docs Entity Service is the canonical data surface. Start from the definition so your code uses current property names, types, and relationships. Every operation takes an optional token and tenantCode; both fall back to the configured credentials when omitted.
Read one record
getById takes the definition name and the identifier.
const result = await entityService.getById(
{ definitionType: "Customer", id: "customer-123" },
token,
tenantCode,
);
if (!result.success) {
throw new Error(result.message);
}
const customer = result.entity;
Query a page
query takes a QueryParams builder. Every method returns the query, so calls chain. Always bound the result set.
import {
IncludeQuery,
QueryFunction,
QueryParams,
QuerySortType,
} from "@gsb-core/core";
const orders = new QueryParams<Order>("Order")
.filter("status", "open", QueryFunction.Equals)
.include(new IncludeQuery<Customer>("customer").select(["id", "name"]))
.self.sortBy("createDate", QuerySortType.Descending)
.select(["id", "status", "total", "createDate"])
.skip(0)
.take(25)
.returnCount();
const page = await entityService.query(orders, token, tenantCode);
console.log(page.entities?.length, "of", page.totalCount);
returnCount() costs an extra count query. Request it only when the interface shows a total. Use include() instead of issuing one query per row.
See Build queries with QueryParams for the JSON form the CLI and MCP tools accept.
Create and update
save creates when the entity has no id and updates when it does. Nested objects and arrays are processed in the same call, so a parent and its children save together.
const created = await entityService.save(
{
entityDef: { name: "Customer" },
entity: {
firstName: "Ada",
lastName: "Lovelace",
email: "ada@example.com",
},
},
token,
tenantCode,
);
// created.id is the new identifier; created.isUpdate is false.
const updated = await entityService.save(
{
entityDef: { name: "Customer" },
entity: { id: created.id, email: "ada.lovelace@example.com" },
},
token,
tenantCode,
);
// updated.isUpdate is true.
Use saveMulti for a batch of entities of the same definition. Validate input against the definition before sending it — the backend validates too, but a schema check at your boundary gives a better error.
Delete
Deletes are destructive and gated behind a two-step confirmation. Call delete once to get a confirmationToken bound to the exact payload, then repeat the identical call with confirm: true.
const challenge = await gsbEntityTools.delete({
request: { entDefName: "Customer", entityId: "customer-123" },
token,
tenantCode,
});
// challenge.requiresConfirmation === true
const removed = await gsbEntityTools.delete({
request: { entDefName: "Customer", entityId: "customer-123" },
confirm: true,
confirmationToken: challenge.confirmationToken,
token,
tenantCode,
});
console.log(removed.data.affectedRowCount);
Prefer a known identifier over deleteQuery; a filter that matches more rows than intended cannot be undone. Model-generated destructive operations require explicit user confirmation before the second call is made.
Relationships
await entityService.saveMappedItems(
{
entityDef: { name: "Order" },
entityId: "order-123",
propName: "tags",
items: [{ entityId: "tag-priority" }],
},
token,
tenantCode,
);
Detach with removeMappedItems. Do not duplicate related records into browser storage; read them through the relation each time.
Error handling
Every operation returns { success: false, message } rather than throwing across the tool boundary. Branch on success before touching any other field.
const result = await entityService.query(orders, token, tenantCode);
if (!result.success) {
logger.warn({ requestId, operation: "query", message: result.message });
throw new ServiceError(result.message);
}
Never log the token. Carry a request id instead.