# Pikbase documentation > Pikbase is a metadata-driven backend platform: define entities once, then operate governed APIs, functions, workflows, auth, and storage from one console. # Developer documentation Source: / # Pikbase developer documentation Guides and API references generated from the same Entity Service documentation exposed through the Pikbase CLI and MCP server. One contract source, four integration surfaces: the console, the TypeScript client, the `gsb` CLI, and the MCP server. ## Where to start - [Getting started](/getting-started) — pick a surface and make your first contract-backed query. - [Core concepts](/guides/core-concepts) — tenants, entities, definitions, properties, relationships, workflows. - [Work with entity data](/guides/entity-data) — worked read, query, save, and delete examples. - [Authentication and tenancy](/guides/authentication) — verified sessions and server-side tenant resolution. ## Reference - [API reference](/api) — every Entity Service, Schema Manager, and workflow operation. - [Pikbase CLI](/tools/cli) — every `gsb` command with its real flags. - [MCP server](/tools/mcp) — the permission-scoped tool catalog for AI clients. - [Search](/search) — search every guide and operation page. --- # Getting started Source: /getting-started # Getting started Pikbase is a backend platform built around **entities**, **definitions**, and **permission-scoped operations**. The same contracts power the console, SDK, CLI, and MCP tools. ## Choose your surface | Surface | Best for | |---|---| | Console | Exploring definitions, data, functions, and workflows | | Entity Service | Application data access through the TypeScript client | | `gsb` CLI | Development, inspection, and repeatable operations | | MCP server | Permission-scoped tools for AI-enabled editors and agents | ## Before you begin You need a Pikbase tenant and an account with access to that tenant. Keep credentials server-side. Browser applications use the established authentication flow and the direct Entity Service client; never bundle an API key or client secret. ## First steps 1. Sign in to the console and inspect an entity definition. 2. Use the query builder to verify the data shape and access policy. 3. Initialize the CLI with your tenant. 4. Inspect the read-only tool catalog before running an operation. ```bash gsb init --tenant your-tenant gsb tools --read-only gsb call getDocs --input '{"methodName":"query"}' --raw ``` Continue with [core concepts](/guides/core-concepts), then use the [query reference](/api/query) for the exact request and response contract. --- # Core concepts Source: /guides/core-concepts # Core concepts Six concepts carry the whole platform. Each maps to concrete operations, linked below. ## Tenant A tenant is the primary isolation boundary. Every authorized operation runs in tenant context. Do not infer tenant access from a URL or a client-supplied value; authorization establishes it. Every operation accepts an optional `tenantCode`, which falls back to the tenant encoded in the verified token. ## Entity An entity is a business record with a unique identifier, properties, optional relationships, and metadata — a customer, project, ticket, or invoice. Read one with [getById](/api/getbyid), a page of them with [query](/api/query), and duplicate one with [getCopy](/api/getcopy). ```typescript const customer = await entityService.getById( { definitionType: "Customer", id: "customer-123" }, token, tenantCode, ); ``` ## Entity definition An entity definition is the schema contract for a class of entities: property names and types, constraints, references, indexes, and display metadata. It is itself a record, so it is readable at runtime. Read one with [getEntityDef](/api/getentitydef), list them with [queryEntityDefs](/api/queryentitydefs), and evolve them with [createEntityDef](/api/createentitydef) and [updateEntityDef](/api/updateentitydef). ```typescript const definition = await entityDefService.getEntityDef( { entityDef: { name: "Customer" } }, token, tenantCode, ); ``` ## Property Properties hold scalar values, structured values, or references. Evolve them through the definition operations rather than relying on undocumented shapes. Use [getCommonPropertyDefs](/api/getcommonpropertydefs) to discover the available data types, then [addProperty](/api/addproperty), [updateProperty](/api/updateproperty), and [removeProperty](/api/removeproperty). ```typescript await entityDefService.addProperty( { entityDef: { name: "Customer" }, property: { name: "loyaltyTier", title: "Loyalty tier" }, }, token, tenantCode, ); ``` ## Relationship Mapped-item operations maintain relationships between entities. Use them when the definition models a relation rather than embedding copied data. Attach with [saveMappedItems](/api/savemappeditems), detach with [removeMappedItems](/api/removemappeditems), and read across a relation with [queryMapped](/api/querymapped) or `include()` on a [query](/api/query). ## Workflow Workflows coordinate longer-running business processes. Start one with [startWorkflow](/api/startworkflow) when the caller should not wait; use [runWorkflow](/api/runworkflow) only when the caller needs the outcome inline. A waiting human task is advanced by its assignee with [submitWorkflowTask](/api/submitworkflowtask), or programmatically with [iterateTask](/api/iteratetask); administrators step a run one activity at a time with [iterateOnce](/api/iterateonce). See the [workflows guide](/guides/workflows) for the full lifecycle. ## Function A serverless function is tenant-resident backend code. Execute one with [runWfFunction](/api/runwffunction) and dry-run one with [testWfFunction](/api/testwffunction). See [serverless functions](/guides/serverless-functions). --- # Backend platform map Source: /guides/backend-platform # Backend platform map This documentation covers the backend contract used by the console, TypeScript client, CLI, MCP server, and serverless runtime. Start here when you need to decide which service owns a capability. ## Contract surfaces | Need | Canonical surface | Documentation | |---|---|---| | Read or mutate business records | Entity Service | [entity data guide](/guides/entity-data), [CRUD reference](/guides/crud) | | Discover or evolve schemas | Schema Manager | [schema design](/guides/schema-design), [Schema Manager API](/guides/schema-manager) | | Run backend code | Serverless functions | [serverless functions](/guides/serverless-functions) | | Coordinate long-running work | Workflow Service | [workflows](/guides/workflows) | | Automate from a terminal | `gsb` CLI | [CLI reference](/tools/cli) | | Connect an editor or agent | MCP server | [MCP server](/tools/mcp) | | Inspect the contract safely | Read-only inspection tools | [MCP server](/tools/mcp#bounded-inspection-tools) | The API reference lists every registered operation and links to its request, response, and safety contract. ## Backend request lifecycle 1. Authenticate the caller and verify the token signature against the issuer JWKS. 2. Resolve tenant context from the verified session, never from an untrusted URL or body field. 3. Authorize the operation against that tenant and entity definition. 4. Validate external input with a schema before calling Entity Service or a function. 5. Bound reads with selected columns, pagination, and explicit includes. 6. Rate-limit mutations and side effects, then log a request id with secrets and PII redacted. Authentication and authorization are separate controls. See [authentication and tenancy](/guides/authentication) for the server-side session pattern. ## Data and schema lifecycle Definitions are the source of truth for entity properties, references, constraints, and indexes. Read the definition before writing records, select only the fields the caller needs, and evolve properties through Schema Manager operations rather than undocumented payloads. For lists, use [QueryParams](/guides/query-params) with a stable sort, `skip`, and `take`. Use `include` for bounded relationships. Request a total count only when the UI displays it. Prefer `getById` when the identifier is known. ## Code and workflow lifecycle Serverless functions run in the tenant backend and receive runtime context, entity definitions, enums, and referenced libraries. Test code with `testWfFunction` before publishing it. Use `runWfFunction` for an approved invocation, and keep destructive or external side effects behind explicit approval. Use `startWorkflow` for asynchronous processes that wait on people, timers, or external events. Use `runWorkflow` only when the caller needs an inline result. Monitor instances and audit rows as ordinary entities through Entity Service. ## CLI and MCP safety The CLI and MCP server share one registry and implementation. Read tools are bounded inspection surfaces; writes, destructive operations, and external side effects require explicit approval. Inspect `gsb tools --read-only` before automation, and never place tokens in command history, browser storage, or committed configuration. ## Documentation source and Dev1 extraction Tenant-authored help pages live in `GsbHelpPage` and their localized content lives in the related `GsbMlContent` record. English content is the `en_us` field. The canonical CLI extraction query is: ```bash gsb call query --tenant dev1 --raw --input '{ "queryParams": { "entDefName": "GsbHelpPage", "selectCols": [ {"name":"id"}, {"name":"title"}, {"name":"slug"}, {"name":"path"}, {"name":"parent_id"}, {"name":"content_id"}, {"name":"lastUpdateDate"} ], "includes": [{ "name": "content", "selectCols": [{"name":"id"}, {"name":"title"}, {"name":"en_us"}] }], "count": 500, "calcTotalCount": true } }' ``` Treat the returned HTML as untrusted content before rendering it. Keep the export build-time and reviewed; the public docs app must not require tenant credentials or make live backend calls. ## Coverage boundary This site documents the backend contract and its approved integration surfaces. Product-specific frontend behavior, local persistence, fake API responses, and browser-only business logic do not belong here. When a capability is missing from Entity Service, Schema Manager, functions, or workflows, document the missing backend contract instead of simulating it in an app. --- # Authentication and tenancy Source: /guides/authentication # Authentication and tenancy Authentication proves identity. Authorization decides whether that identity may perform an operation on a tenant. They are separate steps and both run on the server. ## Getting a token Credentials are exchanged for a token at `/api/auth/getToken`. The TypeScript client wraps that call: ```typescript interface GetTokenRequest { email: string; password: string; remember?: boolean; includeUserInfo?: boolean; variation?: { tenantCode: string }; } const response = await entityService.getToken({ email, password, remember: true, includeUserInfo: true, variation: { tenantCode }, }); const token = response.auth?.token; ``` The response is a `GsbAuthResponse`: | Field | Type | Description | |---|---|---| | `auth.token` | string | Bearer token for subsequent requests | | `auth.userId` | string | Identifier of the authenticated user | | `auth.tenantCode` | string | Tenant the token was issued for | | `auth.roles` | string[] | Role names granted to the user | | `auth.groups` | string[] | Group names the user belongs to | | `auth.expireDate` | string | Expiry timestamp | | `status` | number | HTTP status of the exchange | The token encodes the user id (`uid`), tenant code (`tc`), instance id (`i`), expiry (`exp`), and issuer (`iss`). This exchange runs on your server. The password never reaches a browser bundle, and the token never reaches `localStorage`. ## Storing the session Set the token in a cookie from a server route. `HttpOnly` keeps it out of reach of any script, so a single XSS cannot become account takeover. ```typescript // Server route. Never runs in the browser. const auth = await entityService.getToken({ email, password, variation: { tenantCode }, }); if (!auth.auth?.token) { return new Response("Invalid credentials", { status: 401 }); } return new Response(null, { status: 204, headers: { "Set-Cookie": [ `session=${auth.auth.token}`, "HttpOnly", "Secure", "SameSite=Lax", "Path=/", `Max-Age=${60 * 60 * 8}`, ].join("; "), }, }); ``` ## Verifying the session Decoding `exp` and trusting it is not authentication — a forged cookie passes. Verify the signature against the issuer's JWKS on every protected request. ```typescript import { createRemoteJWKSet, jwtVerify } from "jose"; const jwks = createRemoteJWKSet(new URL(process.env.GSB_JWKS_URL)); export async function verifySession(request: Request) { const token = readCookie(request, "session"); if (!token) throw new UnauthorizedError(); const { payload } = await jwtVerify(token, jwks, { issuer: process.env.GSB_TOKEN_ISSUER, audience: process.env.GSB_TOKEN_AUDIENCE, }); // Tenant comes from the verified claim, never from the request. return { token, userId: String(payload.uid), tenantCode: String(payload.tc), }; } ``` `jwtVerify` checks the signature, `exp`, `nbf`, issuer, and audience together. A path-prefix check in middleware is routing, not security. ## Authorizing the request ```typescript export async function GET(request: Request) { const session = await verifySession(request); // A verified identity is not yet an authorized one. if (!(await canRead(session.userId, session.tenantCode, "Order"))) { return new Response("Forbidden", { status: 403 }); } const orders = new QueryParams("Order").skip(0).take(25); const page = await entityService.query( orders, session.token, session.tenantCode, ); return Response.json(page); } ``` Pass `session.tenantCode` explicitly. A tenant code read from a query string, header, or request body is caller-controlled input and must never reach a service call. ## Errors | Status | Body | Cause | |---|---|---| | 401 | `{ "status": 401, "message": "Invalid credentials" }` | Wrong email or password | | 400 | `{ "status": 400, "message": "Tenant code is required" }` | `variation.tenantCode` omitted | | 401 | `{ "status": 401, "message": "Token has expired" }` | Expired token; re-authenticate | ## Rules - Server-only secrets. Anything behind `NEXT_PUBLIC_` or `VITE_` is compiled into the browser bundle and is public. - `HttpOnly; Secure; SameSite=Lax` cookies set by a server route. Never `localStorage`. - Verify the signature against JWKS on every protected request. Never decode and trust. - Resolve tenant context from the verified claim, then authorize the operation on that tenant. - Grant the narrowest role that works. Keep read-only inspection separate from mutation. - Validate every external input with a schema before it reaches a service call. - Redact tokens, credentials, and personal data from logs. Carry a request id instead. - Always HTTPS. Handle revocation and expiry explicitly. --- # Work with entity data Source: /guides/entity-data # Work with entity data 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](/api/getbyid) takes the definition name and the identifier. ```typescript 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](/api/query) takes a `QueryParams` builder. Every method returns the query, so calls chain. Always bound the result set. ```typescript import { IncludeQuery, QueryFunction, QueryParams, QuerySortType, } from "@gsb-core/core"; const orders = new QueryParams("Order") .filter("status", "open", QueryFunction.Equals) .include(new IncludeQuery("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](/guides/query-params) for the JSON form the CLI and MCP tools accept. ## Create and update [save](/api/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. ```typescript 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](/api/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](/api/delete) once to get a `confirmationToken` bound to the exact payload, then repeat the identical call with `confirm: true`. ```typescript 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](/api/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 ```typescript await entityService.saveMappedItems( { entityDef: { name: "Order" }, entityId: "order-123", propName: "tags", items: [{ entityId: "tag-priority" }], }, token, tenantCode, ); ``` Detach with [removeMappedItems](/api/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. ```typescript 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. --- # Entity Service CRUD reference Source: /guides/crud # GSB CRUD Operations Guide ## Table of Contents 1. [Overview](#overview) 2. [Core Types](#core-types) 3. [Entity Service Implementation](#entity-service-implementation) 4. [CRUD Operations](#crud-operations) 5. [Best Practices](#best-practices) 6. [Analytics and Aggregation](#analytics-and-aggregation) 7. [Advanced Query Techniques](#advanced-query-techniques) ## Overview The Pikbase platform provides a framework for implementing CRUD (Create, Read, Update, Delete) operations for entities. This guide covers the implementation details and best practices for working with GSB. ## Core Types ### Request Types ```typescript // Query Parameters interface QueryParams { propertyName?: string; // Property name for reference when used as include entity?: T | null; // Entity object for reference or template id?: string; // ID for single entity retrieval entityId?: string; // Alternative ID for single entity retrieval entityDef?: GsbEntityDef; // Entity definition object selectCols?: SelectCol[]; // Columns to select/include in response includes?: IncludeQuery[]; // Related entities to include filters?: Filter[]; // Query filters startIndex?: number; // Pagination start index count?: number; // Pagination count sortCols?: SortCol[]; // Sort columns searchText?: any; // Text to search across searchable columns calcTotalCount?: boolean; // Whether to calculate total count queryType?: QueryType; // Type of query to execute mapColName?: string; // Column name for mapping disableTransaction?: boolean; // Whether to disable transaction // Getters/Setters get refColName(): string; // Gets mapColName set refColName(v: string); // Sets mapColName get entDefId(): string; // Gets entity definition ID set entDefId(v: string); // Sets entity definition ID get entDefName(): string; // Gets entity definition name set entDefName(v: string); // Sets entity definition name // Methods type(queryType: QueryType): QueryParams; take(count: number): QueryParams; limit(count: number): QueryParams; skip(count: number): QueryParams; filter

( predicate: string | Filter | ((item: T) => P), value?: any, queryFunction?: QueryFunction, relation?: QueryRelation, ): QueryParams; select( col: string | string[] | ((item: T) => any), options?: SelectCol, ): QueryParams; include( ...colNames: (string | ((item: T) => any) | IncludeQuery)[] ): { self: QueryParams; inc: IncludeQuery | null }; sortBy(col: ((item: T) => any) | string, sortType: string): QueryParams; } // Save Request interface GsbSaveRequest { entDefName?: string; // Entity definition name entDefId?: string; // Entity definition ID entityDef?: Record; // Entity definition object entity?: any; // The entity data to save filters?: any[]; // Filters for bulk operations entityId?: string; // Entity ID for update/delete } // Bulk Request interface GsbBulkCallRequest { endPoint: string; // API endpoint to call request: any; // Request data reqId?: string; // Request ID for correlation } interface GsbBulkRequest { bulkCalls: GsbBulkCallRequest[]; // Array of calls to make in bulk variation?: { appCode?: string; langCode?: string; tenantCode: string; sessionId?: string; screen?: number; provider?: string; }; } // Filter Support interface Filter { col?: SelectCol; // Column to filter on val?: SelectCol; // Value to compare with relationLevel?: number; // Nesting level for grouping children?: Filter[]; // Nested conditions relation?: QueryRelation; // Logical operator (AND/OR) for children name?: string; // Name for the filter negate?: boolean; // Whether to negate the condition // Helper methods aggregate(func: AggregateFunction): Filter; groupBy(value?: boolean): Filter; isEqual(value: any): Filter; isLike(value: any): Filter; isGreater(value: any): Filter; isSmaller(value: any): Filter; contains(value: any): Filter; bitwiseAnd(value: any): Filter; bitwiseOr(value: any): Filter; bitwiseXor(value: any): Filter; in(value: any): Filter; fullTextSeach(value: any): Filter; is(value: any): Filter; not(): Filter; funcVal(queryFunction: QueryFunction, value: any): Filter; } ``` ### Response Types ```typescript interface GsbQueryResponse { message?: string; // Response message status?: number; // Response status code entities?: any[]; // Array of entities entity?: any; // Single entity totalCount?: number; // Total count of matching entities success?: boolean; // Whether the operation was successful } interface GsbSaveResponse { message?: string; // Response message status?: number; // Response status code id?: string; // ID of the saved entity success?: boolean; // Whether the operation was successful isUpdate?: boolean; // Whether this was an update (true) or create (false) operation } interface GsbSaveMultiResponse { message?: string; // Response message status?: number; // Response status code ids?: string[]; // IDs of saved entities } interface GsbQueryOpResponse { message?: string; // Response message status?: number; // Response status code affectedRowCount?: number; // Number of affected rows deleteCount?: any; // Number of deleted rows } interface GsbBulkCallResult { reqId: string; // Request ID response: string; // Response data } interface GsbBulkResponse { results: GsbBulkCallResult[]; // Results of bulk calls status: number; // Overall status message?: string; // Response message } ``` ## Entity Service Implementation ### Basic Entity Service ```typescript class GsbEntityService { private apiService: GsbApiService; private useCache: boolean; constructor(useCache: boolean = true) { this.apiService = GsbApiService.getInstance(); this.useCache = useCache; } // Static method to get the singleton instance static getInstance(useCache: boolean = true): GsbEntityService { if (!serviceInstance) { serviceInstance = new GsbEntityService(useCache); } return serviceInstance; } // Get entity by ID async getById( definitionType: (new () => T) | string, id: string, token?: string, tenantCode?: string, ): Promise { const req = new QueryParams(definitionType); req.entityId = id; const result = await this.get(req, token, tenantCode); return result.entity as T; } // Get entity copy (without ID) async getCopy( definitionType: (new () => T) | string, id: string, token?: string, tenantCode?: string, ): Promise { const entity = await this.getById(definitionType, id, token, tenantCode); if (entity) { delete (entity as any).id; } return entity; } // Get a single entity async get( req: QueryParams, token?: string, tenantCode?: string, ): Promise { // Implementation } // Query entities async query( req: QueryParams, token?: string, tenantCode?: string, ): Promise { // Implementation } // Query with mapped results async queryMapped( req: QueryParams, token?: string, tenantCode?: string, ): Promise { // Implementation } // Save an entity directly async saveEnt( entity: any, token?: string, tenantCode?: string, ): Promise { // Implementation } // Save an entity with request async save( req: GsbSaveRequest, token?: string, tenantCode?: string, ): Promise { // Implementation } // Update via query async updateQuery( req: QueryParams, token?: string, tenantCode?: string, ): Promise { // Implementation } // Save multiple entities async saveMulti( req: GsbSaveMultiRequest, token?: string, tenantCode?: string, ): Promise { // Implementation } // Execute bulk operations async executeBulk( bulkRequest: GsbBulkRequest, token?: string, tenantCode?: string, ): Promise { // Implementation } // Delete an entity async delete( req: GsbSaveRequest, token?: string, tenantCode?: string, ): Promise { // Implementation } // Delete via query async deleteQuery( req: QueryParams, token?: string, tenantCode?: string, ): Promise { // Implementation } // Additional methods for entity definition and workflow async getDefinition( req: { entityDef: { id?: string; name?: string } }, token?: string, tenantCode?: string, ): Promise { // Implementation } // Workflow related methods async runWorkflow( req: any, token?: string, tenantCode?: string, ): Promise { // Implementation } async startWorkflow( req: any, token?: string, tenantCode?: string, ): Promise { // Implementation } async runWfFunction( req: any, token?: string, tenantCode?: string, ): Promise { // Implementation } async iterateTask( req: any, token?: string, tenantCode?: string, ): Promise { // Implementation } } ``` ## CRUD Operations ### Create ```typescript // Example: Creating a new entity const saveRequest: GsbSaveRequest = { entDefName: "test", // Only one of entDefName, entDefId, or entityDef is required entity: { title: "New Entity", }, }; const response = await entityService.save(saveRequest, token, tenant); // Response will contain: // - message: string (optional) // - status: number (optional) // - id: string (optional) - The ID of the created entity // - isUpdate: boolean (optional) - Whether this was an update (true) or create (false) operation // Alternative: Using saveEnt for simpler saves const entity = { _entDefName: "test", title: "New Entity" }; const response = await entityService.saveEnt(entity, token, tenant); ``` ### Save Operation Details The `save` operation creates a new entity or updates an existing one in the database. When saving an entity without an ID, a new entity is created, and an ID is automatically generated. When saving an entity with an existing ID, the entity is updated. The operation validates the entity against its definition before saving, ensuring data integrity. It also supports complex JSON structures with nested objects and arrays, automatically handling relationships between entities. #### Save Input Parameters | Parameter | Type | Required | Description | | ---------- | ------ | -------- | -------------------------------------------------------------------------------------------- | | entDefName | string | Yes* | Name of the entity definition. Required if entDefId is not provided. | | entDefId | string | Yes* | ID of the entity definition. Required if entDefName is not provided. | | entityDef | object | No | Optional entity definition object with id and/or name properties. | | entity | object | Yes | The entity object to save, which can include nested objects and arrays for related entities. | | token | string | No | Authentication token for your request. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. | #### Save Response ```json { "success": true, "id": "string", // ID of the created or updated entity "isUpdate": boolean, // Whether this was an update (true) or create (false) operation "message": "string", // Optional success message "status": 200 // Optional status code } ``` #### Complex Data Handling The save operation intelligently processes nested objects and arrays: - For each nested entity: - If an ID is provided and exists in the database, the entity will be updated. - If no ID is provided or the ID doesn't exist, a new entity will be created. - Relationships between entities are automatically maintained. - One-to-many and many-to-many relationships are handled through arrays of objects. - One-to-one relationships are handled through nested objects. - All operations are performed in a single transaction, ensuring data consistency. ### Read ```typescript // Example: Querying entities with fluent API const queryParams = new QueryParams("test") .filter("title", "Test", QueryFunction.Like) .skip(0) .take(10) .select("id") .select("title") .sortBy("createdDate", QuerySortType.Descending); const response = await entityService.query(queryParams, token, tenant); // Response will contain: // - message: string (optional) // - status: number (optional) // - entities: any[] (optional) - Array of matching entities // - totalCount: number (optional) - If calcTotalCount was true // Getting entity by ID const entity = await entityService.getById("test", "entity-id", token, tenant); // Getting entity by ID with constructor type class TestEntity { _entDefName = "test"; id?: string; title?: string; } const typedEntity = await entityService.getById( TestEntity, "entity-id", token, tenant, ); ``` ### Query Operation Details The `query` operation fetches entities based on specified query parameters, allowing for complex filtering, sorting, pagination, and analytical queries. It provides a powerful and flexible way to search and retrieve entities based on various criteria. #### Query Input Parameters | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ----------------------------------------------------------------------------- | | entDefName | string | Yes* | Name of the entity definition to query. Required if entDefId is not provided. | | entDefId | string | Yes* | ID of the entity definition to query. Required if entDefName is not provided. | | entityDef | object | No | Optional entity definition object with id and/or name properties. | | selectCols | array | No | Columns to select in the query. If not provided, all columns are selected. | | includes | array | No | Related entities to include in the results. | | filters | array | No | Array of filter conditions to filter the results. | | startIndex | number | No | Pagination start index (0-based). | | count | number | No | Number of records to return. | | sortCols | array | No | Sorting specifications for the results. | | calcTotalCount | boolean | No | Whether to calculate the total count of matching records. | | searchText | string | No | Search term for automatic searching across all searchable fields. | #### Query Filter Functions The query system supports various functions for filtering entities: | Function | Description | Example | | -------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- | | equals | Exact value match | `{ col: { name: "status" }, val: "active", function: "equals" }` | | like | Pattern matching | `{ col: { name: "name" }, val: "John%", function: "like" }` | | greater | Greater than comparison | `{ col: { name: "price" }, val: 100, function: "greater" }` | | smaller | Smaller than comparison | `{ col: { name: "quantity" }, val: 50, function: "smaller" }` | | contains | Array / multi-reference membership (not substring) | `{ col: { name: "roles" }, val: ["role-id"], function: "contains" }` | | in | Value is in a set; on a reference use the dotted id path | `{ col: { name: "roles.id" }, val: ["role-id"], function: "in" }` | | fullTextSearch | Full text search | `{ col: { name: "description" }, val: "search terms", function: "fullTextSearch" }` | | is | Type checking | `{ col: { name: "createDate" }, val: null, function: "is" }` | #### Val Object Structure The `val` property in filters can be either a direct value or an object with additional properties: ```typescript interface ValObject { value?: any; // The actual value to use in the comparison script?: string; // A script to execute to determine the value dynamically nameScript?: string; // A script that returns a display name for the value options?: object; // Additional options for the filter } ``` #### Query Response ```json { "success": true, "entities": [ // Array of matching entities { "id": "string", "property1": "value1", "property2": "value2" // ... } ], "totalCount": 42, // Present only if calcTotalCount is true "message": "string", // Optional message "status": 200 // Optional status code } ``` For analytical queries, the response might include aggregated results: ```json { "success": true, "entities": [ // Array of aggregated results { "groupByField": "value", "count": 42, "sum_amount": 1250.75, "avg_price": 29.99 // Additional aggregates as requested } ], "totalCount": 5, // Number of result groups "message": "string", // Optional message "status": 200 // Optional status code } ``` ### Update ```typescript // Example: Updating an existing entity const updateRequest: GsbSaveRequest = { entDefName: "test", entity: { id: "existing-id", title: "Updated Title", }, }; const response = await entityService.save(updateRequest, token, tenant); // Response will contain: // - message: string (optional) // - status: number (optional) // - id: string (optional) - The ID of the updated entity // Alternative: Using updateQuery for bulk updates const updateQueryParams = new QueryParams("test").filter( "status", "pending", QueryFunction.Equals, ); updateQueryParams.entity = { status: "approved", updatedDate: new Date(), }; const updateResponse = await entityService.updateQuery( updateQueryParams, token, tenant, ); // updateResponse.affectedRowCount shows how many records were updated ``` ### Delete ```typescript // Example: Deleting an entity const deleteRequest: GsbSaveRequest = { entDefName: "test", entityId: "entity-to-delete", }; const response = await entityService.delete(deleteRequest, token, tenant); // Response will contain: // - message: string (optional) // - status: number (optional) // - affectedRowCount: number (optional) - Number of deleted records // Single deletes execute cascade rules and workflow/serverless delete triggers. // Direct bulk delete: does not execute cascades or workflow/serverless delete triggers const deleteQueryParams = new QueryParams("test").filter( "status", "cancelled", QueryFunction.Equals, ); const deleteResponse = await entityService.deleteQuery( deleteQueryParams, token, tenant, ); // deleteResponse.affectedRowCount shows how many records were deleted ``` ### Bulk Operations ```typescript // Example: Executing multiple operations in a single request const bulkRequest: GsbBulkRequest = { bulkCalls: [ { endPoint: "/api/entity/save", request: { entDefName: "test", entity: { title: "First Entity" }, }, reqId: "save1", }, { endPoint: "/api/entity/save", request: { entDefName: "test", entity: { title: "Second Entity" }, }, reqId: "save2", }, { endPoint: "/api/entity/query", request: { entDefName: "test", startIndex: 0, count: 5, }, reqId: "query1", }, ], variation: { tenantCode: "default", }, }; const bulkResponse = await entityService.executeBulk( bulkRequest, token, "default", ); // bulkResponse.results contains individual responses keyed by reqId ``` ## Best Practices 1. **Entity Definition Identification** - Use `entDefName` when you know the entity definition name - Use `entDefId` when you have the entity definition ID - Use `entityDef` object only when it has either name or id property set - For type safety, prefer using class constructors when possible: `new QueryParams(MyEntity)` 2. **Query Operations** - Use the fluent API for readable, chainable query building - Always specify pagination parameters (`skip()` and `take()/limit()`) for large datasets - Use appropriate query functions for filtering - Include only necessary fields in the response using `select()` - For search operations, use the `searchText` parameter for automatic searching across searchable fields - For complex logic, use the Filter class with children and relations - Set `calcTotalCount` to true only when pagination controls require total count - Use `col` and `val` properties in filter objects rather than older property/value naming - For complex filters, leverage the `ValObject` structure with scripts for dynamic values 3. **Save Operations** - Always include the entity ID when updating existing entities - Use `saveEnt()` for simple entity saves when the entity contains `_entDefName` - Use `saveMulti()` for batch entity creations/updates - Use `updateQuery()` for bulk updates based on a query filter - Validate required fields before sending the request - Check the `isUpdate` flag in responses to determine if an entity was created or updated - For complex nested data structures, ensure proper relationships are defined - When saving related entities: - Provide IDs for existing entities to update them - Omit IDs for new entities to create them - All save operations are transactional, ensuring data consistency 4. **Delete Operations** - Verify entity existence before deletion - Use `deleteQuery()` only when conditional bulk deletion does not require cascades or workflow/serverless delete triggers - When lifecycle behavior is required, query the matching IDs and batch individual `delete()` requests - Check `affectedRowCount` to confirm deletion 5. **Error Handling** - Always check response `success` flag - Handle error messages appropriately - Implement proper error recovery mechanisms - Validate inputs before sending requests to avoid validation errors 6. **Security** - Always include valid authentication token - Always include tenant information - Implement proper access control 7. **Performance** - Use pagination for large datasets (`skip()` and `take()`) - Use the singleton instance of `GsbEntityService` for better resource utilization - Set appropriate `disableTransaction` for read-only operations - Consider using `executeBulk()` for multiple related operations - For analytical queries, apply filters before aggregations to reduce processing load - Use appropriate indices on frequently queried columns 8. **Complex Data Handling** - For nested object structures, understand the relationship types: - One-to-one: Represented as nested objects - One-to-many/Many-to-many: Represented as arrays of objects - GSB automatically processes relationships based on entity definitions - All operations within a complex save are performed in a single transaction - If any part of a complex save fails, the entire transaction is rolled back - For large nested structures, consider breaking operations into smaller transactions ## Analytics and Aggregation ### SelectCol Configuration for Analytics ```typescript interface SelectCol { name?: string; // Column name aggregateFunction?: AggregateFunction; // Aggregation function dateModifier?: DateModifier; // Date grouping modifier groupBy?: boolean; // Whether to group by this column script?: string; // Dynamic calculation script fullName?: string; // Full column name title?: string; // Display title value?: any; // Static value nameScript?: string; // Dynamic name script valScript?: string; // Dynamic value script selectAsTitle?: string; // Alias for result } // Available Aggregate Functions enum AggregateFunction { None = 0, Sum = 1, Average = 2, Count = 3, Maximum = 4, Minimum = 5, Variance = 6, } // Available Date Modifiers enum DateModifier { None = 0, Year = 1, Quarter = 2, Month = 3, DayOfYear = 4, DayOfMonth = 5, Week = 6, Weekday = 7, Hour = 8, Minute = 9, Second = 10, Millisecond = 11, } ``` ### Column Scripts and Calculations GSB supports dynamic column calculations and transformations using script expressions: ```typescript interface SelectCol { // ... other properties script?: string; // Dynamic calculation for column value nameScript?: string; // Dynamic calculation for column name valScript?: string; // Dynamic calculation for filter value } ``` #### Using Script for Column Calculations The `script` property allows you to define complex calculations and transformations: ```typescript // Calculate total price as quantity * unit price const totalPriceCol = new SelectCol(); totalPriceCol.script = "[quantity] * [_COALESCE]([productVariant.product.unitPrice],[productVariant.unitPrice])"; totalPriceCol.selectAsTitle = "totalPrice"; ``` ### Date Modifiers Date modifiers allow grouping date fields by different time periods. When used, they append a suffix to the column name in the response: - `Year` → columnName_Year (groups by year) - `Quarter` → columnName_Quarter (groups by quarter) - `Month` → columnName_Month (groups by month) - `DayOfYear` → columnName_DayOfYear (groups by day of year) - `DayOfMonth` → columnName_DayOfMonth (groups by day of month) - `Week` → columnName_Week (groups by week) - `Weekday` → columnName_Weekday (groups by day of week) - `Hour` → columnName_Hour (groups by hour) - `Minute` → columnName_Minute (groups by minute) - `Second` → columnName_Second (groups by second) - `Millisecond` → columnName_Millisecond (groups by millisecond) ### Analytics Query Examples #### Example 1: Count by Date and Type ```typescript // Query to count logs grouped by date and type const query = new QueryParams("GsbSystemLog"); // Configure date column for grouping by day let dateCol = new SelectCol("createDate"); dateCol.groupBy = true; dateCol.dateModifier = DateModifier.DayOfMonth; // Results in createDate_Day in response // Configure count aggregation let countCol = new SelectCol("id"); countCol.aggregateFunction = AggregateFunction.Count; countCol.selectAsTitle = "total"; // Renames the count column to 'total' // Configure type column for grouping let typeCol = new SelectCol("type"); typeCol.groupBy = true; query.selectCols = [dateCol, countCol, typeCol]; // Example response: { "entities": [ { "createDate_Day": "2025-03-29T00:00:00", // Note the _Day suffix "type": 2, "total": 2 // Count result with custom name from selectAsTitle }, // ... more results ] } ``` #### Example 2: Monthly Aggregation with Multiple Metrics ```typescript const query = new QueryParams("GsbSystemLog"); // Group by month let dateCol = new SelectCol("createDate"); dateCol.dateModifier = DateModifier.Month; dateCol.groupBy = true; // Count total occurrences let countCol = new SelectCol("id"); countCol.aggregateFunction = AggregateFunction.Count; // Average value of a numeric field let avgCol = new SelectCol("value"); avgCol.aggregateFunction = AggregateFunction.Average; query.selectCols = [dateCol, countCol, avgCol]; ``` ## Advanced Query Techniques ### Fluent Query Building GSB provides a fluent API for building queries that is type-safe and readable: ```typescript // Type-safe query using a class constructor class Product { _entDefName = "Product"; id?: string; name?: string; price?: number; category?: string; } const query = new QueryParams(Product) .filter((p) => p.category, "Electronics") .filter((p) => p.price, 100, QueryFunction.Greater) .select((p) => p.id) .select((p) => p.name) .select((p) => p.price) .sortBy((p) => p.price, QuerySortType.Descending) .skip(0) .take(10); const response = await entityService.query(query, token, tenant); ``` ### Including Related Entities You can include related entities in a single query: ```typescript // Fetch orders with their related customer and items const query = new QueryParams("Order") .filter("status", "processing") .take(10); // Include customer data const customerInclude = query.include("customer").inc; customerInclude?.select("id").select("name").select("email"); // Include order items with their products const itemsInclude = query.include("items").inc; const productInclude = itemsInclude?.include("product").inc; productInclude?.select("id").select("name").select("price"); const response = await entityService.query(query, token, tenant); ``` ### Complex Filtering Filters form an unlimited AND/OR tree. Two rules: 1. `relation` says how a filter joins to the filter **before** it, and defaults to `And`. It belongs on the right-hand operand. 2. `children` acts as a parenthesis. A group's own `relation` joins **the group** to its preceding sibling — it does not combine the group's children. ```typescript // (status = active OR status = pending) AND (value > 1000 AND priority = high) const activeFilter = new Filter("status", "active", QueryFunction.Equals); const pendingFilter = new Filter("status", "pending", QueryFunction.Equals); // The Or rides on the second operand, not on the group. pendingFilter.relation = QueryRelation.Or; const highValueFilter = new Filter("value", 1000, QueryFunction.Greater); const priorityFilter = new Filter("priority", "high", QueryFunction.Equals); const statusGroup = new Filter(); statusGroup.children = [activeFilter, pendingFilter]; const importanceGroup = new Filter(); importanceGroup.children = [highValueFilter, priorityFilter]; // Joins this group to statusGroup; children stay ANDed by default. importanceGroup.relation = QueryRelation.And; const query = new QueryParams("Task"); query.filters = [statusGroup, importanceGroup]; ``` Setting `statusGroup.relation = QueryRelation.Or` and expecting its two children to OR together is the most common mistake — they stay ANDed, and the query usually returns nothing. `negate` inverts a single filter. It is ignored on a `children` group; to negate a group, invert the operators inside it. ### Best Practices for Advanced Queries 1. **Data Transformation** - Use `selectAsTitle` to provide meaningful aliases for computed or aggregated columns - Consider data transformation for visualization needs by constructing the response in a format suitable for charts or UI components - For complex transformations, use scripts with column calculations 2. **Performance Optimization** - Apply query filters first to reduce the dataset before applying aggregations - Use appropriate indices on frequently queried columns - Consider denormalization for commonly accessed aggregated data - For large datasets, use pagination and incremental loading 3. **Error Handling** - Validate all filter values before sending queries - Handle null/undefined values in transformations - Provide appropriate fallbacks for missing data points 4. **Bulk Operations** - Use bulk operations for related entity updates - Consider transaction boundaries for operations that need to be atomic - Use save/update queries for performing mass updates efficiently 5. **Security** - Be aware that all column scripts and queries are subject to permission checks - Test security with users having different permission levels - Don't rely on client-side filtering for security - always enforce server-side --- # Build queries with QueryParams Source: /guides/query-params # Build queries with QueryParams Pikbase uses one query model across Entity Service, the CLI, MCP tools, and serverless functions. TypeScript callers use the fluent `QueryParams` builder; JSON callers send the fields produced by that builder. ## Fluent TypeScript ```typescript import { IncludeQuery, QueryFunction, QueryParams, QuerySortType, } from "@gsb-core/core"; const query = new QueryParams("Order") .filter("status", "open", QueryFunction.Equals) .include(new IncludeQuery("customer").select(["id", "name"])) .self.sortBy("createDate", QuerySortType.Descending) .skip(0) .take(25) .select(["id", "status", "total", "createDate"]) .returnCount(); const result = await entityService.query(query); ``` Every method returns the query. After `include()`, use `.inc` to configure the included query or `.self` to continue configuring the root. ## CLI and MCP Methods are not executable JSON. Send their serialized fields: ```bash gsb call query --input '{ "queryParams": { "entDefName": "Order", "filters": [{ "col": { "name": "status" }, "val": { "value": "open" }, "function": 0 }], "selectCols": [{ "name": "id" }, { "name": "status" }], "startIndex": 0, "count": 25, "calcTotalCount": true } }' --raw ``` The CLI accepts the older tenant code-library fields `query`, `propVal`, and `colName` for compatibility, but normalizes them before calling Entity Service. Use `filters`, `col/val`, and `name` in all new code. ## Keep queries bounded - Select only fields the caller uses. - Always set `skip()` and `take()` for lists. - Request `returnCount()` only when the interface needs a total. - Use `include()` instead of issuing N+1 queries. - Prefer `getById` when the identifier is already known. Continue with [Advanced and extreme queries](/guides/advanced-queries) for correlated subqueries, aggregate comparisons, set membership, grouped analytics, and nested boolean logic. See the [query operation reference](/api/query) for every field, function enum, and response shape. --- # Advanced and extreme queries Source: /guides/advanced-queries # Advanced and extreme queries The Pikbase query model is more than a list-filter API. A column descriptor can resolve a field, literal, script, or subquery; filters can compare either side; and nested queries can aggregate related data. Together these primitives cover most operational reporting and selection rules without a custom endpoint. Use these techniques deliberately. Bound returned rows, select only required columns, and inspect the generated query before putting an analytical query on a hot path. ## Correlated aggregate subqueries with __PARENT Inside a tenant serverless function, `__PARENT.` references the row currently being evaluated by the immediately enclosing query. This makes a nested query correlated rather than global. The following query selects a cluster only when its capacity is greater than the number of tenants assigned to that same cluster: ```typescript const query = new EntityQueryParams(_defs.GsbCluster); query.query = []; query.count = 1; query.select((cluster) => cluster.id); query.sortBy((cluster) => cluster.priority, _enums.QuerySortType.Desc); const hasCapacity: SingleQuery = { col: { name: "capacity" }, val: { name: "tenants", valQuery: { selectCols: [{ name: "id", aggregateFunction: "Count" }], queries: [ { col: { name: "cluster_id" }, val: { name: "__PARENT.id" }, function: "Equals", }, ], }, }, function: "Greater", }; query.query.push(hasCapacity); const result = await entityService.query(query); const availableCluster = result.entities[0]; ``` Here `val.name: "tenants"` establishes the related collection used by the nested query, `Count(id)` produces one scalar value, and `__PARENT.id` binds each tenant count to the cluster currently under evaluation. Without that parent reference, the count would describe the complete inner result set rather than the current cluster. Use `__PARENT` only in trusted tenant-runtime queries. It is a query expression, not a value supplied by an end user, and it is not the syntax for ordinary browser-side filtering. ## Set membership from another query Use `val.valQuery` with `MatchArrays` when the right-hand side can return many values. This example finds users whose role is selected by another query: ```json { "entDefName": "GsbUser", "filters": [{ "col": { "name": "roles.id" }, "val": { "valQuery": { "entDefName": "GsbRole", "selectCols": [{ "name": "id" }], "filters": [{ "col": { "name": "title" }, "val": { "value": "Administrator" }, "function": 0 }] } }, "function": 27 }], "distinct": true, "count": 100 } ``` `MatchArrays` treats the subquery result as a set. Use `Equals` only when the subquery is guaranteed to produce one scalar row. Multi-reference joins can repeat the outer row, so select distinct results when appropriate. ## Compare a row with a global aggregate An aggregate `selectCols` entry turns a subquery into a scalar. This filter selects files larger than the average file size: ```json { "col": { "name": "size" }, "val": { "valQuery": { "entDefName": "GsbFile", "selectCols": [{ "name": "size", "aggregateFunction": 2 }] } }, "function": 2 } ``` The same shape supports maximum, minimum, sum, count, and variance. Add filters to the inner query to compute the threshold from a selected population. ## Compare two fields on the same row `val` is also a column descriptor. Give it a `name` instead of a literal `value` to compare fields: ```json { "col": { "name": "createDate" }, "val": { "name": "lastUpdateDate" }, "function": 4 } ``` This is useful for stale-state checks, date ordering, reconciliations, and invariant audits. ## Group and aggregate by a date part Combine `groupBy`, `dateModifier`, and `aggregateFunction` for analytical projections: ```json { "entDefName": "Order", "selectCols": [ { "name": "createDate", "dateModifier": 2, "groupBy": true }, { "name": "id", "aggregateFunction": 3, "selectAsTitle": "orderCount" }, { "name": "total", "aggregateFunction": 1, "selectAsTitle": "revenue" } ], "count": 24 } ``` This groups orders by calendar month, counts them, and sums revenue. Add ordinary filters to constrain tenant-visible records, status, region, or date range. ## Build nested boolean logic Use `children` as parentheses. A filter's `relation` joins it to the preceding sibling, while a child group's relation joins the complete group to its preceding sibling: ```json { "filters": [ { "col": { "name": "active" }, "val": { "value": true }, "function": 0 }, { "relation": "and", "children": [ { "col": { "name": "priority" }, "val": { "value": "high" }, "function": 0 }, { "relation": "or", "col": { "name": "overdue" }, "val": { "value": true }, "function": 0 } ] } ], "count": 50 } ``` The result is `active AND (priority = high OR overdue)`. Use `negate` for a single predicate; do not use it to invert a child group. ## Know the boundaries - Authorization and row policies still apply to outer and nested queries. A query shape never grants access. - `Equals` with a multi-row scalar subquery fails; use `MatchArrays` for a value set. - Distinct outer rows may not make `calcTotalCount` a safe pagination count for a multi-reference join. - Includes retrieve bounded related data; they are not a substitute for an unbounded graph traversal. - Correlated aggregates can be expensive because the inner expression is evaluated in the context of outer rows. Filter early and return few rows. - External values still belong in `val.value`; never construct scripts or field names from untrusted input. See [Build queries with QueryParams](/guides/query-params) for the basic builder and transport forms, and the [query operation reference](/api/query) for the complete wire model and enum values. --- # Transactions, triggers, and commit control Source: /guides/transactions-triggers # Transactions, triggers, and commit control Pikbase carries transaction context across related Entity Service writes made during a backend function or synchronous workflow run. This lets a business operation update more than one entity while preserving one database consistency boundary. ## Atomic multi-level saves A save payload can contain nested one-to-one objects and one-to-many or many-to-many arrays at multiple levels. Entity Service identifies creates and updates from record IDs, maintains the relationships, and executes the graph write in one transaction. If validation or persistence fails anywhere in that graph, the database rolls back the complete save. ## Attached workflow triggers Entity definitions can carry workflow triggers for entity operations. A trigger selects its entity definition and operation, can run before or after that operation, and can be ordered, run in parallel, create a workflow instance, or apply to each element of a bulk request. Triggers are optional: ordinary entity operations do not require a workflow. ## Transactional functions and workflows The tenant runtime assigns a transaction ID and propagates it into Entity Service calls made by the function or workflow. Pending database edits therefore participate in the runtime transaction instead of becoming unrelated writes. When execution reports an error before a commit checkpoint, the pending database edits are rolled back. ## Manual commit checkpoints Declarative functions and workflow activities can include a Commit changes operation. It flushes every pending entity change in the current transaction, allowing a long operation to establish an intentional checkpoint before continuing. Work committed before that checkpoint is durable and is not undone by a later failure. ## Side-effect boundary Database rollback cannot retract an email already sent, a payment already submitted, or another external API side effect. Keep irreversible calls after required database validation, use explicit commit points deliberately, and design external integrations for idempotency and compensation. --- # Build static applications on Pikbase Source: /guides/static-apps # Build static applications on Pikbase A Pikbase application does not need an application server merely to reach its backend. CORS is configured for direct browser calls to `https://{tenantCode}.gsbapps.net` through the Core client and established user authentication flow. Build the interface, publish static assets to the host or CDN you choose, and keep the governed backend in Pikbase. ## What stays static The application shell, routes, components, styles, and public assets can be compiled to HTML, CSS, and JavaScript. Deploy them to a static host, object store, CDN, or edge-pages provider. There is no app-owned API proxy, ORM server, database connection, or authorization service to deploy beside them. ## What Pikbase operates - Entity definitions, relational queries, nested transactional saves, and files - Authentication plus entity, row-policy, and property-level authorization - Multilingual application content, search, templates, and generated documents - Tenant-resident functions and shared code libraries - Durable workflows, timers, human tasks, triggers, and execution logs The browser calls Entity Service directly for operations its signed-in user may perform. Backend enforcement remains authoritative; static hosting does not move authorization into client code. ## When custom backend logic is needed Publish a tenant function for secrets, privileged operations, external integrations, calculations, or domain rules that should not run in the browser. Use a workflow when the process spans functions, people, timers, or external events. Both execute in the Pikbase tenant runtime, so adding backend behavior does not require provisioning or deploying a separate application server. ## Delivery model 1. Define the entities and permissions in Pikbase. 2. Build the frontend against the Core client. 3. Publish the frontend's static output anywhere. 4. Push optional functions, libraries, templates, and workflows to the tenant. 5. Operate data and backend behavior from the Pikbase console, CLI, or authorized agent tools. This removes an application-server deployment from the ordinary release path. Provider accounts, domains, frontend hosting, and any external services remain under the team's control. --- # Data tables, policies, and search Source: /guides/data-access # Data tables, policies, and search Pikbase keeps the table, query, and access model on the same entity contract. A client can render columns and filters from an entity definition, but Entity Service remains responsible for enforcing access on every query and save. ## Definition-driven data tables Entity definitions describe properties, relationships, searchable fields, and presentation metadata. The shared data table turns those definitions into columns, type-aware filters, server sorting, pagination, relation selectors, and saved query views. The browser does not become the authority for access merely because it renders the table. ## Row and operation policies A permission policy can target users, roles, groups, positions, or departments and carry an operation mask for read, create, delete, edit, query, and execute. Its `queryStr` expresses the record condition applied by the backend, enabling row-level policies based on the same query model used by Entity Service. Priority, query count limits, paging rules, and nested-only behavior further bound permitted access. ## Column policies Entity definitions can set base `propertyPermissions` for all properties. An individual property can supply its own `permissions` to override that default. This provides field-level control for sensitive columns while leaving explicitly permitted fields available. Enforcement belongs to the backend data operation; hiding a column in the table is presentation, not authorization. ## Key-based upserts Save can identify an existing entity without its generated `id`. A definition may mark more than one property as a primary key. Supplying any configured primary key, such as an existing `GsbUser.email`, updates the matching row instead of inserting a duplicate. Properties marked `isPartialPrimaryKey` work as one composite identifier. Supply every partial-key property in the save payload; when the complete combination exists, Entity Service updates that row, and when it does not, Entity Service creates one. An incomplete partial-key set is not an identity match. Partial-primary-key matching through a reference property or its `*_id` companion is not yet a published guarantee. Use a generated ID or verified scalar key fields for that case until its reference-field behavior is contract-tested. ## Atomic calculated saves Set a property to `_CALC(expression)` to calculate its new value from the row currently stored by Entity Service. Direct properties on that row use references such as `[viewCount]`; `_CALC([viewCount] + 1)` increments the counter inside the save transaction instead of reading it into the application and writing it back later. Numeric literals are supported, including integers and decimals. Combine them with direct row properties, arithmetic, comparisons, parentheses, and supported functions: `_CALC([totalCount] + 1)` is the simple case, while `_CALC([_ROUND](([_COALESCE]([subtotal], 0) + [_COALESCE]([shipping], 0)) * 1.20 - [_ABS]([discount]), 2))` composes several fields and operations. Expression complexity is not artificially limited, but every property, token, and character must belong to the allowlists below. For a key-based upsert, append a create fallback after a colon. `_CALC([_COALESCE]([balance], 0) + 25) : 25` increments the matching row by 25, or initializes `balance` to 25 when the key combination creates a row. Reference direct properties on the saved row as `[propertyName]`. The supported expression tokens are: | Group | Supported tokens | |---|---| | Null and conditions | `NULL`, `CASE`, `WHEN`, `THEN`, `ELSE`, `END`, `COALESCE`, `NULLIF`, `IS`, `NOT`, `AND`, `OR` | | Numeric | `ABS`, `ROUND`, `CEILING`, `FLOOR`, `POWER`, `SQRT`, `EXP`, `LOG` | | Text | `LEN`, `LENGTH`, `LTRIM`, `RTRIM`, `SUBSTRING`, `SUBSTR`, `UPPER`, `LOWER`, `REPLACE`, `CONCAT` | | Aggregate | `COUNT`, `SUM`, `AVG`, `MIN`, `MAX` | Inside an expression, write a supported token in bracketed underscore form, such as `[_COALESCE]`, `[_ROUND]`, or `[_CASE] ... [_WHEN] ... [_THEN] ... [_ELSE] ... [_END]`. This is an allowlist, not arbitrary SQL execution. Outside recognized bracketed property and operator names, expressions allow only digits, spaces, and `+-*/.,():?\ <>=`. Apostrophes are not allowed, so quoted string literals and static string concatenation are not currently supported. Text operations such as `CONCAT`, `REPLACE`, `UPPER`, and `LOWER` work with referenced string properties, for example `[_CONCAT]([firstName], [lastName])`. Store fixed text in a property or calculate it outside `_CALC` instead of embedding a quoted literal. The token is `_CALC` with one leading underscore, not `__CALC`. Validate and convert interpolated values to their expected primitive types; never concatenate untrusted text into a calculation expression. See the [save operation reference](/api/save) for examples and transaction behavior. ## Full-text and multilingual search Mark a property searchable, multilingual, and full-text indexed in its definition. Entity Service supports configured `searchText` search plus `FullTextSearch` and `PhraseSearch` query operators. Requests carry the active `langCode`, so multilingual property values are read and searched in the caller's language context. Combine these controls with nested filters, includes, sorting, pagination, and selected columns through [QueryParams](/guides/query-params). --- # Multilingual documents and email delivery Source: /guides/document-delivery # Multilingual documents and email delivery Pikbase provides the tenant runtime for template storage, language selection, rendering, PDF generation, attachment assembly, and email execution. The tenant supplies and controls its own delivery provider credentials; Pikbase does not silently send from a shared customer identity. ## Application content management The multilingual infrastructure is not limited to documents. Model application content as ordinary entities and mark localized properties as multilingual. When an application calls `setGsbLangCode` with the user's selected language, the Core client persists that choice and automatically adds it as `variation.langCode` on subsequent tenant requests. Entity Service returns the matching language variant, so navigation, catalog content, articles, help text, and other CMS-managed content use the same governed data contract as the rest of the application. A caller can override the language for an individual request when needed. ## Multilingual templates A `GsbDocTemplate` can store one HTML body or multilingual content keyed by language code, including `en_us`, `tr_tr`, `de_de`, `fr_fr`, `es_es`, and other supported variants. Template expressions bind entity, workflow, contact, and parameter data at render time. The renderer selects the language variant from the explicit `langCode` or the active request/workflow variation. ## HTML and PDF generation Use the document-generation operation to render a selected template as PDF and store the generated file on an entity or workflow instance. The operation supports a target property, generated filename, HTML input, byte-array output, and Puppeteer rendering when the document requires browser-grade layout. ## Template email The email function resolves the recipient language, renders the selected document template, translates the subject, and sends HTML or text content. Email operations support To, Cc, Bcc, Reply-To, priority, distinct delivery, and file attachments. ## Generated PDF attachments In a function or workflow, generate the localized PDF first and pass the resulting file to the following email operation as an attachment. The document and message can use the same language context and entity data, keeping invoices, statements, confirmations, and notices consistent. ## Register a delivery provider Configure SMTP or SES for the tenant with the provider account, sender identity, region/server, port, TLS, and credentials required by that provider. Credentials remain server-side in tenant runtime settings. Pikbase supplies rendering, orchestration, attachment handling, retry-capable SMTP delivery, and execution logs; the provider remains responsible for sender verification, reputation, quotas, and final transport. --- # Design entity schemas Source: /guides/schema-design # GSB Schema Management Guide ## Table of Contents 1. [Overview](#overview) 2. [Schema Creation Best Practices](#schema-creation-best-practices) 3. [Core Schema Types](#core-schema-types) 4. [Entity Definition Management](#entity-definition-management) 5. [Property Management](#property-management) 6. [Schema Operations](#schema-operations) 7. [Best Practices](#best-practices) ## Overview The Pikbase platform provides a comprehensive framework for defining and managing data schemas through entity definitions. This guide covers how to work with GSB schema components to create, read, update, and delete data tables and their properties. ## Schema Creation Best Practices ### Creating Initial Schema When creating an initial schema with multiple related entity definitions: 1. **Create entity definitions without reference types first**: - Build all your base entity definitions with standard properties (string, number, etc.) - Save these entities before adding reference properties 2. **Add reference properties in a second pass**: - After all entity definitions exist, add reference properties - GSB automatically manages the bidirectional relationship ### Reference Property Management When adding reference properties between entities: 1. **Add reference to only one entity**: - Only add the reference property to one of the related entity definitions - Specify the correct `refEntDef_id` and `refEntPropName` - GSB automatically adds the corresponding reference property to the other definition 2. **Foreign key handling**: - For single relationships (OneToOne, ManyToOne), GSB automatically adds an `_id` property - For example, adding `customer` ref property to an Order entity will automatically create `customer_id` field 3. **Bidirectional management**: - When you delete a reference property, GSB automatically removes: - The corresponding reference property in the related entity - Any automatically created foreign key fields ### Example ```typescript // Example: Customer has Orders, Order has Customer // 1. First create basic entity definitions await entityDefService.createDataTable( 'Customer', 'Customer Information', 'Stores customer data' ); await entityDefService.createDataTable( 'Order', 'Order Information', 'Stores order data' ); // 2. Then add the reference property to just one entity await entityDefService.addColumn( 'customer-entity-id', // Customer entity { name: 'orders', title: 'Orders', description: 'Customer orders', definition_id: '924acba8-58c5-4881-940d-472ec01eba5f', // Reference type refEntDef_id: 'order-entity-id', // Order entity refEntPropName: 'customer', // Name of property in Order entity refType: RefType.OneToMany } ); // GSB automatically: // 1. Adds 'customer' property to Order entity // 2. Adds 'customer_id' to Order entity for the database relationship ``` ## Core Schema Types ### Entity Definition (GsbEntityDef) The `GsbEntityDef` interface represents a data table in the GSB system: ```typescript export interface GsbEntityDef { id?: string; // Unique identifier name?: string; // Entity name (must be unique) title?: string; // Display title description?: string; // Description dbTableName?: string; // Database table name publicAccess?: boolean; // Whether entity is publicly accessible activityLogLevel?: ActivityLogLevel; // Level of activity logging properties?: GsbProperty[]; // Array of properties (columns) isActive?: boolean; // Whether entity is active isDeleted?: boolean; // Whether entity is deleted createDate?: Date; // Creation date (system-managed) lastUpdateDate?: Date; // Last update date (system-managed) createdBy_id?: string; // Creator ID (system-managed) lastUpdatedBy_id?: string; // Last updater ID (system-managed) permissions?: GsbPermission[]; // Entity permissions workflowTriggers?: GsbWorkflowTrigger[]; // Associated workflow triggers } ``` ### Property (GsbProperty) The `GsbProperty` interface represents a column in a data table: ```typescript export interface GsbProperty { id?: string; // Unique identifier name?: string; // Property name (must be unique within entity) title?: string; // Display title description?: string; // Description definition_id?: string; // Reference to property definition (data type) orderNumber?: number; // Display order isRequired?: boolean; // Whether property is required isSearchable?: boolean; // Whether property is searchable isUnique?: boolean; // Whether property must have unique values isPrimaryKey?: boolean; // Whether property is a primary key isIndexed?: boolean; // Whether property is indexed maxLength?: number; // Maximum length (for strings) defaultValue?: string; // Default value // Reference properties refEntDef_id?: string; // Referenced entity definition ID refEntPropName?: string; // Property name in referenced entity refType?: RefType; // Reference type (OneToOne, OneToMany, etc.) // UI control properties formModes?: number; // Form modes where property is visible listScreens?: ScreenType; // List screens where property is visible // Additional properties enum_id?: string; // Enum ID (for enum properties) isMultiLingual?: boolean; // Whether property supports multiple languages isEncrypted?: boolean; // Whether property value is encrypted regex?: string; // Validation regex pattern // System properties isDefault?: boolean; // Whether it's a default property type?: string; // Property type name } ``` ### Property Definition (GsbPropertyDef) The `GsbPropertyDef` interface represents a data type definition: ```typescript export interface GsbPropertyDef { id: string; // Unique identifier dataType: DataType; // Data type enum value title: string; // Display title name: string; // Type name description?: string; // Description maxLength?: number; // Maximum length scale?: number; // Scale (for decimal numbers) regex?: string; // Default validation regex usage?: number; // Usage counter createDate?: Date; // Creation date lastUpdateDate?: Date; // Last update date defaultControlComponent?: { // Default UI component title: string; id: string; }; } ``` ## Entity Definition Management ### Creating an Entity Definition To create a new data table, use the `EntityDefService`: ```typescript import { EntityDefService } from '@gsb-core/core'; const entityDefService = EntityDefService.getInstance(); // Create a basic data table const tableId = await entityDefService.createDataTable( 'Customer', // Table name 'Customer Information', // Display title 'Stores customer data' // Description ); // Create a more complex entity definition const entityDef: GsbEntityDef = { name: 'Product', title: 'Product Catalog', description: 'Product information and inventory data', properties: [ // Default properties will be added automatically // Add custom properties { name: 'price', title: 'Price', description: 'Product price', definition_id: '35efcf9c-fff0-44d4-8972-73a9a32b93fa', // Number type isRequired: true, isSearchable: false, orderNumber: 10 }, { name: 'category', title: 'Category', description: 'Product category', definition_id: 'c6c34bf3-f51b-4e69-a689-b09847be74b9', // String type isSearchable: true, orderNumber: 11 } ] }; const entityId = await entityDefService.createEntityDef(entityDef); ``` ### Default Properties 1. `id` - Primary key (UUID), Required 2. `title` - Display title, better to define automated form builders use this field 3. `createdBy` - User who created the record (If a property with this name is defined GSB will atuomatically set its value) 4. `lastUpdatedBy` - User who last updated the record (If a property with this name is defined GSB will atuomatically set its value) 5. `createDate` - Creation timestamp (If a property with this name is defined GSB will atuomatically set its value) 6. `lastUpdateDate` - Last update timestamp (If a property with this name is defined GSB will atuomatically set its value) ### Retrieving Entity Definitions ```typescript // Get by ID const entityDef = await entityDefService.getEntityDefById('entity-id'); // Get by name const customerTable = await entityDefService.getDataTableByName('Customer'); // Get all tables with pagination const { entityDefs, totalCount } = await entityDefService.getEntityDefs(1, 10); // Search for tables const { entityDefs, totalCount } = await entityDefService.searchEntityDefs('customer', 1, 10); // Get all tables const allTables = await entityDefService.getAllDataTables(); ``` ### Updating Entity Definitions ```typescript // Update an entity definition const entityDef = await entityDefService.getEntityDefById('entity-id'); if (entityDef) { entityDef.title = 'Updated Title'; entityDef.description = 'Updated description'; const success = await entityDefService.updateEntityDef(entityDef); } ``` ### Deleting Entity Definitions ```typescript // Soft delete (sets isDeleted flag) const success = await entityDefService.deleteEntityDef('entity-id'); // Permanent delete (removes table and data) const success = await entityDefService.permanentlyDeleteDataTable('entity-id'); ``` ## Property Management ### Adding Properties ```typescript // Add a simple string property await entityDefService.addColumn( 'entity-id', { name: 'address', title: 'Address', description: 'Customer address', definition_id: 'c6c34bf3-f51b-4e69-a689-b09847be74b9', // String type isSearchable: true } ); // Add a reference property await entityDefService.addColumn( 'entity-id', { name: 'category', title: 'Category', description: 'Product category', definition_id: '924acba8-58c5-4881-940d-472ec01eba5f', // Reference type refEntDef_id: 'category-entity-id', refEntPropName: 'products', refType: RefType.OneToMany } ); ``` ### Common Property Types GSB provides several pre-defined property types: | Type | Definition ID | Description | |------|--------------|-------------| | ID | 5c0aa76f-9c32-4e7e-a4bc-b56e93877883 | Unique identifier | | String | c6c34bf3-f51b-4e69-a689-b09847be74b9 | Text string | | Number | 35efcf9c-fff0-44d4-8972-73a9a32b93fa | Numeric value | | Boolean | 7868afdf-2709-45be-87e3-87de8d35f30f | True/false value | | DateTime | 12e647e0-ebd2-4ec2-a4e3-82c1dfe07da2 | Date and time | | Reference | 924acba8-58c5-4881-940d-472ec01eba5f | Entity reference | | Enum | 7bf08f4f-7de0-469e-bbfb-f4c43762f4d7 | Enumerated value | | RichText | e07f578e-2705-49c1-b97f-3ca5963c67c0 | Rich text content | | Email | df7ce94b-d59c-4b67-8519-aa4c98ab477c | Email address | | Password | 7291fbc2-a7cf-4713-a876-0cff085cc035 | Password field | ### Removing Properties ```typescript // Remove a property by name await entityDefService.removeColumn('entity-id', 'propertyName'); // Remove a property by ID await entityDefService.removeColumn('entity-id', 'property-id'); ``` ## Schema Operations ### Checking Name Uniqueness Before creating a new entity or property, check if the name is already used: ```typescript // Check entity name uniqueness const { entityDefs } = await entityDefService.checkNameUniqueness('Customer'); const isNameUnique = entityDefs.length === 0; // Check reference property name uniqueness const { isValid, validationMessage } = await entityDefService.checkRefPropNameUniqueness( 'products', 'category-entity-id' ); ``` ### Working with References GSB supports different types of entity relationships: ```typescript enum RefType { OneToOne = 1, OneToMany = 2, ManyToOne = 3, ManyToMany = 4 } ``` When creating a reference property: 1. Set `definition_id` to the Reference type ID 2. Set `refEntDef_id` to the referenced entity's ID 3. Set `refEntPropName` to create a back-reference property in the referenced entity 4. Set `refType` to define the relationship type Example: ```typescript // Create a one-to-many relationship from Category to Product await entityDefService.addColumn( 'product-entity-id', { name: 'category', title: 'Category', definition_id: '924acba8-58c5-4881-940d-472ec01eba5f', // Reference type refEntDef_id: 'category-entity-id', refEntPropName: 'products', // Creates a 'products' property in Category entity refType: RefType.ManyToOne } ); ``` ## Best Practices ### Entity Definition Naming 1. **Use PascalCase for entity names**: `Customer`, `ProductCategory`, `OrderItem` 2. **Use singular nouns**: `Product` instead of `Products` 3. **Be descriptive but concise**: `CustomerAddress` instead of `CustAddr` or `CustomerAddressInformation` 4. **Avoid special characters**: Use only letters, numbers, and underscores 5. **Start with a letter**: Entity names must start with a letter ### Property Naming 1. **Use camelCase for property names**: `firstName`, `orderDate`, `productCategory` 2. **Be descriptive**: `customerAddress` instead of `custAddr` 3. **Use consistent naming patterns**: `createDate`/`updateDate` instead of mixing `createDate`/`modifiedOn` 4. **Prefix boolean properties with 'is' or 'has'**: `isActive`, `hasAttachments` ### Schema Design 1. **Normalize appropriately**: Break down complex entities into related tables 2. **Use references instead of duplicating data**: Link to a Customer entity instead of duplicating customer fields 3. **Add appropriate indexes**: Mark frequently searched fields as `isIndexed: true` 4. **Set searchable fields**: Mark fields that should be included in search as `isSearchable: true` 5. **Define required fields**: Mark mandatory fields as `isRequired: true` 6. **Set appropriate field lengths**: Define `maxLength` for string fields ### Performance Considerations 1. **Cache entity definitions**: GSB automatically caches entity definitions 2. **Limit the number of properties**: Too many columns can impact performance 3. **Use appropriate data types**: Use the most specific type for each property 4. **Index wisely**: Only index fields used in filters and sorts 5. **Use reference relationships appropriately**: Choose the right relationship type ### Security Best Practices 1. **Set appropriate permissions**: Define who can view and modify each entity 2. **Mark sensitive fields as encrypted**: Use `isEncrypted: true` for sensitive data 3. **Use publicAccess flag carefully**: Only set `publicAccess: true` when necessary 4. **Implement field-level security**: Control which users can see specific fields 5. **Audit important changes**: Set appropriate `activityLogLevel` --- # Schema Manager API Source: /guides/schema-manager # GSB Schema Management API Documentation ## Overview The GSB Schema Management API provides a comprehensive set of operations for managing entity definitions (data tables) in your GSB applications. All operations require authentication via a token, and optionally accept a tenant code. ## Key Concepts 1. **Entity Definition**: Represents a data table in the database 2. **Property**: Represents a column in a data table 3. **Property Definition**: Defines the data type and behavior of properties 4. **References**: Define relationships between entities ## Available Documentation Each method below has its own reference page under `/guides/schema-manager/`. MCP and CLI clients read the same text through the `getSchemaManagerDocs` and `inspectSchemaDocs` tools. ### Entity Definition Management - **[getCommonPropertyDefs](/guides/schema-manager/getcommonpropertydefs)**: Get available property data types - **[createEntityDef](/guides/schema-manager/createentitydef)**: Create a new data table - **[updateEntityDef](/guides/schema-manager/updateentitydef)**: Update an existing data table - **[getEntityDef](/guides/schema-manager/getentitydef)**: Get a data table by ID - **[queryEntityDefs](/guides/schema-manager/queryentitydefs)**: Get paginated list of data tables ### Property Management - **[addProperty](/guides/schema-manager/addproperty)**: Add a column to a data table - **[removeProperty](/guides/schema-manager/removeproperty)**: Remove a column from a data table - **[updateProperty](/guides/schema-manager/updateproperty)**: Update a column in a data table ## Best Practices 1. **Entity Definition Naming** - Use PascalCase for entity names - Use singular nouns - Be descriptive but concise - Avoid special characters - Start with a letter 2. **Property Naming** - Use camelCase for property names - Be descriptive - Use consistent naming patterns - Prefix boolean properties with 'is' or 'has' 3. **Schema Design** - Normalize appropriately - Use references instead of duplicating data - Add appropriate indexes - Set searchable fields - Define required fields - Set appropriate field lengths --- # getCommonPropertyDefs() — Schema Manager Source: /guides/schema-manager/getcommonpropertydefs # Get Common Property Definitions Retrieves the list of common property definitions that can be used when creating entity properties. ## Request Format ```typescript getCommonPropertyDefs( token: string, // Authentication token tenantCode?: string // Optional tenant code ): Promise<{ success: boolean, data?: Record, error?: string }> ``` ## Response Format ```json { "success": boolean, "data": { // Dictionary of property definitions with their IDs as keys }, "error": "string" // Present only if success is false } ``` ## Common Property Definition Types | Type | Definition ID | Description | |------|--------------|-------------| | ID | 5c0aa76f-9c32-4e7e-a4bc-b56e93877883 | Unique identifier | | String | c6c34bf3-f51b-4e69-a689-b09847be74b9 | Text string | | Number | 35efcf9c-fff0-44d4-8972-73a9a32b93fa | Numeric value | | Boolean | 7868afdf-2709-45be-87e3-87de8d35f30f | True/false value | | DateTime | 12e647e0-ebd2-4ec2-a4e3-82c1dfe07da2 | Date and time | | Reference | 924acba8-58c5-4881-940d-472ec01eba5f | Entity reference | | Enum | 7bf08f4f-7de0-469e-bbfb-f4c43762f4d7 | Enumerated value | | RichText | e07f578e-2705-49c1-b97f-3ca5963c67c0 | Rich text content | | Email | df7ce94b-d59c-4b67-8519-aa4c98ab477c | Email address | | Password | 7291fbc2-a7cf-4713-a876-0cff085cc035 | Password field | ## Example ```typescript const result = await getCommonPropertyDefs("your-auth-token"); // Access a specific property definition const stringType = result.data["c6c34bf3-f51b-4e69-a689-b09847be74b9"]; ``` --- # createEntityDef() — Schema Manager Source: /guides/schema-manager/createentitydef # Create Entity Definition Creates a new entity definition (data table) with the specified schema. ## Request Format ```typescript createEntityDef( entityDef: GsbEntityDef, // Entity definition object token: string, // Authentication token tenantCode?: string // Optional tenant code ): Promise<{ success: boolean, data?: string, // ID of the created entity definition error?: string }> ``` ## GsbEntityDef Structure ```typescript interface GsbEntityDef { id?: string; // Unique identifier (auto-generated if not provided) name?: string; // Entity name (must be unique) title?: string; // Display title description?: string; // Description dbTableName?: string; // Database table name publicAccess?: boolean; // Whether entity is publicly accessible activityLogLevel?: ActivityLogLevel; // Level of activity logging properties?: GsbProperty[]; // Array of properties (columns) isActive?: boolean; // Whether entity is active isDeleted?: boolean; // Whether entity is deleted permissions?: GsbPermission[]; // Entity permissions workflowTriggers?: GsbWorkflowTrigger[]; // Associated workflow triggers } ``` ## Default Properties When creating a new entity definition, these properties are automatically added: 1. `id` - Primary key (UUID), Required 2. `title` - Display title 3. `createdBy` - User who created the record 4. `lastUpdatedBy` - User who last updated the record 5. `createDate` - Creation timestamp 6. `lastUpdateDate` - Last update timestamp ## Response Format ```json { "success": boolean, "data": "string", // ID of the created entity definition "error": "string" // Present only if success is false } ``` ## Example ```typescript const entityDef = { name: "Customer", title: "Customer Information", description: "Stores customer data", properties: [ { name: "email", title: "Email Address", description: "Customer email", definition_id: "df7ce94b-d59c-4b67-8519-aa4c98ab477c", // Email type isRequired: true, isUnique: true, isSearchable: true, orderNumber: 10 }, { name: "phoneNumber", title: "Phone Number", description: "Customer phone number", definition_id: "c6c34bf3-f51b-4e69-a689-b09847be74b9", // String type isSearchable: true, orderNumber: 20 } ] }; const result = await createEntityDef(entityDef, "your-auth-token"); ``` --- # updateEntityDef() — Schema Manager Source: /guides/schema-manager/updateentitydef # Update Entity Definition Updates an existing entity definition with new schema information. ## Request Format ```typescript updateEntityDef( entityDef: GsbEntityDef, // Entity definition object with ID token: string, // Authentication token tenantCode?: string // Optional tenant code ): Promise<{ success: boolean, data?: boolean, // Success status error?: string }> ``` ## Important Notes - The `id` field in the entityDef object is required for updates - Only the fields provided in the entityDef object will be updated - To update properties, use the dedicated property management methods ## Response Format ```json { "success": boolean, "data": boolean, // True if update was successful "error": "string" // Present only if success is false } ``` ## Example ```typescript const entityDef = { id: "12345", // Required for update title: "Updated Customer Information", description: "Updated customer data storage" }; const result = await updateEntityDef(entityDef, "your-auth-token"); ``` ## Schema Evolution When updating entity definitions: 1. Changing `name` or `dbTableName` will rename the database table 2. Setting `isActive: false` will disable operations on the entity 3. Updating `publicAccess` will change security settings 4. Modifying `activityLogLevel` will change audit trail behavior Be careful when updating entity definitions in production systems, as some changes may affect existing data or application behavior. --- # getEntityDef() — Schema Manager Source: /guides/schema-manager/getentitydef # Get Entity Definition Retrieves an entity definition by its ID. ## Request Format ```typescript getEntityDef( entityDefId: string, // ID of the entity definition to retrieve token: string, // Authentication token tenantCode?: string // Optional tenant code ): Promise<{ success: boolean, data?: { entityDef: GsbEntityDef // Entity definition object }, error?: string }> ``` ## Response Format ```json { "success": boolean, "data": { "entityDef": { "id": "string", "name": "string", "title": "string", "description": "string", "dbTableName": "string", "publicAccess": boolean, "activityLogLevel": number, "properties": [ // Array of GsbProperty objects ], "isActive": boolean, "isDeleted": boolean, "createDate": "string", "lastUpdateDate": "string", "createdBy_id": "string", "lastUpdatedBy_id": "string", "permissions": [ // Array of permission objects ], "workflowTriggers": [ // Array of workflow trigger objects ] } }, "error": "string" // Present only if success is false } ``` ## Example ```typescript const result = await getEntityDef("entity-id", "your-auth-token"); const entityDef = result.data.entityDef; ``` ## Using Entity Definition Data The retrieved entity definition contains complete schema information that can be used for: 1. **Metadata Exploration**: Understanding the structure of the entity 2. **Dynamic UI Generation**: Building forms or tables based on properties 3. **Schema Modification**: Making changes to the entity definition 4. **Relationship Analysis**: Examining references between entities --- # queryEntityDefs() — Schema Manager Source: /guides/schema-manager/queryentitydefs # Query Entity Definitions Retrieves a paginated list of entity definitions. ## Request Format ```typescript queryEntityDefs( page: number, // Page number (1-based) pageSize: number, // Number of items per page token: string, // Authentication token tenantCode?: string // Optional tenant code ): Promise<{ success: boolean, data?: { entityDefs: GsbEntityDef[], // Array of entity definitions totalCount: number // Total number of entity definitions }, error?: string }> ``` ## Response Format ```json { "success": boolean, "data": { "entityDefs": [ // Array of entity definition objects ], "totalCount": number }, "error": "string" // Present only if success is false } ``` ## Example ```typescript // Get the first page with 10 entity definitions per page const result = await queryEntityDefs(1, 10, "your-auth-token"); // Access the entity definitions and total count const { entityDefs, totalCount } = result.data; // Calculate total pages const totalPages = Math.ceil(totalCount / 10); ``` ## Pagination - Page numbers start at 1 - If there are no results for the specified page, an empty array is returned - The `totalCount` field indicates the total number of entity definitions available ## Use Cases 1. **Schema Browser**: Building a UI to explore available data tables 2. **Data Dictionary**: Creating documentation of the data model 3. **Dependency Analysis**: Finding relationships between entities 4. **Schema Governance**: Monitoring entity definitions for compliance --- # addProperty() — Schema Manager Source: /guides/schema-manager/addproperty # Add Property Adds a new property (column) to an existing entity definition. ## Request Format ```typescript addProperty( entityDefId: string, // ID of the entity definition to modify property: GsbProperty, // Property definition to add token: string, // Authentication token tenantCode?: string // Optional tenant code ): Promise<{ success: boolean, data?: boolean, // Success status error?: string }> ``` ## GsbProperty Structure ```typescript interface GsbProperty { id?: string; // Unique identifier (auto-generated if not provided) name?: string; // Property name (must be unique within entity) title?: string; // Display title description?: string; // Description definition_id?: string; // Reference to property definition (data type) orderNumber?: number; // Display order isRequired?: boolean; // Whether property is required isSearchable?: boolean; // Whether property is searchable isUnique?: boolean; // Whether property must have unique values isPrimaryKey?: boolean; // Whether property is a primary key isIndexed?: boolean; // Whether property is indexed maxLength?: number; // Maximum length (for strings) defaultValue?: string; // Default value // Reference properties refEntDef_id?: string; // Referenced entity definition ID refEntPropName?: string; // Property name in referenced entity refType?: RefType; // Reference type (OneToOne, OneToMany, etc.) // UI control properties formModes?: number; // Form modes where property is visible listScreens?: number; // List screens where property is visible // Additional properties enum_id?: string; // Enum ID (for enum properties) isMultiLingual?: boolean; // Whether property supports multiple languages isEncrypted?: boolean; // Whether property value is encrypted regex?: string; // Validation regex pattern } ``` ## Reference Types When creating reference properties, use one of these reference types: ```typescript enum RefType { OneToOne = 1, OneToMany = 2, ManyToOne = 3, ManyToMany = 4 } ``` ## Response Format ```json { "success": boolean, "data": boolean, // True if property was added successfully "error": "string" // Present only if success is false } ``` ## Example: Adding a Simple Property ```typescript const property = { name: "address", title: "Address", description: "Customer address", definition_id: "c6c34bf3-f51b-4e69-a689-b09847be74b9", // String type isSearchable: true, orderNumber: 30 }; const result = await addProperty("entity-id", property, "your-auth-token"); ``` ## Example: Adding a Reference Property ```typescript const property = { name: "category", title: "Category", description: "Product category", definition_id: "924acba8-58c5-4881-940d-472ec01eba5f", // Reference type refEntDef_id: "category-entity-id", refEntPropName: "products", // Creates a 'products' property in Category entity refType: 3, // ManyToOne orderNumber: 40 }; const result = await addProperty("product-entity-id", property, "your-auth-token"); ``` ## Auto-Mirror Properties When adding a reference property with `refEntPropName` specified: 1. GSB automatically creates the mirror property in the referenced entity 2. The relationship is managed bidirectionally 3. For ManyToMany relationships, a mapping table is created automatically --- # updateProperty() — Schema Manager Source: /guides/schema-manager/updateproperty # Update Property Updates an existing property in an entity definition. ## Request Format ```typescript updateProperty({ entityDefId: string, // ID of the entity definition to modify propertyName: string, // Name of the property to update property: GsbProperty, // Updated property definition token?: string, // Authentication token tenantCode?: string // Optional tenant code }): Promise<{ success: boolean, data?: boolean, // Success status error?: string }> ``` ## Important Notes - The `name` field in the property object identifies which property to update - Only the fields provided in the property object will be updated - Some property attributes cannot be changed after creation (e.g., `definition_id`) ## Response Format ```json { "success": boolean, "data": boolean, // True if property was updated successfully "error": "string" // Present only if success is false } ``` ## Example ```typescript const property = { name: "address", // Identifies which property to update title: "Updated Address", description: "Updated customer address", isRequired: true, maxLength: 200 }; const result = await updateProperty({ entityDefId: "entity-id", propertyName: "address", property: property, token: "your-auth-token" }); ``` ## Safe vs. Unsafe Updates ### Safe Updates (No data loss risk) - `title` - `description` - `orderNumber` - `isSearchable` - `isIndexed` - `formModes` - `listScreens` ### Potentially Unsafe Updates (May affect data) - `isRequired` (if changing from false to true) - `isUnique` (if changing from false to true) - `maxLength` (if decreasing) - `regex` (if adding or making more restrictive) ### Unsafe Updates (May require data migration) - `definition_id` (changing data type) - `refEntDef_id` (changing referenced entity) - `refType` (changing relationship type) --- # removeProperty() — Schema Manager Source: /guides/schema-manager/removeproperty # Remove Property Removes a property (column) from an existing entity definition. ## Request Format ```typescript removeProperty( entityDefId: string, // ID of the entity definition to modify propertyName: string, // Name of the property to remove token: string, // Authentication token tenantCode?: string // Optional tenant code ): Promise<{ success: boolean, data?: boolean, // Success status error?: string }> ``` ## Response Format ```json { "success": boolean, "data": boolean, // True if property was removed successfully "error": "string" // Present only if success is false } ``` ## Example ```typescript const result = await removeProperty("entity-id", "address", "your-auth-token"); ``` ## Important Notes 1. **Data Loss Warning**: Removing a property will delete all data stored in that column 2. **Default Properties**: Some default properties (like `id`) cannot be removed 3. **Reference Properties**: When removing a reference property: - The mirror property in the referenced entity is also removed - For ManyToMany relationships, the mapping table may be dropped 4. **Dependent Components**: Check for UI components or business logic that depend on the property before removal --- # Serverless functions Source: /guides/serverless-functions # GSB Serverless Functions: Comprehensive Guide This document provides a comprehensive guide to building backend serverless functions on the Pikbase platform. It covers the fundamental concepts, available services, best practices, and examples to help developers and AI build robust functions. ## 1. Introduction to GSB Serverless Functions GSB Serverless Functions are JavaScript code snippets that run on the GSB backend in response to various events or triggers. They allow developers to extend and customize the platform's functionality without managing server infrastructure. Functions are typically defined in a JSON structure, which includes: - `id`: A unique identifier for the function. - `name`: A human-readable name for the function. - `code`: The JavaScript code for the function. - `references`: An array of objects containing library/service definition IDs that the function depends on. Each object has the format `{"id": "uuid-of-the-service"}`. This tells the runtime which services (like `GsbEntityService`, `GsbUtil`, etc.) to make available to the function's scope. - `operations` (optional): A JSON string defining a sequence of declarative operations that can be part_of the function's execution. These operations can include setting entity properties, running scripts, sending notifications, etc. ### Execution Environment When a GSB serverless function executes, it has access to a specific environment and a set of global objects: - `_runtime`: An object providing methods to interact with the function's execution context (e.g., ending the function, logging, accessing tenant information). - `_instance`: An object containing instance-specific data relevant to the current function execution, such as the triggering entity or parameters passed to the function. - `_defs`: An object that provides access to GSB entity definitions. This allows you to strongly-type entity objects (e.g., `let order = new _defs.GsbPrtOrder();`). - `_enums`: An object providing access to various enumerations defined in GSB (e.g., `_enums.OrderStatus.Cancelled`). - Service Instances: Services referenced in the function's `references` array are available as pre-instantiated objects or classes that you can instantiate (e.g., `GsbEntityService`, `Utils`). The runtime also injects these values directly into function and library scope. Use them as globals; do not call `require()` or add package imports for them: - Execution context: `_runtime`, `runtime`, `_instance`, `instance`, `tenantCode`, `_tenantCode`. - Platform data: `Big`, `_defs`, `_enums`, `_cache`, `_settings`. - Templates and utilities: `nunjucks`, `nunjucks_date_filter`, `nunjucks_comma_filter`, `axios`, `xlsx`, `cheerio`, `fetch`, `FormData`, `multer`. - Messaging and storage: `nodemailer`, `aws_s3`, `aws_ses`, `aws_translate`, `aws_sqs`, `azure_storage_blob`, `azure_storage_queue`, `bullmq`, `ioredis`. For example, create an SMTP transporter with `nodemailer.createTransport(...)`, not `require("nodemailer")`. All I/O operations, especially calls to GSB services, are asynchronous. Therefore, functions heavily rely on JavaScript's `async/await` syntax and `Promise`s. ## 2. Core Concepts ### `_runtime` Object The `_runtime` object is crucial for controlling the function's lifecycle and interacting with the GSB environment. A function can finish through any of these methods: - `_runtime.end(code = 200, message = "", result = "", response = undefined, action = undefined)` - `code`: The HTTP response status. This is part of the caller-facing transport contract, not an application-specific error code. - `message`: An execution message. - `result`: The workflow result used to select a conditional route. - `response`: The payload returned to the caller. - `action`: A process-action name such as `"BreakActivity"` that controls what the workflow engine does next. - `_runtime.success(result = "", response = undefined, action = undefined)`: Ends successfully with HTTP 200. `result` still controls workflow routing; it is not a display message. - `_runtime.error(exception = undefined, message = undefined, userMessage = undefined, action = undefined, code = undefined)`: Ends with an error. Pass the caught exception separately from the internal `message`, a safe caller-facing `userMessage`, the workflow `action`, and the HTTP status `code`. Runtime termination does not make JavaScript control flow unreachable. Always return immediately: `return _runtime.end(...)`, `return _runtime.success(...)`, or `return _runtime.error(...)`. This prevents writes, external calls, or a second termination attempt after the runtime has ended. ### Standard response boundary New and migrated serverless functions should reference `serverlessResponseService` and create one `ServerlessResponseService` per execution. It preserves every `_runtime.end` dimension through named options, validates HTTP statuses, gives expected failures stable application codes, and emits one error envelope: `{ success: false, error: { code, message, issues, details? } }` - Use `return responseService.respond({ httpStatus, message, result, response, action })` for a completed outcome. Omitted `httpStatus` is 200. - Use `responseService.fail({ code, httpStatus, message, userMessage?, issues?, details?, result?, action? })` for an expected domain failure. It throws a typed boundary error; default workflow failure control is `result: "Error"` and `action: "BreakFunction"`. - Use `responseService.validation({ issues, ... })` for request validation. Its default code is `INVALID_REQUEST` and default status is 400. - Catch once at the outer execution boundary and `return responseService.sendError(error, fallback?)`. Unknown exceptions become the sanitized fallback 500 response. - Do not call `_runtime.error` in migrated functions. It ends with 500 unless its final positional code is explicitly supplied, and positional arguments are easy to misapply. Example: ```typescript const responseService = new ServerlessResponseService(_runtime); async function runAll() { try { if (!order) { responseService.fail({ code: "ORDER_NOT_FOUND", httpStatus: 404, message: "The order was not found.", }); } return responseService.respond({ result: "approved", response: { orderId: order.id }, }); } catch (error) { return responseService.sendError(error); } } runAll().then(); ``` Pass `action` as its canonical text name, for example `"BreakActivity"` or `"CancelWorkflow"`. The runtime also accepts `_enums.ProcessAction.BreakActivity` and legacy numeric values, but text is clearer in authored code and serialized requests. IntelliSense exposes all supported names and numeric compatibility values through `RuntimeProcessAction`. Use proper HTTP statuses. Typical choices are `200` for success, `400` for malformed input, `401` for missing authentication, `403` for denied authorization, `404` for a missing resource, `409` for a state conflict, `422` for a semantically invalid request, `429` for rate limiting, and `500`, `502`, or `503` for server or dependency failures. Do not return HTTP 200 for an error. | Process-action name | Engine behavior | |---|---| | `Continue` | Continue normal processing | | `BreakOperation` | Stop the current operation | | `BreakFunction` | Stop the current function | | `BreakActivity` | Stop the current activity | | `BreakWorkflow` | Stop the current workflow | | `ReRunOperation` | Run the current operation again | | `ReRunFunction` | Run the current function again | | `ReRunActivity` | Run the current activity again | | `RestartWorkflow` | Restart the workflow | | `CancelWorkflow` | Cancel the workflow | - `_runtime.log(message, operation, exception, type)`: Logs a message to the GSB logging system. (Often superseded by `GsbLogService`). - `_runtime.route(routeName)`: Used in workflow functions to direct the workflow to a specific route. - `_runtime.token`: Accesses the current user's authentication token, useful for making API calls. - `_runtime.tenantCode`: Accesses the current tenant's code. - `_runtime.apiUrl`: Base API URL for the current execution. - `_runtime.userToken`: Current user token when one is available. - `_runtime.instance`: The current function instance. - `_runtime.transactionId`: Current execution transaction ID. - `_runtime.hostName` / `_runtime.protocol`: API host and protocol for the current execution. - `_runtime.settings`: The same module settings exposed by `instance?.moduleSettings`. - `_runtime.variation`: The same variation exposed by `instance?.variation`. The runtime constructs this context before executing authored code: `apiUrl`, `token`, `userToken`, `instance`, `transactionId`, `hostName`, `protocol`, `settings`, and `variation` are therefore execution-owned values. Do not replace them with caller-supplied parameters or persist tokens from them in entities, logs, or background-job payloads. ### Operational capability notes These paths have different contracts and should not be treated as interchangeable: - The SMTP-backed `sendEmail` function uses `SmtpService` and the injected `nodemailer` global. After the EU1 backend upgrade, plain email and an in-memory PDF attachment were both delivered successfully. The attachment arrived with MIME type `application/pdf` and disposition `attachment`. - Declarative notification operation type `13` successfully sends ordinary email. A tested `GsbFile` PDF attachment was omitted, so this path must not be used when attachment delivery is required until its mapping is repaired and revalidated. - The custom SES library is an alternate provider path, but it requires tenant-owned `_settings.aws.ses` configuration. A missing setting is a configuration failure, not a reason to embed provider credentials in function source. - Task scheduling uses the runtime-provided `bullmq` and `ioredis` modules. The current `groupOperations` model schedules the SMTP-backed `sendEmail` function by stable function ID. Re-run schedule, worker, status, retry, cancellation, tenant-isolation, and cleanup tests after backend changes before declaring the complete task path healthy. - Redis connection details and credentials must move out of `TaskSchedulerService` source into server-owned configuration. Rotate the existing credential during that migration, and do not serialize runtime or user tokens into queue jobs. - PDF document generation currently fails when launching Chromium with `spawn /usr/bin/chromium ENOENT`. The backend image or renderer configuration must provide a valid executable path before operation type `12` document generation can be considered available. SMTP attachment delivery working does not prove PDF rendering works. ### `_instance` Object The `_instance` object provides data specific to the current invocation of the serverless function. - `_instance.entity`: Often represents the primary GSB entity that the function is operating on. Its type can be cast using `_defs` (e.g., `let order = _instance.entity as _defs.GsbPrtOrder;`). - `_instance.entity_id`: The ID of the primary entity. - `_instance.prms`: An object to store or pass parameters within a function's execution, especially useful in workflows with multiple steps or when setting up sub-flow instances. For example, `_instance.prms.dynamicAssignRole = 'role_id';` or `_instance.prms.subFlowInstances = [];`. - `_instance.response`: Can be used to build up a response object during the function's execution. - `_instance.result`: Can hold a result message or status. - `_instance.parentResult`: In sub-flows, this can be used to set a result for the parent flow. ### `_defs` (Definitions) The `_defs` object acts as a namespace for all GSB entity type definitions. This is extremely useful for: - Strongly typing variables: `let newInvoice = new _defs.GsbPrtInvoice();` - IntelliSense and code completion in supporting IDEs. - Clarity and maintainability of code. Example: `let customerOrder = _instance.entity as _defs.GsbPrtOrder;` `let newProduct = new _defs.GsbInvProduct({ title: "New Product" });` ### `_enums` (Enumerations) The `_enums` object provides access to predefined sets of constants (enumerations) used throughout GSB. This helps avoid using "magic strings" or numbers and improves code readability. Example: `upOrder.status = _enums.OrderStatus.Completed;` `if (order.type == _enums.OrderType.Sales) { ... }` `payment.paymentOption.paymentType == _enums.PaymentType.BankTransfer` Common `_enums` include: - `OrderStatus` - `InvoiceStatus` - `PaymentStatus` - `OrderQuantityStatus` - `QuerySortType` - `QueryFunction` (for query conditions) - `QueryRelation` (AND/OR for query conditions) - `ProcessAction` (for workflow control) - `DataPassType` - `OrderSubProcessType` - `TriggerGroup` ### Asynchronous Operations Nearly all interactions with services (EntityService, ApiService, etc.) are asynchronous and return Promises. Always use `async` for functions that contain such calls and `await` to get their results. ```javascript async function myAsyncFunction() { try { let entityService = new GsbEntityService(_runtime); let order = await entityService.getById(_defs.GsbPrtOrder, 'some-order-id'); // ... process order ... } catch (error) { return _runtime.error(error, "Order lookup failed.", "The order could not be loaded.", undefined, 500); } } ``` ## 3. Available Services (from Code Library) GSB provides a set of built-in services, defined in the "Code Library," that functions can use by referencing their IDs. When a code library is referenced in a function, the code library is automatically added to the function's scope. All you need to do is to reference the code library in the function's `references` array and instantiate it. For example if you have entity service in your code library, you can directly instantiate it like this: ```javascript let entityService = new GsbEntityService(_runtime); ``` ### `GsbApiService` Service for making general API calls, either to internal GSB endpoints or external HTTP/HTTPS services. **Definition ID:** `57bfbcdd-95a2-4d0f-9efb-67433c5b83fa` **Instantiation:** ```javascript let apiService = new GsbApiService(_runtime); ``` **Key Methods:** - `callApi(req, endPoint, tenantCode = undefined, token = this.runtime.token): Promise` - Calls a GSB API endpoint. - `req`: The request object. - `endPoint`: The API endpoint path (e.g., `/api/entity/queryJson`). - `tenantCode` (optional): Tenant code. - `token` (optional): Bearer token. Defaults to `_runtime.token`. ```javascript // Example: // let response = await apiService.callApi({ someData: 'value' }, '/api/custom/endpoint'); ``` - `httpCall(dto: HttpCallRequest, callback, errorCallback)`: (Legacy) Calls an HTTP/HTTPS API with callbacks. Prefer `httpCallAsync`. - `httpCallAsync(dto: HttpCallRequest): Promise`: Calls an HTTP/HTTPS API asynchronously. - `dto`: An `HttpCallRequest` object. ```javascript // Example: // let request = new HttpCallRequest(); // request.method = "GET"; // request.protocol = "https"; // request.hostName = "api.example.com"; // request.path = "/data"; // request.bearerToken = "some_token"; // let externalData = await apiService.httpCallAsync(request); ``` - `convertToRequestData(requestParameters: Object): string`: Converts an object to a request data string (likely query string). #### `HttpCallRequest` Class Used with `httpCallAsync` and `httpCall`. ```javascript class HttpCallRequest { method = "POST"; protocol = "https"; hostName; port = "443"; path; content; // Body of the request bearerToken; contentType = "application/json"; headers; // Object for additional headers jsonResponse = true; skipCheckErrorStatus = false; } ``` ### `GsbEntityService` The primary service for interacting with GSB entities (CRUD operations, queries, etc.). **Definition ID:** `99e4c845-3032-458f-996f-8db3302f4e38` **Instantiation:** ```javascript let entityService = new GsbEntityService(_runtime); ``` The constructor `constructor(_runtime)` also initializes `this.apiService = new GsbApiService(_runtime);` internally. **Key Methods:** - `query(req: EntityQueryParams, tenantCode?, token?): Promise`: Queries entities. - `req`: An `EntityQueryParams` object defining the query. - `delete(req: EntityQueryParams, tenantCode?, token?): Promise`: Deletes entities matching the query. - `queryMapped(req: EntityQueryParams, tenantCode?, token?): Promise`: Queries entities with mapping. - `get(req: EntityQueryParams, tenantCode?, token?): Promise`: Retrieves a single entity based on query parameters (expects one result). - `save(req: GsbSaveRequest, tenantCode?, token?): Promise`: Saves a single entity. - `req`: A `GsbSaveRequest` object, which includes `entDefName` and `entity` data. ```javascript // Example: Save // let newCustomer = new _defs.GsbPartyCustomer({ name: "New Co" }); // let saveReq = new GsbSaveRequest(); // saveReq.entDefName = "GsbPartyCustomer"; // saveReq.entity = newCustomer; // let saveResponse = await entityService.save(saveReq); // let newCustomerId = saveResponse.id; ``` - `saveEnt(entity: any, tenantCode?, token?): Promise`: A more direct way to save an entity. The entity object should be an instance of a `_defs` class. The service infers `entDefName`. ```javascript // Example: saveEnt // let productToUpdate = new _defs.GsbInvProduct({ id: 'existing-id', price: 29.99 }); // await entityService.saveEnt(productToUpdate); ``` - `updateQuery(req: EntityQueryParams, tenantCode?, token?): Promise` (Note: Response type might be `GsbQueryOpResponse` based on library, often `GsbSaveResponse` is used but `affectedRowCount` is the key) - Updates entities matching a query. The `req` object's `entity` property should contain the fields to update. ```javascript // Example: Update entities matching a query // let whQuantitesUpdateReq = new EntityQueryParams(_defs.GsbPrtOrderQuantity); // whQuantitesUpdateReq.filter("order_id", entity.id); // // The 'entity' property of EntityQueryParams holds the update payload // whQuantitesUpdateReq.entity = { status: _enums.OrderQuantityStatus.Cancelled } as _defs.GsbPrtOrderQuantity; // await entityService.updateQuery(whQuantitesUpdateReq); ``` - `saveMulti(req: GsbSaveMultiRequest, tenantCode?, token?): Promise`: Saves multiple entities of the same type. - `req`: A `GsbSaveMultiRequest` object with `entDefName` and an `entities` array. - `getCode(req: GsbGetCodeRequest, tenantCode?, token?): Promise`: Generates a unique code (e.g., order number). - `getById(definitionType: (new () => T) | string, id: string): Promise`: Retrieves an entity by its ID. - `definitionType`: The entity class from `_defs` (e.g., `_defs.GsbPrtOrder`) or its name as a string. - `id`: The entity's ID. ```javascript // Example: getById // let order = await entityService.getById(_defs.GsbPrtOrder, 'order-uuid'); // if (order) { /* ... */ } ``` - `runWorkflow(req, tenantCode?, token?): Promise`: Executes a workflow. - `startWorkflow(req, tenantCode?, token?): Promise`: Starts a workflow. - `runWfFunction(req, tenantCode?, token?): Promise`: Runs a workflow function. - `iterateTask(req, tenantCode?, token?): Promise`: Iterates a task in a workflow. - `getCopy(definitionType: (new () => T) | string, id: string): Promise`: Retrieves a copy of an entity. #### Entity Service Data Models (Requests/Responses) These classes are typically used as parameters or return types for `GsbEntityService` methods. They generally extend `GsbResponseBase { message?: string; status?: string; }`. - `GsbGetCodeRequest { codeGeneratorId?: string; }` - `GsbGetCodeResponse extends GsbResponseBase { code?: string; }` - `GsbDeleteResponse extends GsbResponseBase { deleteCount: number; }` - `GsbSaveRequest { entDefName?: string; entDefId?: string; entityDef?: any; entity?: any; query?: any[]; }` - `GsbSaveResponse extends GsbResponseBase { id?: string; }` - `GsbSaveMultiRequest { entDefName?: string; entDefId?: string; entityDef?: any; entities?: any[]; }` - `GsbSaveMultiResponse extends GsbResponseBase { ids?: string[]; }` - `GsbQueryResponse extends GsbResponseBase { entities?: any[]; }` - `GsbGetResponse extends GsbResponseBase { entity?: any; }` - `GsbQueryOpResponse extends GsbResponseBase { affectedRowCount?: number; }` (for update/delete operations) - `GsbDefinitionResponse extends GsbResponseBase { entityDef?: any; }` - `GsbSaveMappedRequest { entDefName: string; entDefId?: string; entityDef?: any; items?: any; entityId?: string; propName?: string; }` ### `GsbLogService` Provides methods for structured logging. **Definition ID:** `ca071e22-184e-443b-b7e5-05ad29a1dc13` **Instantiation:** ```javascript let logService = new GsbLogService(_runtime); ``` **Key Methods:** Each method returns a `Promise`. - `log(msg, operation, exception, type): Promise`: Generic log method. - `logError(msg, operation?, exception?): Promise` - `logInfo(msg, operation?, exception?): Promise` - `logWarning(msg, operation?, exception?): Promise` - `logCritical(msg, operation?, exception?): Promise` ```javascript // Example: // await logService.logInfo("User logged in", "UserLogin", { userId: user.id }); // try { // // ... some operation ... // } catch (e) { // await logService.logError("Failed to process order", "OrderProcessing", e); // return _runtime.error(e, "Order processing failed.", "The order could not be processed.", undefined, 500); // } ``` ### `GsbUtil` (available as `Utils`) A collection of utility functions. It's often available directly as a pre-instantiated `Utils` object if referenced. **Definition ID:** `085d21cb-349f-4a99-b5dc-679c8ee7947e` The library defines `const Utils = new GsbUtil();`, so you can use `Utils` directly. **Key Methods:** - `getSettings(): any`: Gets system settings. - `translate(text, langCode): string`: Translates text. - `getMlDictionary(): []`: Gets the multilingual dictionary. - `equalsCaseInsensitive(v1: string, v2: string): boolean`: Case-insensitive string comparison. - `idEquals(v1: string, v2: string): boolean`: Compares two GSB IDs (handles nulls/empties gracefully). - `isIdEmpty(id): boolean`: Checks if a GSB ID is null, undefined, or an empty string. ```javascript // if (!Utils.isIdEmpty(entity.invoice_id)) { /* ... */ } ``` - `removeNestedObjects(obj, exceptionProps = []): any`: Removes nested objects, useful for simplifying objects before saving. - `newId(): string`: Generates a new GSB-compatible unique ID (GUID). ```javascript // let newItem = new _defs.MyEntity({ id: Utils.newId(), name: "Test" }); ``` - `checkPropId(entity, propName, createIfNotExists = false)`: Checks if a linked entity property (e.g., `entity.customer_id` and `entity.customer`) has an ID, optionally creating one. - `sortByOrderNum(arr: any[], prop = undefined): any[]`: Sorts an array of objects by an `orderNumber` property (or a custom property). - `deepCopy(source): any`: Creates a deep copy of an object. - `isAdmin(): boolean`: Checks if the current user is an admin. - `entityHasMoreThanId(entity:any) : boolean`: Checks if an entity object has properties other than just its `id`. ## 4. Building Queries with `QueryParams` The `QueryParams` (and its subclass `EntityQueryParams`) classes are fundamental for fetching data from GSB. **Definition ID (QueryParams):** `1c9bc14d-2151-4e14-b6c8-63c0421c1243` **Instantiation:** ```javascript // For a specific entity type: let eqp = new EntityQueryParams(_defs.GsbPrtOrder); // or by entity definition name string let eqpByName = new EntityQueryParams("GsbPrtOrder"); // For includes (nested queries): let includeQuery = new IncludeQuery("items"); // "items" is the property name ``` **Key Features & Methods:** - **Defining the Entity:** - The constructor takes the entity definition (`_defs.YourEntity` or `"YourEntityName"`). - `entDefName`: String name of the entity definition. - `entDefId`: ID of the entity definition. - **Filtering (`filter`):** - `filter(propName, value, queryFunction?, relation?)` adds a `Filter` to `filters` and returns the query. - Property names may be strings (including dotted paths) or typed expressions such as `(item) => item.customer.name`. - Use `QueryFunction` for comparisons and `QueryRelation.And` / `QueryRelation.Or` to combine filters. ```javascript eqp .filter("status", OrderStatus.Pending, QueryFunction.Equals) .filter("customer.name", "John%", QueryFunction.ILike) .filter("totalAmount", 100, QueryFunction.Greater); ``` - The serialized field is `filters`, where each item uses `col: { name }`, `val: { value }`, and `function`. - **Selecting Columns (`select`):** - `select(col: (string | string[] | ((item: T) => any)), options?: SelectCol): QueryParams`: Specifies which properties to retrieve. - `col`: Can be a property name string, an array of property names, or a lambda function for typed selection. - `options`: A `SelectCol` object for advanced options (aliasing, aggregation - though aggregation is less common in basic serverless functions). ```javascript eqp.select(["id", "orderNumber", "customer.name"]); ``` - `selectCols?: SelectCol[]`: Array of `SelectCol` objects. - `SelectCol { name, aggregateFunction?, dateModifier?, script?, groupBy?, selectAsTitle? }` - **Including Related Entities (`include`):** - `include(...colNames)`: Includes related entities and returns the root query with `.inc` and `.self` helpers. - `colNames`: Property names of navigation properties. - `.inc` is the latest `IncludeQuery`; `.self` resumes the root query. ```javascript // Example: Include order items and the product for each item const orderQuery = new EntityQueryParams("GsbPrtOrder") .filter("id", "some-order-id") .include(new IncludeQuery("items").select(["id", "quantity"])) .self.select(["id", "orderNumber"]); // let result = await entityService.query(orderQuery); ``` - `includes?: IncludeQuery[]`: Array of `IncludeQuery` objects. - **Sorting (`sortBy`):** - `sortBy(colName: (string | ((item: T) => any)), sortType: _enums.QuerySortType): QueryParams` - `colName`: Property name or lambda. - `sortType`: `_enums.QuerySortType.Asc` or `_enums.QuerySortType.Desc`. ```javascript eqp.sortBy("createdDate", QuerySortType.Descending); ``` - `sortCols?: SortCol[]`: Array of `SortCol` objects. - `SortCol { col: SelectCol, sortType: _enums.QuerySortType }` - **Pagination:** - `skip(startIndex)` sets the zero-based offset. - `take(count)` / `limit(count)` bounds the returned records. - `returnCount()` requests the total matching count. - **Fluent Interface:** - `type`, `filter`, `search`, `select`, `include`, `sortBy`, `skip`, `take`, `limit`, `pickEntity`, `returnCount`, and `setName` return the query. - CLI and MCP callers send the serialized `filters`, `selectCols`, `includes`, and `sortCols` fields rather than method calls. **Example of `EntityQueryParams` for an update operation:** As seen in `Function (1).json` (e.g., "Cancel Order" function): ```javascript // To update entities, you set the 'entity' property on the EntityQueryParams object // with the new values. The query part defines WHICH entities to update. let whQuantitesUpdateReq = new EntityQueryParams(_defs.GsbPrtOrderQuantity); whQuantitesUpdateReq.filter("order_id", _instance.entity_id); // This is the payload for the update: let updatePayload = { status: _enums.OrderQuantityStatus.Cancelled } as _defs.GsbPrtOrderQuantity; whQuantitesUpdateReq.entity = updatePayload; // GsbEntityService.updateQuery uses this // await entityService.updateQuery(whQuantitesUpdateReq); ``` ## 5. Working with Operations In the `Function (1).json` structure, some functions have an `operations` field. This field contains a JSON string representing an array of operation objects. Each operation defines a step in a process. ```json // Snippet from Function (1).json { // ... other function properties ... "operations": "[{\"id\":\"GUID\",\"orderNumber\":1,\"operationType\":10, ... }, ...]", // ... } ``` - `operationType`: Indicates the type of operation (e.g., 7 for "Script", 10 for "Set Entity Properties", 9 for "Get Entity", 13 for "Notification"). - `scriptCode`: If `operationType` is for a script, this field contains the JavaScript code (often base64 encoded or escaped). This script runs within the same GSB function context. - `setEntityOptions`: For operations that modify entities, this defines which properties to set. - `getEntityOptions`: For operations that fetch entities, this defines query parameters. - `notification`: Defines notification settings if the operation sends one. When a function includes an `operations` definition, the GSB platform likely processes these operations sequentially. If an operation is a script, that script is executed. These scripts can use all the same `_runtime`, `_instance`, services, etc., as a primary function defined in the main `code` property. This allows for a mix of declarative (JSON-defined operations) and imperative (JavaScript) logic within a single GSB function definition. ## 6. Function Structure and Examples Based on `Function (1).json`, a common pattern for GSB serverless functions is: ```javascript // 1. Instantiate necessary services (ensure they are in 'references' array as objects with id properties) let entityService = new GsbEntityService(_runtime); let logService = new GsbLogService(_runtime); // let Utils = new GsbUtil(); // Usually available globally as Utils if referenced // 2. Access instance data (if needed) let currentOrder = _instance.entity as _defs.GsbPrtOrder; let params = _instance.prms; // 3. Define the main logic in an async function async function mainProcess() { try { // 4. Implement the function's logic // Example: Fetch related data let customerReq = new EntityQueryParams(_defs.GsbPartyCustomer); customerReq.filter("id", currentOrder.customer_id); let customerResp = await entityService.get(customerReq); let customer = customerResp.entity; if (!customer) { await logService.logWarning("Customer not found for order: " + currentOrder.id, "mainProcess"); return _runtime.error(undefined, "Customer lookup returned no record.", "Customer not found.", undefined, 404); } // Example: Update the order let orderUpdate = new _defs.GsbPrtOrder({ id: currentOrder.id, status: _enums.OrderStatus.Processed, processedDate: new Date() }); await entityService.saveEnt(orderUpdate); await logService.logInfo("Order processed: " + currentOrder.id, "mainProcess"); // 5. End the function execution return _runtime.success("processed", { orderId: currentOrder.id }); } catch (error) { // 6. Handle errors await logService.logError("Error in mainProcess", "mainProcess", error); return _runtime.error(error, "Order processing failed.", "The order could not be processed.", undefined, 500); } } // 7. Invoke the main async function mainProcess().then().catch(err => { // Fallback error handling, though _runtime.error should ideally be caught within mainProcess // This outer catch might be useful for programming errors in mainProcess itself before try/catch return _runtime.error(err, "Unhandled rejection in mainProcess.", "The order could not be processed.", undefined, 500); }); ``` ### Example: Cancel Order and Related Entities (derived from `Function (1).json`) This example demonstrates updating an order and its related quantities. ```javascript let entityService = new GsbEntityService(_runtime); let orderToCancel = _instance.entity as _defs.GsbPrtOrder; // Assume _instance.entity is the order async function cancelFullOrder() { try { // 1. Update Order status let updatedOrder = new _defs.GsbPrtOrder({ id: orderToCancel.id, status: _enums.OrderStatus.Cancelled }); await entityService.saveEnt(updatedOrder); console.log('Order status set to Cancelled.'); // Or use GsbLogService // 2. Update related OrderQuantities status let quantityUpdateQuery = new EntityQueryParams(_defs.GsbPrtOrderQuantity); quantityUpdateQuery.filter("order_id", orderToCancel.id); // The 'entity' property on EntityQueryParams is used by updateQuery as the payload quantityUpdateQuery.entity = { status: _enums.OrderQuantityStatus.Cancelled } as _defs.GsbPrtOrderQuantity; let updateResult = await entityService.updateQuery(quantityUpdateQuery); console.log(`Updated ${updateResult.affectedRowCount} order quantities.`); // GsbQueryOpResponse might be actual type // Optionally, cancel related invoice if exists if (!Utils.isIdEmpty(orderToCancel.invoice_id)) { let updatedInvoice = new _defs.GsbPrtInvoice({ id: orderToCancel.invoice_id, status: _enums.InvoiceStatus.Cancelled }); await entityService.saveEnt(updatedInvoice); console.log('Related invoice cancelled.'); } return _runtime.end(200, "Order and related entities cancelled.", "cancelled", undefined, _enums.ProcessAction.CancelWorkflow); } catch (error) { console.error("Error cancelling order:", error); // Use GsbLogService for persistent logs return _runtime.error(error, "Order cancellation failed.", "The order could not be cancelled.", undefined, 500); } } cancelFullOrder().then(); ``` ## 7. Error Handling and Logging Robust error handling and logging are vital for maintainable serverless functions. - **`try...catch` Blocks:** Surround all potentially failing operations (especially I/O like service calls) with `try...catch` blocks. - **`_runtime.error()`:** Use this to terminate the function when an error prevents successful completion. Keep the exception, internal message, safe user message, process action, and HTTP status in their dedicated arguments, then return immediately. - **`GsbLogService`:** Use for detailed, persistent logging. - Log informational messages for key steps. - Log warnings for recoverable issues or unusual conditions. - Log errors with as much context as possible, including the operation name and the exception object. ```javascript async function someOperation() { let logService = new GsbLogService(_runtime); try { await logService.logInfo("Starting operation X", "someOperation"); // ... potentially failing code ... let result = await entityService.getById(_defs.MyEntity, "non-existent-id"); if (!result) { await logService.logWarning("Entity not found, but proceeding.", "someOperation", { id: "non-existent-id" }); } // ... more code ... await logService.logInfo("Operation X completed", "someOperation"); return _runtime.success("completed"); } catch (e) { await logService.logError("Critical failure in operation X", "someOperation", e); return _runtime.error(e, "Operation X failed.", "The operation could not be completed.", undefined, 500); } } ``` ## 8. Best Practices - **Explicit Dependencies:** Always list the services your function uses in its `references` array as objects with id properties (e.g., `[{"id": "99e4c845-3032-458f-996f-8db3302f4e38"}]` for `GsbEntityService`). - **Asynchronous Code:** Correctly use `async/await` for all Promises. Avoid blocking operations. - **Error Handling:** Implement comprehensive `try...catch` blocks and use `_runtime.error()` and `GsbLogService.logError()`. - **Type Safety:** Use `_defs` to cast entities and instantiate new ones (`let order = _instance.entity as _defs.GsbPrtOrder;`). Use `_enums` for status codes, types, etc. - **Modularity:** Keep functions focused on a single responsibility. For complex logic, consider breaking it into smaller helper functions within the script or multiple GSB functions orchestrated by a workflow. - **Readability:** Write clean, well-commented code. - **Idempotency:** If a function might be retried, design it to be idempotent (running it multiple times with the same input has the same effect as running it once). - **Input Validation:** Validate input parameters (`_instance.prms`, `_instance.entity`) at the beginning of your function. - **Service Usage:** - Instantiate services once at the top of your script if they are used multiple times. - Use `GsbEntityService.saveEnt()` for simple saves if you have a typed entity object. Use `GsbEntityService.save()` with `GsbSaveRequest` if you need more control or don't have a fully typed object. - Leverage `QueryParams` effectively for precise data retrieval and updates. - **Performance:** - Only select the data you need using `QueryParams.select()`. - Be mindful of N+1 query problems when fetching related data; use `QueryParams.include()` where appropriate. - **Security:** - Be cautious when constructing queries or commands from user input to prevent injection attacks (though GSB services and `QueryParams` generally mitigate SQL injection). - Do not log sensitive information. - **Configuration:** Avoid hardcoding IDs or configuration values. If possible, retrieve them from GSB settings or entity configurations. - **`Utils` Object:** Make use of the `GsbUtil` (via `Utils`) for common tasks like ID checking (`Utils.isIdEmpty`), ID generation (`Utils.newId()`), and comparisons (`Utils.idEquals`). By following this guide and utilizing the provided services and concepts, developers can effectively build powerful and reliable serverless functions within the GSB platform. ## 9. Creating GSB Serverless Functions ### Function Creation Process To create a new GSB serverless function, follow these steps: 1. **Define function structure**: Create a new JSON object with the following properties: - `id`: A unique identifier (UUID) for the function - `name`: A descriptive name for the function - `code`: JavaScript code for the function's execution - `references`: Array of objects with id properties for each service the function depends on - `operations` (optional): JSON string defining declarative operations 2. **Write function code**: The function code should follow this pattern: ```javascript // Initialize required services let entityService = new GsbEntityService(_runtime); let logService = new GsbLogService(_runtime); // Access the current entity if needed let entity = _instance.entity as _defs.YourEntityType; async function mainProcess() { try { // Your function logic here // End function execution with success return _runtime.success("completed", resultData); // OR with specific process action // return _runtime.end(200, "Completed.", "completed", resultData, _enums.ProcessAction.Continue); } catch (error) { await logService.logError("Error in function", "mainProcess", error); return _runtime.error(error, "Function failed.", "The operation could not be completed.", undefined, 500); } } // Execute the main function mainProcess().then(); ``` 3. **Add required service references**: Include all necessary services in the `references` array as objects with id properties: ```json "references": [ {"id": "99e4c845-3032-458f-996f-8db3302f4e38"}, // GsbEntityService {"id": "ca071e22-184e-443b-b7e5-05ad29a1dc13"}, // GsbLogService {"id": "085d21cb-349f-4a99-b5dc-679c8ee7947e"}, // GsbUtil {"id": "1c9bc14d-2151-4e14-b6c8-63c0421c1243"} // QueryParams ] ``` ### Using Declarative Operations Functions can use a combination of code and declarative operations: ```json { "operations": "[ { \"id\": \"GUID\", \"orderNumber\": 1, \"operationType\": 10, \"title\": \"Set Entity Status\", \"setEntityOptions\": { \"setProps\": [{ \"name\": \"status\", \"value\": 2 }] } }, { \"id\": \"GUID\", \"orderNumber\": 2, \"operationType\": 7, \"title\": \"Process Data\", \"scriptCode\": \"// JavaScript code here\" } ]" } ``` Common operation types: - `7`: Script execution - `8`: Transaction commit - `9`: Get entity - `10`: Set entity properties - `13`: Notification ## 10. Calling Functions ### Calling Functions from UI In the GSB UI, functions can be triggered through various means: 1. **Button Actions**: Buttons can be configured to call functions 2. **Workflow Tasks**: User tasks in workflows can call functions upon completion 3. **Event Handlers**: UI events (form submit, field change) can trigger functions The UI typically sends: - The function ID or name to execute - The current entity context - Optional parameters ### Calling Functions from Other Functions Functions can call other functions using the `GsbEntityService.runWfFunction()` method: ```javascript // Define the function request let functionRequest = { function: { // Either use ID id: "function-uuid-here", // OR use name (one of these is required) name: "Function Name Here" }, instance: { // The entity to pass to the function (optional) entity: myEntity, // Additional parameters to pass (optional) prms: { param1: "value1", param2: "value2" } } }; // Call the function let result = await entityService.runWfFunction(functionRequest); // The result contains the function's response let functionResponse = result.response; ``` ### Function Call Response When a function is called using `runWfFunction`, the response object contains: 1. A `response` field with whatever was set in the called function using: - `_instance.response = {...}` - `return _runtime.success("route-result", responseData)` - `return _runtime.end(statusCode, message, "route-result", responseData)` 2. Status information and execution results from the function Example: ```javascript // In the called function: _instance.response = { success: true, data: { id: "123", status: "completed" } }; return _runtime.success("completed", _instance.response); // In the calling function: let result = await entityService.runWfFunction(request); console.log(result.response.success); // true console.log(result.response.data.id); // "123" ``` ### Passing Data Between Functions You can pass data between functions in several ways: 1. **Entity Context**: Pass an entity as the context object 2. **Parameters**: Use the `prms` object to pass custom parameters 3. **Response Data**: Return data in the response that the calling function can access Example of a complete function call with response handling: ```javascript async function callAnotherFunction() { try { let entityToPass = await entityService.getById(_defs.GsbPrtOrder, "order-id"); let functionRequest = { function: { name: "Calculate Order Total" }, instance: { entity: entityToPass, prms: { applyDiscounts: true } } }; let result = await entityService.runWfFunction(functionRequest); if (result.response && result.response.success) { // Use the calculated total let calculatedTotal = result.response.totalPrice; // Continue processing... } else { throw new Error(result.response?.errorMessage || "Function execution failed"); } } catch (error) { return _runtime.error(error, "Function call failed.", "The operation could not be completed.", undefined, 500); } } ``` ### How to manage functions For convenience you can create the functions under [projectRoot]/.gsb/functions/[functionName].ts its not meant to be run locally, but to be used as a reference for the AI to build the function. It will give linter errors, just ignore them. **Example:** 1- Create a file called .gsb/functions/myFunction.ts ```typescript //does nothing hust returns a success message return _runtime.success("success", {ret : _instance.entity?.a + _instance.prms?.b}); ``` 2- Test the function remotely use the "testFunction" tool to test the function remotely. test operation does not save the function to the GSB Backend, it only runs and returns the result. test operation can be used with both code and operations. you can pass instance with prms and entity to the function. ```json { "function": { "name": "myFunction", "code": "// Returns the workflow result and response payload. return _runtime.success("success", {ret : _instance.entity?.a + _instance.prms?.b});" }, "instance": { "entity": { "a": 1 }, "prms": { "b": 2 } } ``` 3- Save te function to GSB Backend use the "save" tool to save the function to the GSB Backend ```json { "entDefName": "GsbWfFunction", "entity": { "name": "myFunction",//required "title": "My Function",//required "code": "// Returns the workflow result and response payload. return _runtime.success("success", {ret : _instance.entity?.a + _instance.prms?.b});" } } ``` 4- Use the function use the "runWfFunction" tool to run the function. From user interface you can use the "runFunction" method from gsb-entity-service. ```json { "function": { "name": "myFunction" }, "instance": { "entity": { "a": 1 }, "prms": { "b": 2 } } } ``` --- # Entity versioning Source: /guides/entity-versioning # Entity versioning and change history GSB has two independent history systems. Use them for different jobs. | System | Enabled by | Created when | Stored in | Restorable | |---|---|---|---|---| | Explicit snapshots | `GsbEntityDef.isVersioned` | A caller invokes `POST /api/entity/addVersion` | `GsbEntityVersion` | Yes | | Automatic change tracking | `GsbEntityDef.isTracked` | Every create or update | `GsbTrackVersion` | No; it is an audit log | A definition can enable either flag, both flags, or neither. A normal save does not create a `GsbEntityVersion` snapshot. For example, the live `GsbWfFunction` definition is versioned but not tracked, while `GsbAddress` is tracked but not versioned. ## When to create a snapshot Create an explicit snapshot before a risky or meaningful change: a function rewrite, schema migration, workflow redesign, release, or production correction. Routine edits do not need a snapshot. This is narrower and faster than a full tenant backup, but it protects only one entity. Keep tenant backups for tenant-wide releases, migrations, and disaster recovery. A safe function workflow is: ```bash gsb version add \ --definition GsbWfFunction \ --note "Before registration validation rewrite" gsb push .gsb//serverless/functions-wf/myFunction ``` ## Create an explicit snapshot The standard version widget calls: `POST /api/entity/addVersion` ```json { "entityId": "live-entity-id", "entDefId": "entity-definition-id", "minor": false, "versionInfo": { "note": "Before registration validation rewrite" } } ``` The entity definition must have `isVersioned: true`. `entityId` identifies the current entity; `entDefId` identifies its definition. Write a note that explains why the snapshot exists, not merely that a save occurred. The `minor` flag requests the version level. Always query the created `GsbEntityVersion` row and use its returned `version` as authoritative. On dev1 testing on 2026-08-26, `minor: true` still produced the next major value (`2.0.0` after an active `1.0.0`), so clients must not invent or assume a semantic version. ## List and inspect snapshots Query `GsbEntityVersion` with `entity_id` equal to the live entity ID and sort `editDate` descending. Useful fields are: - `id`: snapshot row ID; this is the restore identifier. - `version`, `level`, `note`: release metadata. - `fullEntity`: serialized complete entity snapshot used for preview and restore. - `editDate`, `editUser_id`: audit metadata. ```bash gsb version list gsb version list --json ``` MCP clients use `listEntityVersions({ entityId, limit })`. ## Restore a snapshot Restoration is destructive because it overwrites the current entity state. Pass the `GsbEntityVersion.id`, not the live entity ID: `POST /api/entity/restoreVersion` ```json { "entityId": "gsb-entity-version-row-id" } ``` ```bash gsb version restore ``` The CLI requires tenant-code confirmation unless `--yes` is supplied. After restore, fetch the live entity and verify the expected fields and `currentVersion`. If the current state may also be needed, create a snapshot before restoring. MCP exposes `recoverEntityVersion({ versionId })` as a destructive, approval-required tool. Recovery overwrites the current live entity with the selected snapshot. Mutation tools are disabled by default in the server registry; an operator must enable them under the deployment's authorization policy. ## Automatic field-change history When `GsbEntityDef.isTracked` is true, each create and update adds a `GsbTrackVersion` row automatically. No `addVersion` call is needed. Query rows by the live entity's `entity_id`. The `values` field is Base64-encoded JSON. Decoding it yields entries shaped like: ```json [ { "propName": "title", "prevValue": "Old title", "newValue": "New title" } ] ``` Creation rows include initial values and generated metadata; update rows contain changed fields. Track rows are protected system logs and cannot be deleted directly. They are for auditing and comparison, not rollback. ```bash gsb version changes gsb version changes --json ``` The CLI decodes `values` in JSON output. MCP clients use `listTrackedChanges({ entityId, limit })`; consumers should Base64-decode `values` before rendering it. ## MCP operations | Tool | Risk | Default state | Purpose | |---|---|---|---| | `listEntityVersions` | read | enabled | List bounded snapshot recovery metadata | | `listTrackedChanges` | read | enabled | List automatic field changes | | `addEntityVersion` | write | disabled | Validate the definition and create an explicit snapshot | | `recoverEntityVersion` | destructive | disabled | Overwrite the live entity from a snapshot row | MCP credentials and tenant identity come from the verified server session. They are not accepted from model arguments. ## Operational checklist 1. Confirm the definition has `isVersioned` before creating a snapshot or `isTracked` before expecting audit rows. 2. Create a snapshot before the risky edit, with a meaningful note. 3. Perform and validate the edit. 4. Re-query history; trust the stored version string rather than calculating one locally. 5. Before restore, preserve the current state if it may be needed. 6. Restore by snapshot row ID, then read the live entity and test its behavior. 7. Use a full tenant backup when a change spans many entities or requires disaster recovery. --- # Workflows Source: /guides/workflows # Workflows A workflow coordinates a business process that spans more than one request: an approval, an onboarding, a nightly reconciliation. It is modelled as data in the tenant, so it is queryable like anything else. ## The records | Definition | Role | |---|---| | `GsbWorkflow` | The design: name, title, activities, transitions, triggers | | `GsbActivity` | One node — a function call, a human task, a pause | | `GsbTransition` | One edge, with an optional condition | | `GsbWorkflowInstance` | One run, with `status`, `result`, and `lastUpdateDate` | | `GsbWorkflowLog` | Per-step audit rows for an instance | | `GsbWorkflowTrigger` | What starts a run: an entity operation or a cron expression | ## Routing and composition A workflow can mix standard, conditional, and named parallel transitions. Conditional routes continue only when the activity result matches the configured value. Parallel routes fork independent activities; an Await Parallel activity joins the named branches and resumes only after every inbound branch has completed. Use a Multi Inner Workflow activity to compose processes instead of duplicating them. It can start one child workflow for every element supplied through a runtime instance parameter, run those child instances sequentially or in parallel, and continue the parent after the selected child workflow runs complete. The child workflow and entity definition are configurable, while the runtime parameter controls which instances are created for a particular run. ## Human decisions User activities can assign a task to a role, position, the instance starter, or a manager approval step. The workflow remains durable while it waits. A person advances the task with an action such as approve, reject, reassign, or cancel, and the result selects the next conditional route. ## Functions inside activities Each activity has before, main, and after function pipelines. A pipeline can call reusable functions, run server-side script, read or write entities, call GSB or external APIs, commit pending changes, generate and attach a PDF, send email or SMS, and create in-app notifications. Operations run in order, so a flow can calculate data, render a document, deliver it, and continue routing without moving orchestration into the frontend. ## Start a run Use [startWorkflow](/api/startworkflow) whenever the process waits on a person, a timer, or an external event. It returns as soon as the run is accepted. ```typescript const started = await entityService.startWorkflow( { workflow_id: "workflow-uuid", data: { orderId: "order-123" } }, token, tenantCode, ); if (!started.success) { throw new Error(started.message); } const instanceId = started.instance?.id; ``` Use [runWorkflow](/api/runworkflow) only when the caller genuinely needs the outcome inline. It holds the connection open for the whole run. Both operations are registered as `external-side-effect`: disabled by default, and refused through the CLI or an MCP client without explicit approval. ```bash gsb call startWorkflow --yes --raw --input '{ "request": { "workflow": { "name": "Order Approval" }, "data": { "orderId": "order-123" } } }' ``` ## Advance a waiting task An activity that needs a decision leaves the instance waiting. Three calls supply that decision, and they differ in **who calls them** and **where they stop**. The assignee submits their own task with [submitWorkflowTask](/api/submitworkflowtask). The decision is a transition choice: `selection_id` is the id of the `GsbTransition` leaving the current activity, so a task screen reads the outgoing transitions and offers them as buttons rather than hardcoding "approve" and "reject". ```typescript await entityService.submitWorkflowTask( { task: { id: "task-123", selection_id: "transition-uuid", note: "Within policy.", }, }, token, tenantCode, ); ``` An integration or server function completes a task programmatically with [iterateTask](/api/iteratetask), which then carries on with the rest of the workflow. ```typescript await entityService.iterateTask( { taskId: "task-123", action: "approve", input: { comments: "Within policy." }, }, token, tenantCode, ); ``` An administrator debugging a design steps it with [iterateOnce](/api/iterateonce), which advances exactly one activity and halts. Every step must carry `instance.workflow_id`. ```typescript const step = await entityService.iterateOnce( { instance: { id: "instance-123", workflow_id: "workflow-uuid" } }, token, tenantCode, ); ``` For an administrator the response instance also reports `activity_id`, `result`, `currentTask`, `lastProcessor_id`, `locker_id` and `message` — where the run is, what it chose, and why it stopped. ## Monitor instances There is no dedicated instance operation. Workflow instances are ordinary entities, so read them with [query](/api/query). ```typescript const instances = new QueryParams("GsbWorkflowInstance") .filter("workflow_id", "workflow-uuid") .sortBy("lastUpdateDate", QuerySortType.Descending) .select(["id", "status", "result", "startDate", "lastUpdateDate"]) .skip(0) .take(25) .returnCount(); const page = await entityService.query(instances, token, tenantCode); ``` Filter on `entity_id` to find every run touching one record. Read `GsbWorkflowLog` by `instance_id` for the engine's step-by-step trace, including activity, function, operation, result, details, and processing time. Read `GsbWfInstanceHistory` for the human history: who acted, when they completed the task, their note and result, and any attachments. ## Operating rules - Starting the same workflow twice creates two instances. Guard against duplicate starts in the caller. - Keep the `data` payload small; it is stored with the run. - A single activity failing marks the instance failed. Read `result` and `responseStr` before retrying. - Never start or run a workflow from model-generated code without explicit user confirmation. Activities call serverless functions — see [serverless functions](/guides/serverless-functions) and [runWfFunction](/api/runwffunction). --- # Pikbase CLI Source: /tools/cli # Pikbase CLI The binary is `gsb`. It syncs serverless functions and libraries, manages tenant users, and exposes the same MCP tool catalog the MCP server does. Commands that touch a tenant read `.gsb//credentials.json`, written by `gsb init`. The tenant is chosen by `--tenant `, else `GSB_TENANT_CODE`, else the only folder under `.gsb/`. ## Setup ### `gsb init` Signs in to a tenant and stores a token in `.gsb//credentials.json`. | Flag | Description | |---|---| | `-t, --tenant ` | Tenant code, for example `dev1` | | `-e, --email ` | User email | | `--api-url ` | Override the API base URL | | `--no-remember` | Request a short-lived token instead of a remembered one | | `-f, --force` | Overwrite existing credentials without asking | ```bash gsb init --tenant dev1 --email you@example.com ``` The password is prompted for. Do not pass it on the command line — shell history is not a secret store. ### `gsb config` Prints the current configuration: tenant, API URL, and where the credentials came from. ```bash gsb config ``` ## Serverless functions and libraries ### `gsb pull [name]` Pulls resources from the backend into the working directory. Pulls everything matching the selected types when `name` is omitted. | Flag | Description | |---|---| | `-f, --function` | Pull functions | | `-l, --library` | Pull libraries | | `-d, --doc-template` | Pull document templates | | `-v, --verbose` | Verbose output | ```bash gsb pull --function gsb pull "Calculate Order Total" --function --verbose ``` ### `gsb push ` Pushes one TypeScript file, or a document template folder, to the backend. | Flag | Description | |---|---| | `-t, --type ` | `function`, `library`, or `auto` (default `auto`) | | `-d, --doc-template` | Push a document template from a folder | | `--dry-run` | Show what would be pushed without pushing | | `-v, --verbose` | Verbose output | ```bash gsb push src/functions/calculate-order-total.ts --dry-run gsb push src/functions/calculate-order-total.ts --type function ``` Run `--dry-run` first. A push overwrites the tenant-side record. ### `gsb list` Lists local resources on disk, not tenant records. | Flag | Description | |---|---| | `-f, --function` | List functions | | `-l, --library` | List libraries | | `-d, --doc-template` | List document templates | | `-v, --verbose` | Verbose output | ### `gsb test ` Executes a function against the tenant without saving it. | Flag | Description | |---|---| | `--entity ` | JSON string for the entity context | | `--params ` | JSON string for the parameters | | `-v, --verbose` | Verbose output | ```bash gsb test src/functions/calculate-order-total.ts \ --entity '{"id":"order-123"}' \ --params '{"applyDiscounts":true}' ``` ### `gsb find [searchTerm]` Searches the tenant for existing serverless functions and libraries — `GsbWfFunction` and `GsbWfCodeLibrary` records. It does not search documentation. | Flag | Description | |---|---| | `-n, --count ` | Max results per type (default `25`) | ```bash gsb find order gsb find --count 50 ``` Run this before `push` so you do not create a second function under a name that already exists. ## Tools ### `gsb tools` Lists the MCP tools callable through `gsb call`, each with its risk classification and whether it needs `--yes`. | Flag | Description | |---|---| | `-r, --read-only` | Only list tools classified `read` | ```bash gsb tools --read-only ``` ### `gsb call ` Calls one MCP tool against the configured tenant. Same implementations as the MCP server; only the transport differs. | Flag | Description | |---|---| | `-i, --input ` | Tool input as a JSON string (default `{}`) | | `--input-file ` | Read tool input from a JSON file | | `-y, --yes` | Approve a write, destructive, or side-effecting tool | | `--raw` | Print raw JSON without the summary header | ```bash gsb call query --raw --input '{ "queryParams": { "entDefName": "Order", "startIndex": 0, "count": 10 } }' gsb call save --yes --input-file ./payload.json ``` Read tools run without `--yes`. Everything else refuses without it — do not add `--yes` to a script by default. ### `gsb mcp [args...]` Runs the MCP server. Unknown options are forwarded. See [MCP server](/tools/mcp). ```bash gsb mcp -y ``` ## Users and roles ### `gsb users add ` Creates a user and optionally assigns a role. | Flag | Description | |---|---| | `-r, --role ` | Role name or ID | | `-p, --password ` | Password; generated securely when omitted | | `-n, --name ` | Given name | | `-s, --surname ` | Surname | | `--write-env ` | Write `GSB_TEST_*` credentials to one or more ignored env files | | `--yes` | Skip confirmation | ```bash gsb users add qa@example.com --role Tester --write-env .env.test ``` Omit `--password` and let the CLI generate one. Every `--write-env` target must be git-ignored. ### `gsb users list` Lists users in the configured tenant. | Flag | Description | |---|---| | `-l, --limit ` | Maximum users to return (default `20`) | ### `gsb users roles` Lists the roles available in the configured tenant. Takes no flags. ### `gsb users assign-role ` Assigns an existing role to an existing user. | Flag | Description | |---|---| | `--yes` | Skip confirmation | ### `gsb users reset-password ` Resets a user's password. | Flag | Description | |---|---| | `-p, --password ` | Password; generated securely when omitted | | `--write-env ` | Write `GSB_TEST_*` credentials to one or more ignored env files | | `--yes` | Skip confirmation | ## Assistant ### `gsb assistant seed [manifest]` Creates or updates `LlmConfiguration` rows from a checked-in manifest. Defaults to `.gsb//assistant/manifest.json`. | Flag | Description | |---|---| | `--deployment ` | Override the LLM deployment name for this tenant | ```bash gsb assistant seed --deployment your-deployment ``` ## Documentation from the terminal Documentation tools are local, read-only calls that need no tenant. ```bash gsb call getDocs --input '{"methodName":"query"}' --raw gsb call getApiDocs --raw gsb call getSchemaDocs --raw gsb call getServerlessFunctionDocs --raw ``` ## Safety Inspect a tool with `gsb tools` before invoking it. Write and destructive tools require `--yes`; do not bypass that in scripts or agent workflows. Keep tokens out of command history and out of committed files. --- # MCP server Source: /tools/mcp # MCP server The Pikbase MCP server lets compatible AI clients discover and invoke the same governed tool contracts available through the CLI. The registry, risk classification, and approval rules are shared — only the transport differs. ## Start the server ```bash gsb mcp -y ``` Configure your MCP client to launch that command in an environment where the CLI has been initialized for the intended tenant. Keep credentials out of committed client configuration. ## Risk classification Every tool carries a risk level that decides whether it is enabled by default. | Risk | Enabled by default | Approval | |---|---|---| | `read` | Yes | Never required | | `write` | No | Required | | `destructive` | No | Required | | `external-side-effect` | No | Required | Read tools are the only ones an agent can invoke unattended. ## Read tools | Tool | Input | Returns | |---|---|---| | `getById` | `{ definitionType, id }` | One entity | | `getCopy` | `{ request }` | A detached copy of an entity | | `query` | `{ queryParams }` | A page of entities | | `queryMapped` | `{ queryParams }` | A page of mapped entities | | `getEntityDef` | `{ entityDef }` | One entity definition | | `queryEntityDefs` | `{ searchTerm, page, pageSize, includeSystem }` | `{ entityDefs, totalCount }` | | `getCommonPropertyDefs` | `{}` | Available property data types | ## Bounded inspection tools Inspection tools page their results and cite the artifact they read, so an agent cannot pull an unbounded payload into context. | Tool | Input | Returns | |---|---|---| | `inspectSchema` | `{ searchTerm, page, pageSize, includeSystem }` | Paged definitions with a cited artifact | | `inspectQuery` | `{ queryParams, page, pageSize }` | A bounded page of entities | | `inspectLogs` | `{ queryParams, page, pageSize }` | A bounded page of log rows | | `inspectDocs` | `{ methodName }` | Operation documentation | | `inspectSchemaDocs` | `{}` | Schema management documentation | `page` defaults to `1` and `pageSize` to `25`. ## Documentation tools | Tool | Input | |---|---| | `getDocs` | `{ "methodName": "query" }` | | `getApiDocs` | `{}` | | `getSchemaDocs` | `{}` | | `getServerlessFunctionDocs` | `{}` | | `getSchemaManagerDocs` | `{}` | ## Write tools Disabled by default; each needs explicit approval. | Tool | Input | |---|---| | `save` | `{ request }` | | `saveMulti` | `{ request }` | | `saveMappedItems` | `{ request }` | | `removeMappedItems` | `{ request }` | | `createEntityDef` | `{ entityDef }` | | `updateEntityDef` | `{ entityDef }` | | `createOrUpdateSchema` | `{ entityDefs }` | | `addProperty` | `{ property, entityDef }` | | `updateProperty` | `{ property, entityDef }` | | `removeProperty` | `{ property, entityDef }` | ## Destructive tools | Tool | Input | |---|---| | `delete` | `{ request, confirm, confirmationToken }` | | `deleteQuery` | `{ queryParams, confirm, confirmationToken }` | Both use a two-step handshake. The first call returns a `confirmationToken` bound to the exact payload and tenant; the second repeats the identical payload with `confirm: true`. Changing any field invalidates the token, and each token is consumed on use. ## External side-effect tools | Tool | Input | |---|---| | `runWfFunction` | `{ request }` | | `testWfFunction` | `{ request }` | | `runWorkflow` | `{ request }` | | `startWorkflow` | `{ request }` | | `iterateTask` | `{ request }` | These reach systems outside the tenant database. Treat them as irreversible. ## Agent safety Treat model output as untrusted input. Keep tools permission-scoped, meter costly operations per tenant, and require explicit human confirmation before any generated mutation that can remove or overwrite data. --- # getDocs() — MCP documentation tool Source: /tools/getdocs # GetDocs Operation ## General Description The `getDocs` operation retrieves the API documentation for the GSB Entity Service. ## Detailed Description This operation provides comprehensive documentation for all available operations in the GSB Entity Service API. It returns a structured object containing general information about the API and detailed documentation for each operation. This is useful for developers who need to understand the capabilities and usage of the API without having to refer to external documentation sources. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ## Response ### Success Response ```json { "success": true, "data": { "api": { "name": "GSB Entity Service API", "version": "string", "description": "API for managing entity data and definitions" }, "operations": { "getById": "Markdown documentation for getById operation", "getCopy": "Markdown documentation for getCopy operation", "query": "Markdown documentation for query operation", // ... Documentation for all other operations } } } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Get Complete API Documentation ```typescript const result = await getDocs({ token: "your-auth-token" }); if (result.success) { const apiInfo = result.data.api; console.log(`API Name: ${apiInfo.name}`); console.log(`Version: ${apiInfo.version}`); console.log(`Description: ${apiInfo.description}`); // Get documentation for a specific operation const getByIdDocs = result.data.operations.getById; console.log("Documentation for getById operation:"); console.log(getByIdDocs); // List all available operations console.log("Available operations:"); Object.keys(result.data.operations).forEach(op => { console.log(`- ${op}`); }); } else { console.error("Error:", result.error); } ``` ### Generate HTML Documentation ```typescript import * as marked from 'marked'; async function generateHtmlDocs(token) { const result = await getDocs({ token }); if (!result.success) { console.error("Error fetching documentation:", result.error); return null; } const apiInfo = result.data.api; const operations = result.data.operations; let html = ` ${apiInfo.name} Documentation

${apiInfo.name}

Version: ${apiInfo.version}

${apiInfo.description}

Operations

`; // Add each operation's documentation Object.entries(operations).forEach(([name, docs]) => { html += `

${name}

${marked.parse(docs as string)}
`; }); html += ` `; return html; } // Usage const htmlDocs = await generateHtmlDocs("your-auth-token"); // Save htmlDocs to a file or serve it via a web server ``` ## Additional Information - The getDocs operation is primarily intended for developers who need to understand the API's capabilities. - The documentation returned is in Markdown format, which can be easily rendered into HTML or other formats. - The documentation includes detailed information about each operation's parameters, response formats, and example usage. - This operation can be useful for generating dynamic documentation for client applications or developer portals. - The documentation is versioned along with the API, ensuring that it always reflects the current capabilities. - For the most up-to-date and comprehensive documentation, it's recommended to use this operation rather than relying on potentially outdated external documentation. --- # getApiDocs() — MCP documentation tool Source: /tools/getapidocs # GetApiDocs Operation ## General Description The `getApiDocs` operation provides general information about the GSB Entity Service API. ## Detailed Description This operation returns basic metadata about the API, including its name, version, and description. Unlike the `getDocs` operation, it does not include detailed documentation for individual operations. This is useful when you need a quick overview of the API without the detailed operation documentation. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ## Response ### Success Response ```json { "success": true, "data": { "name": "GSB Entity Service API", "version": "string", "description": "API for managing entity data and definitions" } } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Get API Information ```typescript const result = await getApiDocs({ token: "your-auth-token" }); if (result.success) { const apiInfo = result.data; console.log(`API Name: ${apiInfo.name}`); console.log(`Version: ${apiInfo.version}`); console.log(`Description: ${apiInfo.description}`); } else { console.error("Error:", result.error); } ``` ### Check API Version ```typescript async function checkApiVersion(requiredVersion, token) { const result = await getApiDocs({ token }); if (!result.success) { console.error("Error checking API version:", result.error); return false; } const currentVersion = result.data.version; // Compare versions (this is a simple string comparison) // For more complex version comparisons, consider using a version comparison library if (currentVersion === requiredVersion) { console.log(`API version ${currentVersion} matches required version ${requiredVersion}`); return true; } else { console.warn(`API version mismatch: current ${currentVersion}, required ${requiredVersion}`); return false; } } // Usage const isCompatible = await checkApiVersion("1.0.0", "your-auth-token"); if (!isCompatible) { console.warn("This client may not be fully compatible with the current API version"); } ``` ### Display API Information in UI ```typescript import React, { useEffect, useState } from 'react'; function ApiInfoComponent({ token }) { const [apiInfo, setApiInfo] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function fetchApiInfo() { try { const result = await getApiDocs({ token }); if (result.success) { setApiInfo(result.data); } else { setError(result.error); } } catch (err) { setError(err.message); } finally { setLoading(false); } } fetchApiInfo(); }, [token]); if (loading) return
Loading API information...
; if (error) return
Error: {error}
; return (

{apiInfo.name}

Version: {apiInfo.version}

{apiInfo.description}

); } // Usage ``` ## Additional Information - The getApiDocs operation provides only general information about the API, not detailed documentation for individual operations. - For detailed documentation on all operations, use the getDocs operation instead. - This operation is lightweight and can be used for quick API version checks or displaying basic API information in a user interface. - The API version information can be useful for client applications to ensure compatibility with the API. - This operation requires minimal permissions and can typically be called with any valid authentication token. --- # API reference Source: /api # GSB Entity Service API ## Introduction The GSB Entity Service API provides a comprehensive set of operations for managing entity data and definitions within the GSB platform. It allows you to create, read, update, and delete entity data, as well as define and modify entity schemas. ## Key Concepts ### Entities Entities represent business objects in your application domain. Each entity has: - A unique identifier - A set of properties (fields) - Optional relationships with other entities - Metadata describing its structure and behavior ### Entity Definitions Entity definitions (schemas) define the structure of entities: - Property definitions (name, type, constraints) - Relationships with other entity types - Indexes for optimizing queries - Validation rules - Display and UI metadata ### Properties Properties are the individual fields that make up an entity: - Simple types (string, number, boolean, date) - Complex types (objects, arrays) - References to other entities - Computed properties ## Authentication The API uses token-based authentication. Most operations require a valid authentication token, which should be included in the request parameters. The token determines the permissions and access level for the operations. ## Common Parameters Most operations accept these common parameters: | Parameter | Type | Description | |-----------|------|-------------| | token | string | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ## Response Format All API operations return responses in a consistent format: ### Success Response ```json { "success": true, "data": { // Operation-specific response data } } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Operation Categories ### Entity Data Operations Operations for working with entity data: - `getById`: Retrieve an entity by its ID - `getCopy`: Get a copy of an entity with a new ID - `query`: Query entities based on filters - `queryMapped`: Query entities with mapped results - `save`: Create or update an entity - `saveMulti`: Create or update multiple entities - `delete`: Delete an entity by ID - `deleteQuery`: Delete entities matching a query ### Entity Relationship Operations Operations for managing relationships between entities: - `saveMappedItems`: Save relationships between entities - `removeMappedItems`: Remove relationships between entities ### Entity Definition Operations Operations for managing entity definitions (schemas): - `getEntityDef`: Get an entity definition by ID - `getDefinition`: Get an entity definition by name - `queryEntityDefs`: Query entity definitions - `createEntityDef`: Create a new entity definition - `updateEntityDef`: Update an existing entity definition - `getCommonPropertyDefs`: Get common property definitions ### Property Operations Operations for managing entity properties: - `addProperty`: Add a property to an entity definition - `updateProperty`: Update a property in an entity definition - `removeProperty`: Remove a property from an entity definition ### Workflow Operations Operations for executing workflows: - `runWorkflow`: Execute a workflow - `startWorkflow`: Start a workflow - `runWfFunction`: Run a workflow function - `iterateTask`: Iterate through a workflow task ### Documentation Operations Operations for retrieving API documentation: - `getDocs`: Get comprehensive API documentation - `getApiDocs`: Get general API information ## Best Practices 1. **Error Handling**: Always check the `success` field in responses and handle errors appropriately. 2. **Pagination**: When querying large datasets, use pagination parameters to limit the result size. 3. **Validation**: Validate entity data against entity definitions before saving to avoid errors. 4. **Transactions**: For operations that modify multiple entities, consider using batch operations like `saveMulti` to ensure atomicity. 5. **Security**: Always use the principle of least privilege when assigning permissions to tokens. 6. **Performance**: Use appropriate indexes in entity definitions to optimize query performance. 7. **Caching**: Consider caching frequently accessed entity definitions and reference data. ## Getting Started To get started with the GSB Entity Service API: 1. Obtain an authentication token with appropriate permissions. 2. Explore the available entity definitions using `queryEntityDefs`. 3. Retrieve detailed documentation for specific operations using `getDocs`. 4. Start with simple operations like `getById` and `query` to retrieve data. 5. Progress to more complex operations as you become familiar with the API. ## Additional Resources - For detailed documentation on each operation, use the `getDocs` operation. - For information about the API version and general metadata, use the `getApiDocs` operation. --- # getById() — Entity Service Source: /api/getbyid # GetById Operation ## General Description The `getById` operation retrieves a single entity by its unique identifier. ## Detailed Description This operation allows you to fetch a specific entity record from the database using its unique ID. It returns the complete entity with all its properties as defined in the entity definition. This is the most direct way to retrieve a specific entity when you know its ID. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | definitionType | string | Yes* | Name or id of the entity definition. | | id | string | Yes | ID of the entity to retrieve. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ## Response ### Success Response ```json { "success": true, "entity": { "id": "string", "title": "string", "createDate": "string", "lastUpdateDate": "string", // All other properties of the entity "property1": "value1", "property2": "value2" } } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Get Customer by ID ```typescript const result = await getById({ entDefName: "Customer", id: "customer-id-123", token: "your-auth-token" }); if (result.success) { const customer = result.entity; console.log("Customer:", customer.title); console.log("Email:", customer.email); console.log("Created:", customer.createDate); } else { console.error("Error:", result.error); } ``` ### Get Entity Using Entity Definition ID ```typescript const result = await getById({ entDefId: "customer-def-456", id: "customer-id-123", token: "your-auth-token" }); if (result.success) { const customer = result.entity; // Process customer data } else if (result.error === "Entity not found") { console.log("Customer does not exist"); } else { console.error("Error:", result.error); } ``` ### Get Entity with Optional Parameters ```typescript const result = await getById({ entityDef: { name: "Product" }, id: "product-id-789", token: "your-auth-token", tenantCode: "tenant1" }); if (result.success) { const product = result.entity; console.log("Product:", product.title); console.log("Price:", product.price); console.log("In Stock:", product.inStock ? "Yes" : "No"); } else { console.error("Error:", result.error); } ``` ## Additional Information - The getById operation is used to retrieve a single entity by its unique identifier. - The operation returns all properties of the entity as defined in the entity definition. - If the entity does not exist, the operation will return an error with message "Entity not found". - For retrieving multiple entities or filtering by criteria, use the query operation instead. - For retrieving an entity with its related entities based on cascade references, use the getCopy operation. - Access permissions are enforced based on the provided token. - The operation does not follow references to other entities; it only returns the requested entity. - If you need to retrieve related entities, you'll need to make separate getById calls or use a query with includes. --- # getCopy() — Entity Service Source: /api/getcopy # GetCopy Operation ## General Description The `getCopy` operation retrieves a copy of an entity with its related entities based on cascade reference rules. ## Detailed Description This operation creates a deep copy of an entity, including all its properties and related entities based on cascade reference rules defined in the entity properties. Unlike the standard getById operation, getCopy follows reference relationships and includes related entities in a multilevel JSON structure. This is particularly useful when you need to copy an entity along with its related entities or when you need to retrieve a complete object graph for display or processing. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | request | object | Yes | Object containing the entity identifier. It should have `definitionType` (string, name of the entity definition) and `id` (string, ID of the entity to copy). | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ## Response ### Success Response ```json { "success": true, "entity": { "id": "string", "title": "string", "createDate": "string", "lastUpdateDate": "string", // All other properties of the entity // Related entities as nested objects based on cascade references "relatedEntity1": { "id": "string", "title": "string", // Properties of the related entity // Potentially more nested related entities }, "relatedEntities": [ { "id": "string", "title": "string", // Properties of each related entity in the collection } ] } } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Get a Copy of an Entity with Related Entities ```typescript const result = await getCopy({ request: { definitionType: "Order", id: "order-id-123" }, token: "your-auth-token" }); if (result.success) { const order = result.entity; console.log("Order:", order.title); // Access related customer entity (assuming a cascade reference) if (order.customer) { console.log("Customer:", order.customer.name); } // Access related order items (assuming a cascade reference) if (order.orderItems && Array.isArray(order.orderItems)) { console.log("Order Items:", order.orderItems.length); order.orderItems.forEach(item => { console.log(`- ${item.title}: ${item.quantity} x ${item.unitPrice}`); // Access product information if cascaded if (item.product) { console.log(` Product: ${item.product.title}`); } }); } } else { console.error("Error:", result.error); } ``` ### Copy an Entity for Duplication ```typescript // First, get a copy of the entity with related entities const copyResult = await getCopy({ request: { definitionType: "Product", id: "product-id-456" }, token: "your-auth-token" }); if (copyResult.success) { const productCopy = copyResult.entity; // Modify the copy as needed delete productCopy.id; // Remove ID to create a new entity productCopy.title = `Copy of ${productCopy.title}`; // Save the modified copy as a new entity const saveResult = await save({ entDefName: "Product", entity: productCopy, token: "your-auth-token" }); if (saveResult.success) { console.log("Created copy with ID:", saveResult.id); } } ``` ## Additional Information - The getCopy operation follows cascade reference rules defined in the entity properties. - Cascade references are determined by the property's refType and additional cascade settings. - All the data is returned with newly generated ids or no id set, except for the references where copyAction is set to reference. - The title field is return as title + " Copy" - You can use the save operation to save the copy. - copyAction filed in property definition determines what happens with the related entities.(include, exclude, reference) - The operation returns a multilevel JSON object with nested related entities, not just a flat entity. - This operation is particularly useful for: - Creating duplicates of complex entities with related data - Retrieving a complete object graph for display or processing - Implementing copy functionality in user interfaces - Unlike the standard getById operation, getCopy includes related entities directly in the response. - The depth of cascading is determined by the entity definition's reference properties. - For references marked as cascade, the related entities are included in the result. - For references not marked as cascade, only the reference ID is included. - Access permissions are enforced for both the main entity and all related entities. - Performance may be impacted for entities with many cascade references or deep object graphs. --- # query() — Entity Service Source: /api/query # Query Operation ## General Description The `query` operation fetches entities based on specified query parameters, allowing for complex filtering, sorting, pagination, and analytical queries. ## Detailed Description This operation provides a powerful and flexible way to search and retrieve entities based on various criteria. It supports filtering by property values, sorting results, paginating through large result sets, and including related entities. The query system is designed to handle complex queries while maintaining performance. Additionally, it supports analytical queries with groupBy, aggregates, and modifiers for data analysis. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | queryParams | object | Yes | The query parameters object that defines the search criteria and result options. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ### QueryParams Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | entDefName | string | Yes* | Name of the entity definition to query. Required if entDefId is not provided. Getter/setter for entityDef.name. | | entDefId | string | Yes* | ID of the entity definition to query. Required if entDefName is not provided. Getter/setter for entityDef.id. | | entityDef | object | No | Entity definition object with id and/or name properties. | | entity | object | No | Optional entity instance for the query. | | id | string | No | ID of this query. | | entityId | string | No | Property for entity ID. | | selectCols | array | No | Array of SelectCol objects defining columns to select in the query. If not provided, all columns are selected. | | includes | array | No | Array of IncludeQuery objects for related entities to include in the results. | | filters | array | No | Array of Filter objects to filter the results. | | startIndex | number | No | Pagination start index (0-based). | | count | number | No | Number of records to return. | | sortCols | array | No | Array of SortCol objects for sorting specifications. | | calcTotalCount | boolean | No | Whether to calculate the total count of matching records. | | searchText | string | No | Search term for automatic searching across all searchable fields. Alias of the scalar `filter`. | | filter | string | No | Scalar shorthand for the cross-field auto-search over `isSearchable=true` fields. Same as `searchText`. For per-field predicates use `filters[]` instead. | | queryType | number | No | Type of query using QueryType enum (Single=0, List=1, Search=2, AutoComplete=3, Full=4, FullWithSingleRefs=5, FullNonPersonal=6). | | disableTransaction | boolean | No | Whether to disable database transaction for this query. | | propertyName | string | No | Name of the property being queried when using relationship queries. | | propName | string | No | Alias for propertyName. | | mapColName | string | No | Alias for propertyName used in mapping. | | refColName | string | No | Getter/setter for mapColName. | ### SelectCol Object Structure `SelectCol` is the universal column descriptor. It appears as `selectCols[]` entries, as `sortCols[].col`, and as **both sides** of a filter (`col` and `val`). The same object therefore expresses "which database field" and "which value" depending on which fields are set. The four resolution modes, and the symmetry between them: | Mode | Field | Resolves to | |------|-------|-------------| | Field path | `name` | a database field, dotted across relations: `order.partner.orgUnit.manager` | | Literal | `value` | a hardcoded value | | Field script | `nameScript` | an expression that evaluates to a database address/expression | | Value script | `valScript` | an expression that evaluates to values | | Subquery | `valQuery` | a nested query whose result set supplies the values | **Either side may use any mode.** A filter's `val` can carry a `name`, which makes it a column-to-column comparison rather than a comparison against a literal. | Property | Type | Description | |----------|------|-------------| | name | string | Field path to select or compare. Dot notation traverses relations. | | value | any | Static value. | | nameScript | string | Expression evaluating to a database address/expression. See _CALC token rules below. | | valScript | string | Expression evaluating to values. | | valQuery | QueryDto | Nested query supplying the value set. See Subqueries below. | | aggregateFunction | number | AggregateFunction enum (None=0, Sum=1, Average=2, Count=3, Maximum=4, Minimum=5, Variance=6). | | dateModifier | number | DateModifier enum (None=0, Year=1, Quarter=2, Month=3, DayOfYear=4, DayOfMonth=5, Week=6, Weekday=7, Hour=8, Minute=9, Second=10, Millisecond=11). | | groupBy | boolean | Group by this column in an analytical query. | | distinct | boolean | Deduplicate on this column. Also accepted at the root of queryParams. | | dataType | number | DataType hint for the column. | | hideFromResults | boolean | Compute/join the column but omit it from the response. | | dynamicDefinition | GsbEntityDef | Entity definition supplied inline instead of by name. | | skipProcess | boolean | Skip post-processing for this column. | | searchHighlight | string | Search text to highlight within the returned column value. | | selectAsTitle | string | Alias for the column in the result. | | script | string | JavaScript expression to calculate the column value. | | fullName | string | Full name including table prefixes. | | title | string | Display title for UI purposes. | ### Subqueries (verified live against dev1, 2026-08-20) A subquery is a `QueryDto` on `valQuery`. The value set it returns becomes the right-hand side of the filter. ```json { "entDefName": "GsbUser", "filters": [{ "col": { "name": "roles.id" }, "val": { "valQuery": { "entDefName": "GsbRole", "selectCols": [{ "name": "id" }], "filters": [{ "col": { "name": "title" }, "val": { "value": "Admin" }, "function": 0 }] } }, "function": 27 }], "distinct": true } ``` - **`MatchArrays` (27)** treats the subquery result as a **set** — the row matches if the column is a member. This is the general-purpose form. - **`Equals` (0)** treats it as a **scalar** subquery. If the subquery returns more than one row the server fails with SQLSTATE `21000` ("more than one row returned by a subquery used as an expression"). - The join behind a subquery over a multi-reference is **not deduplicated**. Set `distinct: true` or the same parent row repeats once per match. - `calcTotalCount` reports the **pre-deduplication** row count even when `distinct` is set. Do not page on `totalCount` for a deduplicated subquery result. #### Aggregate subqueries A subquery whose `selectCols` carry an `aggregateFunction` resolves to a **scalar**, which is how you compare a row against a value computed from the whole set: ```json { "col": { "name": "size" }, "val": { "valQuery": { "entDefName": "GsbFile", "selectCols": [{ "name": "size", "aggregateFunction": 2 }] }}, "function": 2 } ``` Verified against 349 `GsbFile` rows: `size > AVG(size)` returned 35, `size < AVG(size)` returned 221, and `size = MAX(size)` returned exactly the one largest row. Use this for "above average", "at the maximum", and any threshold derived from the data. #### Correlated subqueries in tenant runtime Tenant serverless queries can correlate an inner query with the row currently being evaluated by using `__PARENT.`. The parent token belongs in the inner filter's `val.name`; `val.name` on the outer filter identifies the related collection that supplies the subquery rows. This predicate keeps a cluster when its capacity is greater than the count of tenants assigned to that same cluster: ```typescript const hasCapacity: SingleQuery = { col: { name: "capacity" }, val: { name: "tenants", valQuery: { selectCols: [{ name: "id", aggregateFunction: "Count" }], queries: [{ col: { name: "cluster_id" }, val: { name: "__PARENT.id" }, function: "Equals", }], }, }, function: "Greater", }; ``` `__PARENT` refers to the immediately enclosing query row. It is a trusted query expression for tenant-runtime code, not a literal value to accept from browser input. Correlated aggregate subqueries can be expensive, so constrain the outer query and select only the fields the caller needs. ### Column-to-column comparison Because `val` is a `SelectCol`, giving it a `name` compares two fields: ```json { "col": { "name": "createDate" }, "val": { "name": "lastUpdateDate" }, "function": 4 } ``` Verified: on 349 `GsbFile` rows, `Equals` matched 321 and `NotEqual` matched 28 — a clean partition of the set. ### Calculated columns and the _CALC token convention `nameScript` uses the same allowlisted-token convention as `_CALC` in save operations: **function and keyword tokens are written as bracketed underscore forms**, and `[propertyName]` references a field. | Script | Result | |--------|--------| | `[size]/1024` | works — arithmetic on a field | | `[_COALESCE]([size],0)` | works — returns the scalar | | `[_CASE] [_WHEN] [size] > 1000 [_THEN] 1 [_ELSE] 0 [_END]` | works — bucketing/tiering | | `COALESCE([size],0)` | **wrong** — an unbracketed name is not a function call; parsed as a row constructor returning `{f1,f2}` | | `CASE WHEN ... END` | **fails** with a SQL syntax error | Unbracketed SQL keywords are not executed, so `nameScript` is a restricted token allowlist rather than raw SQL. Still never interpolate untrusted text into it. ### Filtered includes (verified live against dev1, 2026-08-20) An `IncludeQuery` extends `QueryParams`, so it accepts its own `filters`. This is the way to narrow a related collection to the rows you actually want, instead of hydrating all of them: ```json { "entDefName": "GsbPrtOrder", "selectCols": [{ "name": "id" }], "includes": [{ "name": "invoices", "selectCols": [{ "name": "id" }, { "name": "total" }], "filters": [{ "col": { "name": "issuer.orgUnit_id" }, "val": { "value": "" }, "function": 0 }] }] } ``` The filter is applied **per parent**: verified on `GsbUser.roles` filtered by `title Like '%Admin%'`, three parents returned 0, 0, and 1 related rows while the unfiltered include returned 2, 7, and 2. Use a filtered include wherever you would reach for "the first related row" — "the open invoice", "the primary address", "the active contract". If the filter identifies exactly one row, you get exactly one row per parent. **Include `count` is not a limit.** `count`, `take`, `startIndex`+`count`, `queryType`, and `count`+`sortCols` were all ignored on both a metadata relation and a real many-to-many. Narrow with `filters`, and select only the columns you need — a wide include on a large relation is an unbounded payload. ### Nested filter logic (verified live against dev1, 2026-08-20) Filters compose into an unlimited AND/OR tree. Two rules cover the whole system. **Rule 1 — `relation` describes how a filter joins to the filter *before* it.** It is carried by the right-hand operand and defaults to `"and"`. Putting it on the left operand does nothing. ```json [ A, { ...B, "relation": "or" } ] // A OR B [ { ...A, "relation": "or" }, B ] // A AND B — the "or" is ignored ``` **Rule 2 — `children` is a parenthesis.** A filter carrying `children` is a group. Its own `relation` says how *the group* joins to its preceding sibling — **not** how its children combine. Children follow Rule 1 among themselves. ```json // (listingType = 2 AND size > 1000) OR listingType = 1 "filters": [ { "children": [ { "col": { "name": "listingType" }, "val": { "value": 2 }, "function": 0 }, { "col": { "name": "size" }, "val": { "value": 1000 }, "function": 2 } ]}, { "col": { "name": "listingType" }, "val": { "value": 1 }, "function": 0, "relation": "or" } ] ``` Verified on 349 `GsbFile` rows — folders (A) = 62, files (B) = 284, files over 1000 bytes = 92: | Expression | Shape | Total | |---|---|---| | A OR B | `[A, or(B)]` | 346 = 62 + 284 | | A AND B | `[A, B]` | 0 | | (B AND big) OR A | `[{children:[B,big]}, or(A)]` | 154 = 92 + 62 | | (A OR B) AND big | `[{children:[A,or(B)]}, big]` | 92 | | ((B AND big) OR A) AND name IS NOT NULL | three levels | 154 | Nesting depth is unbounded — a group may contain groups. **`negate` applies to a single filter, not to a group.** On a leaf it inverted 62 rows to 287 as expected; on a `children` wrapper it was ignored (346 instead of 3). To negate a group, invert the operators inside it. **Most common mistake:** putting `relation: "or"` on the wrapper and expecting the children to OR together. They stay ANDed, which usually returns zero rows. ### SortCol Object Structure | Property | Type | Description | |----------|------|-------------| | col | SelectCol | The column to sort by. | | sortType | string | Sort direction: "asc" for ascending, "desc" for descending. | ### Filter Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | col | SelectCol | Yes | The column to filter on. | | val | SelectCol | Yes | The value to compare against as a SelectCol object. | | function | number | string | No | The comparison function. Accepts the QueryFunction enum number OR its name as a case-insensitive string (e.g. `12`, `"Contains"`, or `"contains"`). | | relation | string | No | How this filter joins to the **preceding** filter: "and" (default) or "or". See Nested filter logic. | | children | array | No | Nested filter conditions. A filter carrying `children` acts as a parenthesised group. | | negate | boolean | No | Invert this condition (default: false). Applies to a single filter only, not to a `children` group. | | extra | object | No | Extra operands for functions that need more than one value, e.g. `{ value1, value2 }` for Between (19). | | name | string | No | Optional name for the filter. | | relationLevel | number | No | Level of relation nesting. | ### IncludeQuery Object Structure IncludeQuery extends QueryParams and represents related entities to include: | Property | Type | Description | |----------|------|-------------| | name | string | Name of the relationship property to include. | | propertyName | string | Alias for name. | | (all QueryParams properties) | various | All properties from QueryParams are available for nested queries. | ## Fluent TypeScript API The TypeScript client uses `QueryParams` as the canonical fluent builder. Its methods serialize to the object structure above; method names are not sent over the wire. ```typescript import { IncludeQuery, QueryFunction, QueryParams, QuerySortType, QueryType, } from "@gsb-core/core"; const queryParams = new QueryParams("Order") .type(QueryType.FullNonPersonal) .filter("status", "open", QueryFunction.Equals) .filter("total", 100, QueryFunction.GreaterOrEqual) .include( new IncludeQuery("customer").select(["id", "name"]), ) .self.sortBy("createDate", QuerySortType.Descending) .skip(20) .take(10) .select(["id", "status", "total", "createDate"]) .returnCount(); const result = await entityService.query(queryParams); ``` The current fluent methods are: | Method | Serialized property | Purpose | |---|---|---| | `type()` | `queryType` | Set the query hydration mode. | | `filter()` | `filters` | Add a filter using a property name, value, function, and relation. | | `search()` | `searchText` | Apply configured full-text search. | | `select()` | `selectCols` | Select only required properties. | | `include()` | `includes` | Include related entities; use `.inc` for the latest include and `.self` to return to the root query. | | `sortBy()` | `sortCols` | Sort ascending or descending. | | `skip()` | `startIndex` | Set the zero-based offset. | | `take()` / `limit()` | `count` | Limit the number of returned entities. | | `returnCount()` | `calcTotalCount` | Request the total matching count. | | `pickEntity()` | `entityId` | Target an entity for mapped or relationship operations. | ## CLI JSON CLI and MCP calls use the serialized form of the same model: ```bash gsb call query --input '{ "queryParams": { "entDefName": "Order", "filters": [{ "col": { "name": "status" }, "val": { "value": "open" }, "function": 0 }], "selectCols": [{ "name": "id" }, { "name": "status" }], "sortCols": [{ "col": { "name": "createDate" }, "sortType": "desc" }], "startIndex": 0, "count": 25, "calcTotalCount": true } }' --raw ``` The CLI also reads the legacy GSB code-library shape (`query`, `propVal`, and `colName`) and normalizes it to `filters`, `col/val`, and `name`. New code should always use the current form shown above. ## Server caveats (verified live against dev1, 2026-08-20) These are server behaviors, not client bugs — plan around them. **Predicate delivery** - `filters[]` is honored on its own. Each entry may use either the `col`/`val` shape or the legacy `propVal` shape, and both filter identically — verified deterministic over 25 consecutive runs each, on a scalar column (349 rows narrowed to 62) and on a reference path (`roles.id`, 9 narrowed to 1). - If a request carries **both** `filters` and a legacy `query` array, the `query` array wins. That is safe when the two agree, but a hand-built mirror that drops fields — `aggregateFunction`, `valQuery`, `extra` — silently changes the meaning of the query. Send `filters` alone unless you have a specific reason not to. - `toQueryParams()` throws when a query has no entity target or a filter/sort entry is malformed — a silently dropped predicate would return a broader result set than intended. **Broken filter functions** - **`function: 19` (Between) needs its bounds in `extra`, not `val`.** Put `extra: { value1, value2 }` on the **filter** object; `val` stays empty. Passing an array in `val.value`, or putting `extra` on `col`/`val`, fails with HTTP 500. Verified: `size` between 0 and 1000 returned 164 rows, identical to `GreaterOrEqual` + `SmallerOrEqual`. Works on dates too. ```json { "col": { "name": "size" }, "val": {}, "function": 19, "extra": { "value1": 0, "value2": 1000 } } ``` - **`function: 20` (PhraseSearch) fails with HTTP 500** — `42883: function phraseto_tsquery(unknown, ...) does not exist` server-side. Use `FullTextSearch` (11) or Like. - **`In` (8) on a bare multi-reference property name fails with HTTP 500.** Use the dotted id path (`roles.id`). On a **scalar text column** `In` works with a plain string array — verified: `title In ["Validate Subscription SAAS", "Workspace Create"] ` returned exactly the 2 matching `GsbWfFunction` rows. The dotted `.id` path is required only for reference fields. **Semantics** - **`Contains` (12) is array/multi-reference membership, not substring.** On text it returns 0 rows where Like matches — that is correct behavior, not a bug. Use `Like` (1) with `%` wildcards for text search. - **Reference fields:** `Contains` (12), `In` (8), and `Equals` (0) all match a given id-array against related ids. `Contains` targets the multi-ref property name (`roles`); `In`/`Equals` need the dotted id path (`roles.id`). - `includes[]` requires the include property to resolve on the target definition or the whole query fails with HTTP 400. - **`includes[].count` is not a limit.** A requested limit of 2 returned 9, 37, and 11 related rows on a metadata relation, and 2, 7, 2 on a real many-to-many. `take`, `startIndex`+`count`, `queryType`, and `count`+`sortCols` behave the same. Narrow related collections with `includes[].filters` instead — see Filtered includes. - **Grouped aggregation emits a stray null-key row.** A `sum` grouped by `listingType` returned 3 rows, one with no group key. Filter null-key rows before presenting aggregate results. - **`searchText` is an alias of the scalar `filter`.** Both run the cross-field auto-search over the entity's searchable fields (`isSearchable=true`) — no `filters[]` needed: `{ entDefName: "GsbEntityDef", filter: "workflow" }` narrowed 189 rows to 5, matching on both `name` (`GsbWfLog`) and `title` (`Workflow Log`). Use `FullTextSearch` (11) on a specific field when you need full-text semantics (ranking/headline) rather than a LIKE sweep. - **`searchHighlight` does not guarantee markup.** On `GsbWfFunction.code` it returned a truncated raw-text prefix with **no `` tags** (and `undefined` on non-matching rows), not the `Data` form shown under Full text search below. Treat highlight as best-effort snippet text; only a proper `fullTextIndex` + headline path emits markup. **Patterns for harder shapes** These need more than one call or a different formulation. Each has a working route. - **Top-N per group.** Parent correlation supports related filters and aggregates, but it does not provide a per-parent ranking/window operation, and include limits are ignored. **Route:** if the N rows are identifiable by a predicate, use a **filtered include** (above) — this covers most real cases. For a true ranked top-N, select the parents and issue one child query per parent, or select the relation and slice in the caller. Global top-N is a single query (`sortCols` + `count`), and the single extreme row is an aggregate subquery. - **Heterogeneous union across unrelated definitions.** `unions`, `queries`, an `entityDef` array, and a root `dynamicDefinition` are all rejected or ignored. `GsbEntityDef.parent_id` is organizational grouping, not subtype inheritance — querying a parent definition returns that definition's own rows, so it is not a union mechanism. **Route:** model the feed explicitly. A single definition holding the activity records, written by the workflows that create orders/invoices/payments, turns the feed into one ordinary query with correct paging and `totalCount`. Merging N queries in the caller works for small result sets but makes paging and `totalCount` unreliable. ## Response ### Success Response ```json { "success": true, "entities": [ // Array of matching entities { "id": "string", "property1": "value1", "property2": "value2", // ... } ], "totalCount": 42, // Present only if calcTotalCount is true "message": "string", // Optional message "status": 200 // Optional status code } ``` ## Query Functions (QueryFunction Enum) The query system supports various functions for filtering entities: | Function | Enum Value | Description | Example | |----------|------------|-------------|---------| | Equals | 0 | Exact value match | `{ col: { name: "status" }, val: { value: "active" }, function: 0 }` | | Like | 1 | Pattern matching | `{ col: { name: "name" }, val: { value: "John%" }, function: 1 }` | | Greater | 2 | Greater than comparison | `{ col: { name: "price" }, val: { value: 100 }, function: 2 }` | | Smaller | 3 | Smaller than comparison | `{ col: { name: "quantity" }, val: { value: 50 }, function: 3 }` | | NotEqual | 4 | Not equal comparison | `{ col: { name: "status" }, val: { value: "inactive" }, function: 4 }` | | BitwiseAnd | 5 | Bitwise AND operation | `{ col: { name: "flags" }, val: { value: 8 }, function: 5 }` | | BitwiseOr | 6 | Bitwise OR operation | `{ col: { name: "flags" }, val: { value: 4 }, function: 6 }` | | BitwiseXor | 7 | Bitwise XOR operation | `{ col: { name: "flags" }, val: { value: 2 }, function: 7 }` | | In | 8 | Check if value is in a set. On a reference field use the dotted id path (`ref.id`); on the bare multi-ref property name it fails server-side. | `{ col: { name: "roles.id" }, val: { value: ["role-id"] }, function: 8 }` | | Is | 9 | Type checking (null/not null) | `{ col: { name: "createDate" }, val: { value: null }, function: 9 }` | | IsNot | 10 | Type checking negation | `{ col: { name: "createDate" }, val: { value: null }, function: 10 }` | | FullTextSearch | 11 | Full text search | `{ col: { name: "description" }, val: { value: "search terms" }, function: 11 }` | | Contains | 12 | Array / multi-reference membership: matches rows whose related-id set contains the given id(s). Target the multi-ref property name directly. NOT a substring operator — use Like for text. | `{ col: { name: "roles" }, val: { value: ["role-id"] }, function: 12 }` | | GreaterOrEqual | 13 | Greater than or equal comparison | `{ col: { name: "price" }, val: { value: 100 }, function: 13 }` | | SmallerOrEqual | 14 | Smaller than or equal comparison | `{ col: { name: "quantity" }, val: { value: 50 }, function: 14 }` | | ILike | 15 | Case-insensitive pattern matching | `{ col: { name: "name" }, val: { value: "john%" }, function: 15 }` | | RegexMatch | 16 | Regular expression matching | `{ col: { name: "email" }, val: { value: ".*@domain\.com" }, function: 16 }` | | RegexMatchCaseInsensitive | 17 | Case-insensitive regex matching | `{ col: { name: "email" }, val: { value: ".*@DOMAIN\.COM" }, function: 17 }` | | IsNull | 18 | Check if value is null | `{ col: { name: "deletedAt" }, val: { value: null }, function: 18 }` | | Between | 19 | Range test. Bounds go in `extra: { value1, value2 }` on the **filter**, not in `val`. | `{ col: { name: "price" }, val: {}, function: 19, extra: { value1: 100, value2: 500 } }` | | PhraseSearch | 20 | Phrase-based text search | `{ col: { name: "content" }, val: { value: "exact phrase" }, function: 20 }` | | GeometryOverlaps | 21 | Geometry overlap check | `{ col: { name: "area" }, val: { value: geometryObject }, function: 21 }` | | PointInGeometry | 22 | Point within geometry check | `{ col: { name: "location" }, val: { value: pointObject }, function: 22 }` | | GPSDistance | 23 | GPS distance calculation | `{ col: { name: "coordinates" }, val: { value: [lat, lng, distance] }, function: 23 }` | | GPSWithinRadius | 24 | GPS within radius check | `{ col: { name: "coordinates" }, val: { value: [lat, lng, radius] }, function: 24 }` | | JsonContains | 25 | JSON containment check | `{ col: { name: "metadata" }, val: { value: {"key": "value"} }, function: 25 }` | | JsonHasKey | 26 | JSON key existence check | `{ col: { name: "metadata" }, val: { value: "keyName" }, function: 26 }` | | MatchArrays | 27 | Set membership against the value-set returned by a subquery on `val.valQuery`. Row matches if the column is in the set. For a literal array use `Contains` (12). Set `distinct: true` — the join is not deduplicated. | `{ col: { name: "roles.id" }, val: { valQuery: { entDefName: "GsbRole", selectCols: [{ name: "id" }] } }, function: 27 }` | ## Aggregate Functions (AggregateFunction Enum) The query system supports the following aggregate functions for analytical queries: | Function | Enum Value | Description | Example | |----------|------------|-------------|---------| | None | 0 | No aggregation | `{ name: "id", aggregateFunction: 0 }` | | Sum | 1 | Sum of values | `{ name: "amount", aggregateFunction: 1, selectAsTitle: "total_amount" }` | | Average | 2 | Average of values | `{ name: "price", aggregateFunction: 2, selectAsTitle: "average_price" }` | | Count | 3 | Count of records | `{ name: "id", aggregateFunction: 3, selectAsTitle: "total_records" }` | | Maximum | 4 | Maximum value | `{ name: "price", aggregateFunction: 4, selectAsTitle: "highest_price" }` | | Minimum | 5 | Minimum value | `{ name: "price", aggregateFunction: 5, selectAsTitle: "lowest_price" }` | | Variance | 6 | Variance of values | `{ name: "score", aggregateFunction: 6, selectAsTitle: "score_variance" }` | ## Date Modifiers (DateModifier Enum) For time-based grouping and analysis: | Modifier | Enum Value | Description | |----------|------------|-------------| | None | 0 | No date modification | | Year | 1 | Group by year | | Quarter | 2 | Group by quarter | | Month | 3 | Group by month | | DayOfYear | 4 | Group by day of year | | DayOfMonth | 5 | Group by day of month | | Week | 6 | Group by week | | Weekday | 7 | Group by weekday | | Hour | 8 | Group by hour | | Minute | 9 | Group by minute | | Second | 10 | Group by second | | Millisecond | 11 | Group by millisecond | ## Query Types (QueryType Enum) | Type | Enum Value | Description | |------|------------|-------------| | Single | 0 | Single entity query | | List | 1 | List of entities | | Search | 2 | Search query | | AutoComplete | 3 | Autocomplete query | | Full | 4 | Full entity data | | FullWithSingleRefs | 5 | Full data with single references | | FullNonPersonal | 6 | Full data excluding createDate,lastUpdateDate,createdBy,lastUpdatedBy| ## Example Usage ### Basic Query ```typescript const result = await query({ queryParams: { entDefName: "Customer", filters: [ { col: { name: "status" }, val: { value: "active" }, function: 0 // QueryFunction.Equals } ] } }); if (result.success) { const customers = result.entities; console.log(`Found ${customers.length} active customers`); } ``` ### Query with Pagination and Sorting ```typescript const result = await query({ queryParams: { entDefName: "Order", startIndex: 0, count: 10, sortCols: [ { col: { name: "orderDate" }, sortType: "desc" } ], calcTotalCount: true } }); if (result.success) { const orders = result.entities; const totalOrders = result.totalCount; console.log(`Showing ${orders.length} of ${totalOrders} total orders`); } ``` ### Complex Query with Multiple Conditions `relation` goes on the **second** operand — see Nested filter logic. ```typescript // (price > 500 OR inStock = true) AND category = "tools" const result = await query({ queryParams: { entDefName: "Product", filters: [ { children: [ { col: { name: "price" }, val: { value: 500 }, function: 2 // QueryFunction.Greater }, { col: { name: "inStock" }, val: { value: true }, function: 0, // QueryFunction.Equals relation: "or" // joins to the filter above it } ] }, { col: { name: "category" }, val: { value: "tools" }, function: 0 } ], sortCols: [ { col: { name: "price" }, sortType: "asc" } ] } }); ``` Putting `relation: "or"` on the `children` wrapper instead would AND the two conditions together and usually return nothing. ### Using SelectCol with Scripts ```typescript const result = await query({ queryParams: { entDefName: "Order", selectCols: [ { name: "totalPrice", selectAsTitle: "total", nameScript: "([totalPrice]+[shipping])*[vatRate] - [discount]" } ], filters: [ { col: { name: "orderDate" }, val: { value: "2023-01-01" }, function: 2 // QueryFunction.Greater } ] } }); if (result.success) { console.log(`Found ${result.entities.length} orders`); } ``` ### Using Simple Search ```typescript const result = await query({ queryParams: { entDefName: "Product", searchText: "smartphone", // Will search across all searchable fields startIndex: 0, count: 20 } }); if (result.success) { const products = result.entities; console.log(`Found ${products.length} products matching 'smartphone'`); } ``` ### Including Related Entities ```typescript const result = await query({ queryParams: { entDefName: "Order", includes: [ { propertyName: "customer" }, { propertyName: "items", includes: [ { propertyName: "product" } ] } ] } }); ``` ## Additional Information - For complex queries, the filter conditions can be nested using the children property. - `relation` ("and" / "or") on a filter describes how it joins to the **preceding** filter, and defaults to "and". A `children` group's own `relation` joins the group to its preceding sibling; it does not combine the children. See "Nested filter logic". - The negate property inverts a single condition (NOT). It does not invert a `children` group. - When using includes, you can nest includes to fetch deeply related entities using IncludeQuery objects. - For better performance with large result sets, use pagination with startIndex and count. - The calcTotalCount option adds overhead to the query, so only use it when needed. - For direct access to a single entity by ID, use the getById operation instead. - The searchText parameter provides a simple way to search across all searchable fields. - For more complex search requirements, use explicit filter conditions. - The response contains entities directly in the `entities` field, not in a `data` field. ### SelectCol Usage - The SelectCol object provides extensive configuration options for column selection. - Use aggregateFunction with AggregateFunction enum values for analytical queries. - Use dateModifier with DateModifier enum values for time-based grouping. - The selectAsTitle property allows you to alias column names in results. - Scripts (nameScript, valScript) can contain JavaScript expressions for dynamic calculations. - The groupBy property is essential for analytical queries with aggregations. ### Filter Usage - The Filter object uses SelectCol for both col and val properties. - `val.value` holds a literal; `val.name` makes it a column-to-column comparison, and `val.valQuery` makes it a subquery. - Use QueryFunction enum values for the function property. - `relation` ("and" / "or") joins a filter to the one before it; `children` groups filters like parentheses. See "Nested filter logic". - Functions needing two operands, such as Between, take them in `extra: { value1, value2 }` on the filter. ### Full text search - to be able to use full text search, the property should be marked as fullTextIndex=true - you can use FullTextSearch function (QueryFunction.FullTextSearch = 11) in filters to search for a string in the property, or simply set searchText in the main query. - example queryParams: ```typescript const queryParams = { entDefName: "HelpPage", searchText: "data t", //if any help page has a title like "data table" or the en_us column of its associated content has "data table" will be selected. selectCols: [ { //will apply logical search on title property: title like '%data t%' "name": "title" }, { //will apply full text search on related content.en_us property (it's already marked as fullTextIndex=true) "selectAsTitle": "highlight", // alias name for the result column "searchHighlight": "data t", // search text to be highlighted, if not provided will return all content instead of highlight. "name": "content.en_us" // property name to be searched, we can use dot notation to search in nested properties. } ] } ``` response, data is highlighted with html tags. en_us column name is set to highlight because we set the selectAsTitle to highlight. ```json { "entities": [ { "title": "Data Table", "content": { "highlight": "Data Table The data table may display varying features across different sections or devices. It is designed" } } ] } ``` ### Analytical Queries - Use groupBy property in SelectCol objects to aggregate data by specific fields. - Aggregates define calculations to perform on grouped data using AggregateFunction enum: - Count (3): Count the number of records in each group - Sum (1): Calculate the total of a numeric field - Average (2): Calculate the average of a numeric field - Minimum (5): Find the minimum value - Maximum (4): Find the maximum value - Variance (6): Calculate variance of values - The selectAsTitle property in SelectCol defines the field name in the result. - If it's an analytical query, set groupBy or aggregateFunction to all selectCols, filters, includes and sorts. - A filter whose `col` carries an `aggregateFunction` becomes a `HAVING` clause; a filter whose `col` carries only `groupBy` stays a row-level `WHERE`. See "Filtering grouped results" above. - Analytical queries return aggregated data instead of complete entities. - For time-series analysis you can use dateModifier with DateModifier enum values. - Please refer to documentation for advanced usage. You can use sub queries, calculated fields, scripts, etc. example, get users who have a role with id 699b313c-cf1c-40c1-b86e-ab6e9a53f4f2, and group by their group and createDate based on addedYear. *note: all columns in the selectCols and sorts are either group by or aggregate function. ```typescript { "entityDef": { "name": "GsbUser" }, "filters": [ { "children": [ { "col": { "name": "roles" }, "val": { "value": [ { "id": "699b313c-cf1c-40c1-b86e-ab6e9a53f4f2" } ] }, "function": 12 // QueryFunction.Contains for many-to-many relation } ] } ], "selectCols": [ //group by groups, system automatically understands it's a many-to-many relation and does all the work. { "name": "groups", "groupBy": true }, //count the number of groups, we can use title or any field just to count { "name": "title", "aggregateFunction": 3, // AggregateFunction.Count "selectAsTitle": "count" }, //group by createDate based on addedYear { "name": "createDate", "dateModifier": 1, // DateModifier.Year "groupBy": true, "selectAsTitle": "addedYear" } ], "sortCols": [ //sort by createDate based on addedYear { "col": { "name": "createDate", "groupBy": true, "dateModifier": 1 // DateModifier.Year }, "sortType": "desc" } ] } ``` --- # queryMapped() — Entity Service Source: /api/querymapped # QueryMapped Operation ## General Description The `queryMapped` operation retrieves related entities for a specific entity based on a reference property. ## Detailed Description This operation is specifically designed for querying related entities through a reference property. It's particularly useful for retrieving entities in many-to-many or one-to-many relationships. Unlike the standard query operation, queryMapped requires an entityId (the parent entity) and a mapColName (the reference property name) to determine which related entities to retrieve. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | queryParams | object | Yes | The query parameters object that defines the search criteria and result options. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ### QueryParams Object Structure Same as the standard query operation, with these additional/required parameters: | Property | Type | Required | Description | |----------|------|----------|-------------| | entDefName | string | Yes* | Name of the entity definition to query. Required if entDefId is not provided. Getter/setter for entityDef.name. | | entDefId | string | Yes* | ID of the entity definition to query. Required if entDefName is not provided. Getter/setter for entityDef.id. | | entityDef | object | No | Entity definition object with id and/or name properties. | | entityId | string | Yes | Property for entity ID of the parent entity whose related entities you want to retrieve. | | mapColName | string | Yes | Name of the reference property that defines the relationship. | | propertyName | string | No | Alternative to mapColName, also specifies the reference property name. | | refColName | string | No | Getter/setter for mapColName. | | selectCols | array | No | Array of SelectCol objects defining columns to select in the query. If not provided, all columns are selected. | | filters | array | No | Array of Filter objects to filter the related entities. | | startIndex | number | No | Pagination start index (0-based). | | count | number | No | Number of records to return. | | sortCols | array | No | Array of SortCol objects for sorting specifications. | | includes | array | No | Array of IncludeQuery objects for additional related entities to include in the results. | | calcTotalCount | boolean | No | Whether to calculate the total count of matching records. | | searchText | string | No | Search term for automatic searching across all searchable fields. | | queryType | number | No | Type of query using QueryType enum (Single=0, List=1, Search=2, AutoComplete=3, Full=4, FullWithSingleRefs=5, FullNonPersonal=6). | ## Response ### Success Response ```json { "success": true, "entities": [ // Array of related entities { "id": "string", "property1": "value1", "property2": "value2", // ... } ], "totalCount": 42, // Present only if calcTotalCount is true "message": "string", // Optional message "status": 200 // Optional status code } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Basic Mapped Query ```typescript // Get all order items for a specific order const result = await queryMapped({ queryParams: { entDefName: "Order", entityId: "order-123", mapColName: "items" } }); if (result.success) { const orderItems = result.entities; console.log(`Order has ${orderItems.length} items`); orderItems.forEach(item => { console.log(`- ${item.quantity} x ${item.productName} (${item.unitPrice})`); }); } else { console.error("Error:", result.error); } ``` ### Filtered Mapped Query ```typescript // Get active users in a specific group const result = await queryMapped({ queryParams: { entityDef: { name: "Group" }, entityId: "group-456", mapColName: "members", filters: [ { col: { name: "status" }, val: { value: "active" }, function: 0 // QueryFunction.Equals (default) } ], sortCols: [ { col: { name: "lastName" }, sortType: "asc" } ] } }); if (result.success) { const activeMembers = result.entities; console.log(`Group has ${activeMembers.length} active members`); } ``` ### Mapped Query with Nested Includes ```typescript // Get products in a category with their suppliers const result = await queryMapped({ queryParams: { entDefName: "Category", entityId: "category-789", mapColName: "products", includes: [ { propertyName: "supplier", includes: [ { propertyName: "address", count: 1, // take only one address filters: [ { col: { name: "isDefault" }, val: { value: true }, function: 0 // QueryFunction.Equals } ] } ] } ] } }); if (result.success) { const products = result.entities; products.forEach(product => { if (product.supplier) { console.log(`${product.name} supplied by ${product.supplier.name}`); if (product.supplier.address) { console.log(` Supplier address: ${product.supplier.address.city}, ${product.supplier.address.country}`); } } }); } ``` ### Mapped Query with Aggregation ```typescript // Get count of orders by status for a specific customer const result = await queryMapped({ queryParams: { entDefName: "Customer", entityId: "customer-123", mapColName: "orders", selectCols: [ { name: "status", groupBy: true }, { name: "id", aggregateFunction: 3, // AggregateFunction.Count selectAsTitle: "orderCount" } ] } }); if (result.success) { const orderStats = result.entities; orderStats.forEach(stat => { console.log(`${stat.status}: ${stat.orderCount} orders`); }); } ``` ## Additional Information - The queryMapped operation is specifically designed for retrieving related entities through a reference property. - It requires both the parent entity ID (entityId) and the reference property name (mapColName). - The operation follows the reference defined in the entity definition to retrieve the related entities. - This operation is particularly useful for: - Retrieving items in a many-to-one relationship (e.g., order items for an order) - Retrieving entities in a many-to-many relationship (e.g., users in a group) - Retrieving any related entities where the reference property is defined as multiple - The operation supports all the filtering, sorting, pagination, and analytical capabilities of the standard query operation. - For standard entity queries without relationship mapping, use the query operation instead. - Access permissions are enforced based on the provided token. - Does not work with reference properties that are defined as single. - All SelectCol, Filter, SortCol, and IncludeQuery objects follow the same structure as the standard query operation. - Use QueryFunction enum values (0-27) for filter functions. - Use AggregateFunction enum values (0-6) for analytical queries. - Use DateModifier enum values (0-11) for time-based grouping. - Alternatively, you can use query operation on reference entity definition with filter, or query operation on same entity definition with include. - Example: - entity: Order - reference property: items - query on OrderItem with filter order_id = 'order-123' - query on Order with include items and filter id = 'order-123' - queryMapped on Order with mapColName = 'items' and entityId = 'order-123' --- # save() — Entity Service Source: /api/save # Save Operation ## General Description The `save` operation creates a new entity or updates an existing one in the database. ## Detailed Description This operation handles both creating new entities and updating existing ones. When saving an entity without an ID, a new entity is created, and an ID is automatically generated. When saving an entity with an existing ID, the entity is updated. The operation validates the entity against its definition before saving, ensuring data integrity. The save operation supports complex JSON structures with nested objects and arrays, automatically handling relationships between entities. GSB will intelligently process the data, performing inserts or updates for all nested entities and managing relationships automatically based on the presence of primary keys. ## Primary Key Matching An ID is not the only way to identify an existing entity. An entity definition can mark multiple properties as primary keys. When the save payload supplies any configured primary key and its value matches an existing row, GSB updates that row instead of inserting another one. For example, if `GsbUser.email` is a primary key, saving a user with an existing email updates that user even when the payload omits `id`. Partial primary keys form a composite match. GSB attempts that match only when the payload supplies every property marked `isPartialPrimaryKey`. If the complete value set identifies an existing row, save updates it; otherwise save creates a row. Do not treat one member of a partial-key set as independently identifying the entity. Participation of reference properties and their companion `*_id` fields in partial-primary-key matching is not yet a documented contract. Use `id` or verified scalar key properties for that case until the reference-field behavior has been tested. ## Atomic Calculated Updates Use `_CALC(expression)` as a property value when an update must be calculated from the value currently stored in the database. Direct properties on the row being saved use square-bracket references such as `[viewCount]`. The calculation runs as part of the save transaction, avoiding a read-modify-write race in application code. This example atomically increments an existing counter: ```typescript await save({ entDefName: "Article", entity: { id: articleId, viewCount: "_CALC([viewCount] + 1)", }, }); ``` The expression is not limited to increments. Integer and decimal literals can be combined with referenced direct properties, arithmetic, comparisons, parentheses, and supported functions and keywords. For example: ```typescript await save({ entDefName: "Order", entity: { id: orderId, totalCount: "_CALC([totalCount] + 1)", adjustedTotal: "_CALC([_ROUND](([_COALESCE]([subtotal], 0) + [_COALESCE]([shipping], 0)) * 1.20 - [_ABS]([discount]), 2))", }, }); ``` GSB evaluates the complete expression against the current stored row inside the save transaction. Expressions may be as deeply composed as the business calculation requires, provided every property, token, and character belongs to the documented allowlists. When a key-based save may create the row, append `: createValue` after the expression. GSB evaluates the expression for a matching row and uses the suffix as the initial value when no row exists: ```typescript await save({ entDefName: "AccountBalance", entity: { partner_id: partnerId, currency_id: currencyId, balance: `_CALC([_COALESCE]([balance], 0) + ${change}) : ${change}`, }, }); ``` ### Supported expression tokens Reference direct properties from the row being saved as `[propertyName]`. Use supported functions and keywords in bracketed underscore form, such as `[_COALESCE]` or `[_CASE]`: | Group | Supported tokens | |---|---| | Null and conditions | `NULL`, `CASE`, `WHEN`, `THEN`, `ELSE`, `END`, `COALESCE`, `NULLIF`, `IS`, `NOT`, `AND`, `OR` | | Numeric | `ABS`, `ROUND`, `CEILING`, `FLOOR`, `POWER`, `SQRT`, `EXP`, `LOG` | | Text | `LEN`, `LENGTH`, `LTRIM`, `RTRIM`, `SUBSTRING`, `SUBSTR`, `UPPER`, `LOWER`, `REPLACE`, `CONCAT` | | Aggregate | `COUNT`, `SUM`, `AVG`, `MIN`, `MAX` | For example, `_CALC([_CASE] [_WHEN] [amount] > 0 [_THEN] [_ROUND]([amount], 2) [_ELSE] 0 [_END])` combines keywords, a numeric function, and the entity's `amount` property. This table is the supported allowlist; do not assume arbitrary SQL functions are available. ### Allowed characters and string limitation Outside recognized bracketed property and operator names, calculation expressions accept only digits, spaces, and these punctuation characters: `0123456789+-*/.,():?\ <>=` The apostrophe character (`'`) is not allowed, so `_CALC` does not currently support quoted string literals or concatenating a static string such as `'prefix-'`. Text functions including `CONCAT`, `REPLACE`, `UPPER`, and `LOWER` operate on referenced entity string properties, for example `[_CONCAT]([firstName], [lastName])`. Supply fixed text through an ordinary saved property or calculate it outside `_CALC` rather than embedding a quoted literal. The exact wrapper token is `_CALC` with one leading underscore; `__CALC` is not valid syntax. Treat the expression as executable data-layer syntax: validate and convert interpolated values to expected primitive types, and never concatenate untrusted text into it. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | request | object | Yes | The request object containing the entity definition and entity to save. | | request.entityDef | object | Yes | The entity definition object with id and/or name properties. | | request.entity | object | Yes | The entity object to save, which can include nested objects and arrays for related entities. | | request.entityDef.name | string | Yes* | Name of the entity definition. Required if entDefId is not provided. | | request.entityDef.id | string | Yes* | ID of the entity definition. Required if entDefName is not provided. | | request.entity | object | Yes | The entity object to save, which can include nested objects and arrays for related entities. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ## Response ### Success Response ```json { "success": true, "id": "string", // ID of the created or updated entity "isUpdate": boolean // Whether this was an update (true) or create (false) operation } ``` ### Error Response ```json { "success": false, "message": "Error message describing what went wrong" } ``` ## Example Usage ### Creating a New Entity ```typescript const result = await save({ entDefName: "Customer", entity: { firstName: "John", lastName: "Doe", email: "john@example.com", status: "active" } }); if (result.success) { console.log("New customer created with ID:", result.id); console.log("Is update?", result.isUpdate); // false } else { console.error("Error:", result.error); } ``` ### Updating an Existing Entity ```typescript const result = await save({ entDefName: "Customer", entity: { id: "existing-customer-id", firstName: "John", lastName: "Doe", email: "john.updated@example.com", status: "inactive" } }); if (result.success) { console.log("Customer updated successfully with ID:", result.id); console.log("Is update?", result.isUpdate); // true } else { console.error("Error:", result.error); } ``` ### Saving Complex Nested Data ```typescript const result = await save({ entDefName: "Order", entity: { orderNumber: "ORD-12345", orderDate: "2023-06-15", status: "pending", customer: { id: "existing-customer-id", // Existing customer - will be updated firstName: "John", lastName: "Doe", email: "john@example.com" }, items: [ { id: "existing-item-id", // Existing item - will be updated productName: "Smartphone", quantity: 1, unitPrice: 999.99 }, { // No ID - new item will be created productName: "Phone Case", quantity: 2, unitPrice: 29.99 } ], shippingAddress: { // New address will be created and linked to the order street: "123 Main St", city: "Anytown", state: "CA", zipCode: "12345" }, paymentDetails: { id: "payment-123", // Existing payment - will be updated method: "credit_card", amount: 1059.97 } } }); if (result.success) { console.log("Order saved with ID:", result.id); } ``` ### Using Entity Definition ID ```typescript const result = await save({ entDefId: "customer-def-id", entity: { firstName: "Jane", lastName: "Smith", email: "jane@example.com", status: "active" } }); if (result.success) { console.log("Entity saved with ID:", result.id); } ``` ### Using Entity Definition Object ```typescript const result = await save({ entityDef: { name: "Product" }, entity: { name: "Smartphone", description: "Latest model smartphone", price: 999.99, inStock: true } }); ``` ## Additional Information - When creating a new entity, the system automatically generates an ID and sets system fields like createDate and createdBy. - When updating an entity, the system automatically updates the lastUpdateDate and lastUpdatedBy fields. - Required fields as defined in the entity definition must be provided. - Validation rules defined in the entity definition are enforced during saving. - For saving multiple entities at once, use the saveMulti operation instead. - The operation returns both the ID of the saved entity and a boolean indicating whether it was an update operation. - If the entity has unique constraints, the operation will fail if the constraints are violated. - Access permissions are enforced based on the provided token. - References to other entities can be included in the entity object using their IDs. - For saving mapped items in a many-to-many relationship, use the saveMappedItems operation instead. ### Complex Data Handling - GSB automatically processes nested objects and arrays as related entities. - For each nested entity: - If an ID is provided and exists in the database, the entity will be updated. - If no ID is provided or the ID doesn't exist, a new entity will be created. - Relationships between entities are automatically maintained. - One-to-many and many-to-many relationships are handled through arrays of objects. - One-to-one relationships are handled through nested objects. - Nested entities execute their configured workflow and serverless save triggers just like entities saved directly. - The system intelligently determines whether to perform inserts or updates based on the presence of primary keys. - All operations are performed in a single transaction, ensuring data consistency. - If any part of the complex save operation fails, the entire transaction is rolled back. --- # saveMulti() — Entity Service Source: /api/savemulti # SaveMulti Operation ## General Description The `saveMulti` operation saves multiple entities of the same type in a single operation, improving performance for batch operations. ## Detailed Description This operation allows you to create or update multiple entities of the same type in a single database operation. It's more efficient than making multiple individual save calls, especially when dealing with large batches of data. Like the save operation, it handles both creating new entities and updating existing ones based on whether each entity has an ID. Similar to the save operation, saveMulti supports complex JSON structures with nested objects and arrays. GSB automatically processes related entities, performing inserts or updates for all nested data and managing relationships based on the presence of primary keys. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | request | object | Yes | The save request object containing the entities to save. | | request.entityDef | object | Yes | The entity definition object with id and/or name properties. | | request.entityDef.name | string | Yes* | Name of the entity definition. Required if entDefId is not provided. | | request.entityDef.id | string | Yes* | ID of the entity definition. Required if entDefName is not provided. | | request.entities | array | Yes | Array of entity objects to save. Each entity can have an ID (for update) or not (for create), and can include complex nested objects and arrays for related entities. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ## Response ### Success Response ```json { "success": true, "ids": [ "string", // ID of the first saved entity "string", // ID of the second saved entity // Additional IDs in the same order as the input entities ] } ``` ### Error Response ```json { "success": false, "message": "Error message describing what went wrong" } ``` ## Example Usage ### Creating Multiple New Entities ```typescript const result = await saveMulti({ request: { entDefName: "Product", entities: [ { name: "Product 1", price: 10.99, inStock: true }, { name: "Product 2", price: 20.99, inStock: false }, { name: "Product 3", price: 15.49, inStock: true } ] } }); if (result.success) { const savedIds = result.ids; console.log(`Successfully saved ${savedIds.length} products`); console.log("First product ID:", savedIds[0]); console.log("Second product ID:", savedIds[1]); } else { console.error("Error:", result.error); } ``` ### Mixing Creates and Updates ```typescript const result = await saveMulti({ request: { entDefName: "Product", entities: [ { // New product (no ID) name: "New Product", price: 29.99, inStock: true }, { // Existing product (has ID) id: "existing-product-id", name: "Updated Product Name", price: 19.99, inStock: false } ] } }); if (result.success) { console.log("Saved entity IDs:", result.ids); // First ID is for the new product, second ID is the existing ID } ``` ### Saving Entities with Complex Nested Data ```typescript const result = await saveMulti({ request: { entDefName: "Order", entities: [ { orderNumber: "ORD-12345", status: "pending", customer: { id: "customer-123", // Existing customer - will be updated name: "John Doe", email: "john@example.com" }, items: [ { productName: "Laptop", quantity: 1, unitPrice: 1299.99 }, { productName: "Mouse", quantity: 1, unitPrice: 49.99 } ] }, { orderNumber: "ORD-67890", status: "processing", customer: { // No ID - new customer will be created name: "Jane Smith", email: "jane@example.com" }, items: [ { id: "existing-item-456", // Existing item - will be updated productName: "Headphones", quantity: 1, unitPrice: 199.99 } ] } ] } }); if (result.success) { console.log("Saved order IDs:", result.ids); // Both orders and all their related entities are saved in a single transaction } ``` ### Using Entity Definition ID ```typescript const result = await saveMulti({ request: { entDefId: "product-definition-id", entities: [ { name: "Product A", price: 9.99, inStock: true }, { name: "Product B", price: 14.99, inStock: true } ] } }); if (result.success) { console.log("Saved entity IDs:", result.ids); } ``` ## Additional Information - All entities in a single saveMulti operation must be of the same type (same entity definition). - The operation is transactional - either all entities are saved successfully, or none are. - For each entity, the system automatically handles: - Generating IDs for new entities - Setting createDate and createdBy for new entities - Updating lastUpdateDate and lastUpdatedBy for existing entities - Required fields as defined in the entity definition must be provided for each entity. - Validation rules defined in the entity definition are enforced for each entity. - The operation returns an array of IDs for all saved entities, in the same order as the input entities. - If any entity fails validation, the entire operation fails. - For better performance, batch your saves into reasonably sized groups (e.g., 100-500 entities per call). - For saving a single entity, use the save operation instead. - For saving mapped items in a many-to-many relationship, use the saveMappedItems operation instead. ### Complex Data Handling - Each entity in the entities array can include complex nested objects and arrays. - GSB automatically processes nested objects and arrays as related entities. - For each nested entity: - If an ID is provided and exists in the database, the entity will be updated. - If no ID is provided or the ID doesn't exist, a new entity will be created. - Nested entities execute their configured workflow and serverless save triggers just like entities saved directly. - Relationships between entities are automatically maintained. - One-to-many and many-to-many relationships are handled through arrays of objects. - One-to-one relationships are handled through nested objects. - The system intelligently determines whether to perform inserts or updates based on the presence of primary keys. - All operations are performed in a single transaction, ensuring data consistency across all entities and their related data. - If any part of the complex save operation fails, the entire transaction is rolled back. --- # delete() — Entity Service Source: /api/delete # Delete Operation ## General Description The `delete` operation removes entities from the database based on specified criteria. ## Detailed Description This operation allows you to delete one or more entities that match specific criteria. It can delete a single entity by ID or multiple entities that match certain conditions. The operation is permanent and cannot be undone, so it should be used with caution. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | request | object | Yes | The delete request object specifying what to delete. | | confirm | boolean | No | Must be `true` to execute. Without it the operation returns a confirmation challenge instead of deleting. | | confirmationToken | string | No | The token returned by the challenge. Required when `confirm` is `true`. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ### Request Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | entDefName | string | Yes* | Name of the entity definition. Required if entDefId is not provided. | | entDefId | string | Yes* | ID of the entity definition. Required if entDefName is not provided. | | entityDef | object | No | Optional entity definition object with id and/or name properties. | | entityId | string | Yes | ID of the entity to delete. | ## Two-step confirmation `delete` is registered as a destructive tool. It is disabled by default and gated behind a confirmation handshake: 1. Call it with the request only. The response carries `requiresConfirmation: true` and a `confirmationToken` derived from the request payload and tenant. 2. Repeat the identical call with `confirm: true` and that `confirmationToken`. The token is bound to the exact payload. Changing any field between the two calls invalidates it, and each token is consumed on use. ## Cascade Delete Cascade behavior is configured on the child entity's reference property with `cascadeReference: true`. The reference points from the dependent child to its parent. Deleting the parent then automatically deletes every child that refers to it through that property. Cascade delete is recursive. If a deleted child is itself the parent of another cascade reference, those dependent records are deleted too. For example: ```text Order <- OrderItem.order (cascadeReference: true) OrderItem <- OrderItemVariant.orderItem (cascadeReference: true) ``` A single `delete` request for an `Order` deletes its matching `OrderItem` records and their matching `OrderItemVariant` records. Callers do not issue separate delete requests for those descendants. Single-entity deletes also execute configured workflow and serverless delete triggers. To preserve cascades and triggers while deleting multiple root records, batch multiple single-entity `delete` requests instead of using `deleteQuery`. Only relationships whose child reference has `cascadeReference: true` participate. Unmarked references are not implicitly deleted and may cause the root delete to fail when referential integrity requires the related records to remain. ## Response ### Confirmation Challenge ```json { "success": false, "requiresConfirmation": true, "operation": "delete", "confirmationToken": "delete-1a2b3c4d", "message": "Confirmation required. Repeat delete with confirm=true and confirmationToken=delete-1a2b3c4d." } ``` ### Success Response ```json { "success": true, "data": { "affectedRowCount": 1, "deleteCount": 1, "status": 200, "message": "string" } } ``` `data` is the raw backend response. `affectedRowCount` is the authoritative count of removed rows. ### Error Response ```json { "success": false, "message": "Error message describing what went wrong" } ``` | Condition | `message` | |---|---| | `confirm: true` sent without a token | `confirmationToken is required when confirm=true.` | | Token does not match the payload, or was already used | `Invalid or stale confirmation token. Call the tool once without confirm to get a new token.` | | Neither `entDefName`, `entDefId`, nor `entityDef` supplied | `Entity definition is required` | | `entityId` missing | `Entity id is required` | | Token missing, expired, or issued for another tenant | Authentication error text from the backend | | Caller lacks delete permission on the definition or tenant | Authorization error text from the backend | | A referential integrity constraint blocks the delete | Constraint error text from the backend | | Network or transport failure | The underlying transport error message | The operation does not throw across the tool boundary; every failure is returned in this shape. ## Example Usage ### Delete a Single Entity by ID ```typescript const result = await delete({ request: { entDefName: "Customer", entityId: "customer-id-to-delete" }, token: "your-auth-token" }); if (result.success) { console.log(`Deleted ${result.data.affectedRowCount} customer(s)`); } else { console.error("Error:", result.message); } ``` ### Complete the two-step confirmation from the CLI ```bash # 1. Ask for the token gsb call delete --raw --input '{ "request": { "entDefName": "Customer", "entityId": "customer-id" } }' # 2. Repeat the identical payload with the returned token gsb call delete --yes --raw --input '{ "request": { "entDefName": "Customer", "entityId": "customer-id" }, "confirm": true, "confirmationToken": "delete-1a2b3c4d" }' ``` ### Delete Using Entity Definition ID ```typescript const result = await delete({ request: { entDefId: "customer-def-id", entityId: "customer-id-to-delete" }, confirm: true, confirmationToken: "delete-1a2b3c4d", token: "your-auth-token" }); if (result.success) { console.log("Customer deleted successfully"); } else { console.error("Error:", result.message); } ``` ### Delete with Entity Definition Object ```typescript const result = await delete({ request: { entityDef: { name: "Order" }, entityId: "order-id-to-delete" }, token: "your-auth-token" }); ``` ## Additional Information - The delete operation is permanent and cannot be undone. Use it with caution. - When deleting by ID, exactly one entity will be deleted if it exists. - The count of removed rows is `data.affectedRowCount`. - Access permissions are enforced based on the provided token. - For filter-driven bulk removal that does not require cascades or delete triggers, use [deleteQuery](/api/deletequery). - Cascade rules apply automatically and recursively as described above. - For soft delete (marking entities as deleted without physically removing them), use a status field and [save](/api/save) instead. - Model-generated deletes require explicit user confirmation before the second call is made. --- # deleteQuery() — Entity Service Source: /api/deletequery # DeleteQuery Operation ## General Description The `deleteQuery` operation removes entities from the database based on complex query parameters, providing more flexibility than the standard delete operation. ## Detailed Description This operation allows you to delete entities that match complex query criteria using the same query system as the query operation. It's particularly useful when you need to delete entities based on advanced filtering conditions, relationships, or complex logic that can't be easily expressed with the standard delete operation. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | queryParams | object | Yes | The query parameters object that defines what entities to delete. Same structure as used in the query operation. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ### QueryParams Object Structure Same as the standard query operation. See the query operation documentation for details. ## Behavioral Limitations `deleteQuery` performs a direct bulk deletion. It does **not** execute cascade deletes configured with `cascadeReference`, workflow delete triggers, or serverless delete triggers. These behaviors run for the single-entity `delete` operation only. When cascades or delete triggers are required, first query the matching entity IDs and then batch multiple single-entity `delete` requests. Each request is processed as an individual delete, so its recursive cascades and workflow or serverless delete triggers run normally. ## Response ### Success Response ```json { "success": true, "deletedCount": number // Number of deleted entities } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Basic DeleteQuery ```typescript const result = await deleteQuery({ queryParams: { entDefName: "Order", query: [ { propVal: { name: "status", value: "Cancelled" }, function: "equals" }, { propVal: { name: "createDate", value: "2023-01-01T00:00:00Z" }, function: "smaller" } ] }, token: "your-auth-token" }); if (result.success) { console.log(`Deleted ${result.deletedCount} cancelled orders from 2022`); } else { console.error("Error:", result.error); } ``` ### Complex DeleteQuery with Nested Conditions ```typescript const result = await deleteQuery({ queryParams: { entDefName: "Product", query: [ { propVal: { name: "category", value: "Electronics" }, function: "equals", relation: "AND", children: [ { propVal: { name: "price", value: 100 }, function: "smaller", relation: "OR" }, { propVal: { name: "inStock", value: false }, function: "equals" } ] } ] }, token: "your-auth-token" }); if (result.success) { console.log(`Deleted ${result.deletedCount} electronic products that are either under $100 or out of stock`); } ``` ### DeleteQuery with Relationship Filtering ```typescript const result = await deleteQuery({ queryParams: { entDefName: "Order", includes: [ { propertyName: "customer", query: [ { propVal: { name: "status", value: "Inactive" }, function: "equals" } ] } ] }, token: "your-auth-token" }); if (result.success) { console.log(`Deleted ${result.deletedCount} orders from inactive customers`); } ``` ## Additional Information - The deleteQuery operation is permanent and cannot be undone. Use it with caution. - This operation supports all the query capabilities of the query operation, including complex filtering, nested conditions, and relationship traversal. - The operation returns the number of affected rows (deleted entities). - For performance reasons, consider using more specific filters when deleting large numbers of entities. - Access permissions are enforced based on the provided token. - For simple delete operations, use the standard delete operation instead. - Do not use `deleteQuery` when cascade deletion or workflow/serverless delete triggers are required; batch single-entity `delete` requests instead. - For very large deletions, consider breaking the operation into smaller batches to avoid timeouts or performance issues. --- # saveMappedItems() — Entity Service Source: /api/savemappeditems # SaveMappedItems Operation ## General Description The `saveMappedItems` operation saves or updates mapped items (related entities) for a parent entity. ## Detailed Description This operation allows you to add or update related entities for a parent entity through a reference property. It's particularly useful for managing many-to-many or one-to-many relationships. The operation can create new related entities, update existing ones, and maintain the relationship between them and the parent entity. Like the save and saveMulti operations, saveMappedItems supports complex JSON structures with nested objects and arrays. GSB automatically processes the data, performing inserts or updates for all nested entities and managing relationships based on the presence of primary keys. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | request | object | Yes | The mapped save request object containing the mapping details. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ### Request Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | entDefName | string | Yes* | Name of the entity definition for the mapped items. Required if entDefId is not provided. | | entDefId | string | Yes* | ID of the entity definition for the mapped items. Required if entDefName is not provided. | | entityDef | object | No | Optional entity definition object with id and/or name properties. | | items | array | Yes | Array of items to map to the parent entity. Each item can be a complex object with nested entities. | | entityId | string | Yes | ID of the parent entity to which the items will be mapped. | | propName | string | Yes | Property name in the parent entity that holds the mapped items. | ## Response ### Success Response ```json { "success": true, "ids": [ "string", // ID of the first mapped item "string", // ID of the second mapped item // Additional IDs in the same order as the input items ] } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Add Items to an Order ```typescript const result = await saveMappedItems({ request: { entDefName: "OrderItem", entityId: "order-123", propName: "items", items: [ { productName: "Smartphone", quantity: 1, unitPrice: 999.99 }, { productName: "Phone Case", quantity: 1, unitPrice: 29.99 } ] }, token: "your-auth-token" }); if (result.success) { console.log("Added items with IDs:", result.ids); } else { console.error("Error:", result.error); } ``` ### Update Existing Mapped Items ```typescript const result = await saveMappedItems({ request: { entDefName: "OrderItem", entityId: "order-123", propName: "items", items: [ { id: "item-456", // Existing item - will be updated quantity: 2, // Updating quantity unitPrice: 999.99 } ] }, token: "your-auth-token" }); if (result.success) { console.log("Updated item with ID:", result.ids[0]); } ``` ### Add Users to a Group ```typescript const result = await saveMappedItems({ request: { entDefName: "User", entityId: "group-789", propName: "members", items: [ { id: "user-123" }, // Reference to existing user { id: "user-456" } // Reference to existing user ] }, token: "your-auth-token" }); if (result.success) { console.log("Added users to group"); } ``` ### Save Mapped Items with Complex Nested Data ```typescript const result = await saveMappedItems({ request: { entDefName: "OrderItem", entityId: "order-123", propName: "items", items: [ { productName: "Gaming Console", quantity: 1, unitPrice: 499.99, product: { id: "product-789", // Existing product - will be referenced name: "Next-Gen Console", category: "Electronics" }, options: [ { // New option will be created and linked to the order item name: "Extended Warranty", price: 49.99 }, { id: "option-456", // Existing option - will be updated name: "Premium Controller", price: 69.99 } ], shippingDetails: { // New shipping details will be created and linked method: "Express", estimatedDelivery: "2023-06-20", tracking: { // Nested object within shipping details carrier: "FastShip", number: "FS123456789" } } } ] }, token: "your-auth-token" }); if (result.success) { console.log("Added complex order item with ID:", result.ids[0]); } ``` ## Additional Information - The saveMappedItems operation can both create new related entities and update existing ones. - When an item has an ID, the system will update the existing entity if it exists. - When an item doesn't have an ID, a new entity will be created. - The operation maintains the relationship between the parent entity and the mapped items. - For many-to-many relationships, the operation updates the join table appropriately. - For one-to-many relationships, the operation updates the foreign key in the child entities. - For removing mapped items, use the removeMappedItems operation instead. - Access permissions are enforced based on the provided token. - The operation returns an array of IDs for all saved mapped items, in the same order as the input items. ### Complex Data Handling - Each item in the items array can include complex nested objects and arrays. - GSB automatically processes nested objects and arrays as related entities. - For each nested entity: - If an ID is provided and exists in the database, the entity will be updated. - If no ID is provided or the ID doesn't exist, a new entity will be created. - Nested entities execute their configured workflow and serverless save triggers just like entities saved directly. - Relationships between entities are automatically maintained at all levels of nesting. - One-to-many and many-to-many relationships are handled through arrays of objects. - One-to-one relationships are handled through nested objects. - The system intelligently determines whether to perform inserts or updates based on the presence of primary keys. - All operations are performed in a single transaction, ensuring data consistency across all entities and their related data. - If any part of the complex save operation fails, the entire transaction is rolled back. --- # removeMappedItems() — Entity Service Source: /api/removemappeditems # RemoveMappedItems Operation ## General Description The `removeMappedItems` operation removes mapped items from a parent entity, with options for cascade deletion based on relationship type. ## Detailed Description This operation allows you to remove the relationship between a parent entity and its mapped items. Depending on the relationship type and cascade settings, this operation may: 1. Simply break the reference between entities (for most relationships) 2. Delete the mapped items completely (if the relationship is marked as cascade or is a ManyToOne relationship) 3. Update the reference properties in both entities to maintain data integrity ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | request | object | Yes | The mapped remove request object containing the mapping details. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ### Request Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | entDefName | string | Yes* | Name of the entity definition for the mapped items. Required if entDefId is not provided. | | entDefId | string | Yes* | ID of the entity definition for the mapped items. Required if entDefName is not provided. | | entityDef | object | No | Optional entity definition object with id and/or name properties. | | items | array | Yes | Array of items to unmap from the parent entity. Typically contains IDs or identifying properties of the mapped items. | | entityId | string | Yes | ID of the parent entity from which the items will be unmapped. | | propName | string | Yes | Property name in the parent entity that holds the mapped items. | ## Response ### Success Response ```json { "success": true, "deletedCount": number // Number of removed mapped items } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Remove Items from an Order ```typescript const result = await removeMappedItems({ request: { entDefName: "OrderItem", entityId: "order-123", propName: "items", items: [ { id: "order-item-456" }, { id: "order-item-789" } ] }, token: "your-auth-token" }); if (result.success) { console.log(`Removed ${result.deletedCount} items from the order`); } else { console.error("Error:", result.error); } ``` ### Remove Users from a Group (Reference Only) ```typescript // This will only break the reference between users and the group, // not delete the user entities const result = await removeMappedItems({ request: { entDefName: "User", entityId: "group-456", propName: "members", items: [ { id: "user-123" }, { id: "user-456" } ] }, token: "your-auth-token" }); if (result.success) { console.log(`Removed ${result.deletedCount} users from the group`); } ``` ### Remove Child Entities with Cascade Delete ```typescript // This will delete the comment entities completely because // the relationship is marked as cascade const result = await removeMappedItems({ request: { entDefName: "Comment", entityId: "post-789", propName: "comments", items: [ { id: "comment-111" }, { id: "comment-222" } ] }, token: "your-auth-token" }); if (result.success) { console.log(`Deleted ${result.deletedCount} comments from the post`); } ``` ## Additional Information - The removeMappedItems operation handles different behaviors based on the relationship type: - For standard references (OneToMany, ManyToMany), it typically just breaks the reference - For ManyToOne relationships, it may delete the mapped items completely - For relationships marked with cascade delete, it will delete the mapped items - The operation returns the number of affected items (removed mappings or deleted entities). - When removing items from a relationship: - Reference properties in both entities are updated to maintain data integrity - If the reference is bidirectional, both sides of the relationship are updated - If cascade delete is enabled, dependent entities are also deleted - For adding mapped items, use the saveMappedItems operation instead. - Access permissions are enforced based on the provided token. - The operation is permanent and cannot be undone, so it should be used with caution. - Be particularly careful when working with relationships that have cascade delete enabled, as this will permanently delete the mapped entities. --- # getCommonPropertyDefs() — Schema Manager Source: /api/getcommonpropertydefs # GetCommonPropertyDefs Operation ## General Description The `getCommonPropertyDefs` operation retrieves the list of common property definitions available in the system. ## Detailed Description This operation returns a list of all common property definitions that can be used when defining entity properties. Property definitions represent the data types (like string, number, boolean, date, reference, etc.) that can be assigned to properties in entity definitions. Each property definition includes metadata about the data type, such as its name, description, and validation rules. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ## Response ### Success Response ```json { "success": true, "data": [ { "id": "string", "dataType": number, "title": "string", "name": "string", "description": "string", "maxLength": number, "scale": number, "regex": "string", "usage": number, "createDate": "string", "lastUpdateDate": "string", "defaultControlComponent": { "title": "string", "id": "string" } } ] } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Get All Common Property Definitions ```typescript const result = await getCommonPropertyDefs({ token: "your-auth-token" }); if (result.success) { const propertyDefs = result.data; console.log("Available property definitions:", propertyDefs.length); // List all property definitions propertyDefs.forEach(def => { console.log(`- ${def.title} (ID: ${def.id}): ${def.description}`); }); } else { console.error("Error:", result.error); } ``` ### Find a Specific Property Definition ```typescript const result = await getCommonPropertyDefs({ token: "your-auth-token" }); if (result.success) { // Find string property definition const stringDef = result.data.find(def => def.name === "String"); if (stringDef) { console.log("String property definition ID:", stringDef.id); console.log("Description:", stringDef.description); } // Find reference property definition const refDef = result.data.find(def => def.name === "Reference"); if (refDef) { console.log("Reference property definition ID:", refDef.id); } } else { console.error("Error:", result.error); } ``` ## Common Property Definition IDs Here are the IDs of commonly used property definitions: | Type | ID | Description | |------|--------------|-------------| | String | c6c34bf3-f51b-4e69-a689-b09847be74b9 | Text string | | Number | 35efcf9c-fff0-44d4-8972-73a9a32b93fa | Numeric value | | Boolean | 7868afdf-2709-45be-87e3-87de8d35f30f | True/false value | | DateTime | 12e647e0-ebd2-4ec2-a4e3-82c1dfe07da2 | Date and time | | Reference | 924acba8-58c5-4881-940d-472ec01eba5f | Entity reference | | Enum | 7bf08f4f-7de0-469e-bbfb-f4c43762f4d7 | Enumerated value | | RichText | e07f578e-2705-49c1-b97f-3ca5963c67c0 | Rich text content | | Email | df7ce94b-d59c-4b67-8519-aa4c98ab477c | Email address | | Password | 7291fbc2-a7cf-4713-a876-0cff085cc035 | Password field | ## Additional Information - Property definitions represent the data types available for entity properties. - Each property definition has a unique ID that should be used when creating or updating entity properties. - The response includes metadata about each property definition, such as: - Data type information - Default validation rules - Maximum length (for string types) - Scale (for numeric types) - Default UI component for rendering - When creating entity definitions or adding properties, you'll need to reference these property definition IDs. - The list of available property definitions may vary based on system configuration and extensions. - Access permissions are enforced based on the provided token. --- # getEntityDef() — Schema Manager Source: /api/getentitydef # GetEntityDef Operation ## General Description The `getEntityDef` operation retrieves an entity definition by its ID or name. ## Detailed Description This operation allows you to fetch the complete definition of an entity type, including all its properties, relationships, and metadata. Entity definitions represent the schema or blueprint for entities in the system, defining their structure, validation rules, and relationships to other entities. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | entityDef | object | Yes | The entity definition identifier. Object containing either the `id` (string) or `name` (string) of the entity definition to retrieve. One of these must be provided. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ## Response ### Success Response ```json { "success": true, "data": { "id": "string", "name": "string", "title": "string", "description": "string", "dbTableName": "string", "publicAccess": boolean, "activityLogLevel": number, "properties": [ { "id": "string", "name": "string", "title": "string", "description": "string", "definition_id": "string", "orderNumber": number, "isRequired": boolean, "isSearchable": boolean, "isUnique": boolean, "isPrimaryKey": boolean, "isIndexed": boolean, "maxLength": number, "defaultValue": "string", "regex": "string", "refEntDef_id": "string", "refEntPropName": "string", "refType": number } ], "isActive": boolean, "isDeleted": boolean, "createDate": "string", "lastUpdateDate": "string", "createdBy_id": "string", "lastUpdatedBy_id": "string", "permissions": [ // Permission objects ], "workflowTriggers": [ // Workflow trigger objects ] } } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Get Entity Definition by ID ```typescript const result = await getEntityDef({ entityDef: { id: "entity-def-id-123" } }); if (result.success) { const entityDef = result.entityDef; console.log("Entity Definition:", entityDef.name); console.log("Properties:", entityDef.properties.length); } else { console.error("Error:", result.error); } ``` ### Get Entity Definition by Name ```typescript const result = await getEntityDef({ entityDef: { name: "Customer" }, }); if (result.success) { const entityDef = result.entityDef; console.log("Entity Definition ID:", entityDef.id); console.log("Title:", entityDef.title); console.log("Description:", entityDef.description); // Access properties entityDef.properties.forEach(prop => { console.log(`Property: ${prop.name}, Title: ${prop.title}, Type: ${prop.definition_id}`); }); } else { console.error("Error:", result.error); } ``` ## Additional Information - The getEntityDef operation is used to retrieve the complete definition of an entity type. - You can retrieve an entity definition by either its ID or its name. - The response includes all properties defined for the entity, including their data types, validation rules, and relationships. - Entity definitions are the foundation for working with entities in the system: - They define the structure and validation rules for entities - They establish relationships between different entity types - They control permissions and access control for entities - For retrieving multiple entity definitions, use the queryEntityDefs operation. - For creating new entity definitions, use the createEntityDef operation. - For updating existing entity definitions, use the updateEntityDef operation. - Access permissions are enforced based on the provided token. --- # queryEntityDefs() — Schema Manager Source: /api/queryentitydefs # QueryEntityDefs Operation ## General Description The `queryEntityDefs` operation retrieves a paginated list of entity definitions. ## Detailed Description This operation allows you to fetch multiple entity definitions with pagination support. It's useful for discovering available entity types, building data dictionaries, or creating administrative interfaces that manage entity definitions. The results are paginated to handle large numbers of entity definitions efficiently. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | searchTerm | string | Yes | Case-insensitive term matched against definition name, title, and description. Pass an empty string to list everything. | | page | number | Yes | Page number, **1-based**. Values of zero or less are coerced to 1. | | pageSize | number | Yes | Number of items per page. Values of zero or less are coerced to 10. | | includeSystem | boolean | No | Whether to include system entity definitions. Default is false. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ## Response ### Success Response ```json { "success": true, "entityDefs": [ { "id": "string", "name": "string", "title": "string", "description": "string", "dbTableName": "string", "properties": [] } ], "totalCount": 0 } ``` `totalCount` is the number of definitions matching `searchTerm`, not the length of `entityDefs`. Divide it by `pageSize` to get the page count. ### Error Response ```json { "success": false, "message": "Error message describing what went wrong" } ``` | Condition | `message` | |---|---| | Token missing, expired, or issued for another tenant | Authentication error text from the backend | | Caller lacks read permission on entity definitions | Authorization error text from the backend | | `page` or `pageSize` is not a number | Type error raised before the request is sent | | Network or transport failure | The underlying transport error message | The operation does not throw across the tool boundary; every failure is returned in this shape. ## Example Usage ### Get First Page of Entity Definitions ```typescript const result = await queryEntityDefs({ searchTerm: "", page: 1, pageSize: 10, token: "your-auth-token" }); if (result.success) { console.log(`Found ${result.totalCount} definitions, showing ${result.entityDefs.length}`); for (const def of result.entityDefs) { console.log(`- ${def.title ?? def.name} (${def.name})`); } } else { console.error("Error:", result.message); } ``` ### List the next page from the CLI ```bash gsb call queryEntityDefs --raw --input '{ "searchTerm": "order", "page": 2, "pageSize": 25, "includeSystem": false }' ``` ## Additional Information - Pagination is 1-based. Page 1 is the first page; `page: 0` is silently treated as page 1. - When definition caching is enabled the search runs locally against the cached definitions, so results are filtered on `name`, `title`, and `description` only. - Cached results are projected to `name`, `title`, `description`, `dbTableName`, `id`, and `properties`. Use [getEntityDef](/api/getentitydef) for the complete definition. - System definitions are excluded unless `includeSystem` is true. - Access permissions are enforced from the token; callers only see definitions they may read. - For creating or modifying definitions, use [createEntityDef](/api/createentitydef) and [updateEntityDef](/api/updateentitydef). --- # createEntityDef() — Schema Manager Source: /api/createentitydef # CreateEntityDef Operation ## General Description The `createEntityDef` operation creates a new entity definition in the system. ## Detailed Description This operation allows you to define a new entity type in the GSB system. An entity definition represents a data table and includes metadata about the table itself as well as definitions for all of its properties (columns). When an entity definition is created, the corresponding database table is automatically generated. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | entityDef | object | Yes | The entity definition object. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ### Entity Definition Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | id | string | No | Unique identifier (auto-generated if not provided). | | name | string | Yes | Entity name (must be unique, PascalCase recommended). | | title | string | Yes | Display title for the entity. | | description | string | No | Description of the entity. | | dbTableName | string | No | Database table name (generated from entity name if not provided). | | publicAccess | boolean | No | Whether entity is publicly accessible. | | activityLogLevel | number | No | Level of activity logging (0=None, 1=Changes, 2=All). | | properties | array | No | Array of property definitions (columns). | | permissions | array | No | Array of permission objects controlling access to the entity. If not provided, all users can read and write. | | propertyPermissions | array | No | Base permissions applied to all properties unless overridden. If set, these permissions are applied to all properties that don't have their own permissions defined. | | workflowTriggers | array | No | Array of workflow trigger objects for the entity. | | isActive | boolean | No | Whether the entity is active. | ### Property Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | id | string | No | Unique identifier for the property (auto-generated if not provided). | | name | string | Yes | Name of the property (must be unique within the entity). | | title | string | Yes | Human-readable title for the property. | | description | string | No | Description of the property. | | definition_id | string | Yes | Reference to the property definition (data type). | | orderNumber | number | No | Display order for the property. | | isRequired | boolean | No | Whether the property is required. | | isSearchable | boolean | No | Whether the property should be searchable. | | isUnique | boolean | No | Whether the property value must be unique across all entities. | | isPrimaryKey | boolean | No | Whether the property is a primary key. | | isIndexed | boolean | No | Whether the property should be indexed for faster queries. | | maxLength | number | No | Maximum length for string properties. | | defaultValue | any | No | Default value for the property if not specified when creating an entity. | | regex | string | No | Validation regex pattern. | | refEntDef_id | string | No | Referenced entity definition ID (for reference properties). | | refEntPropName | string | No | Property name in referenced entity (for reference properties). | | refType | number | No | Reference type (OneToOne, OneToMany, etc.). | | isEncrypted | boolean | No | Whether the property value should be encrypted. | | isMultiLingual | boolean | No | Whether the property supports multiple languages. | | fullTextIndex | boolean | No | Whether to create a vector index for full text search (for RichText properties). | | cascadeReference | boolean | No | Whether to cascade delete and include in copy operations (for reference properties). | | permissions | array | No | Array of permission objects controlling access to the property. | | formModes | number | No | Form modes where property is visible. | | listScreens | number | No | List screens where property is visible. | ## Response ### Success Response ```json { "success": true, "entityDef": // created entity definition object including id and properties with id } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Create a Simple Entity Definition with Property Permissions ```typescript const result = await createEntityDef({ entityDef: { name: "User", title: "User Information", description: "User profile data", publicAccess: false, // Set base permissions for all properties - only admins can access propertyPermissions: [ {id: "admin-only-permission-id"} ], properties: [ { name: "username", title: "Username", description: "User's login name", definition_id: "c6c34bf3-f51b-4e69-a689-b09847be74b9", // String type isRequired: true, isSearchable: true, orderNumber: 10, // Override propertyPermissions for this specific property permissions: [ {id: "all-users-read-permission-id"}, {id: "self-write-permission-id"} ] }, { name: "email", title: "Email Address", description: "User's email address", definition_id: "df7ce94b-d59c-4b67-8519-aa4c98ab477c", // Email type isRequired: true, isUnique: true, orderNumber: 20 // No permissions specified, will use propertyPermissions (admin-only) }, { name: "password", title: "Password", description: "User's password", definition_id: "7291fbc2-a7cf-4713-a876-0cff085cc035", // Password type isRequired: true, isEncrypted: true, orderNumber: 30, // Override propertyPermissions for this specific property permissions: [ {id: "self-only-permission-id"} ] } ] }, token: "your-auth-token" }); ``` ### Create an Entity with Reference Properties ```typescript const result = await createEntityDef({ entityDef: { name: "Order", title: "Customer Order", description: "Order information", properties: [ { name: "orderNumber", title: "Order Number", description: "Unique order identifier", definition_id: "c6c34bf3-f51b-4e69-a689-b09847be74b9", // String type isRequired: true, isSearchable: true, isUnique: true, orderNumber: 10 }, { name: "customer", title: "Customer", description: "Customer who placed the order", definition_id: "924acba8-58c5-4881-940d-472ec01eba5f", // Reference type refEntDef_id: "customer-def-id", // ID of the Customer entity definition refEntPropName: "orders", // Property name in Customer entity for back-reference refType: 3, // ManyToOne isRequired: true, orderNumber: 20 }, { name: "notes", title: "Notes", description: "Order notes", definition_id: "e07f578e-2705-49c1-b97f-3ca5963c67c0", // RichText type isSearchable: true, fullTextIndex: true, // Enable full text search orderNumber: 30 }, { name: "items", title: "Items", description: "Items in the order", definition_id: "924acba8-58c5-4881-940d-472ec01eba5f", // Reference type refEntDef_id: "item-def-id", refEntPropName: "order", refType: 3, // ManyToOne cascadeReference: true, // Cascade delete and include in copy orderNumber: 40 } ] }, token: "your-auth-token" }); ``` ## Additional Information - The createEntityDef operation is used to define new data tables in the system. - Entity names must be unique across the entire system. - Entity names should follow PascalCase convention (e.g., "Customer", "ProductCategory"). - Property names should follow camelCase convention (e.g., "firstName", "orderDate"). ### Default Properties Default properties are automatically added to every entity definition: - `id` - Primary key (UUID), Required - `title` - Display title, better to define automated form builders use this field - `createdBy` - User who created the record (If a property with this name is defined GSB will automatically set its value) - `lastUpdatedBy` - User who last updated the record (If a property with this name is defined GSB will automatically set its value) - `createDate` - Creation timestamp (If a property with this name is defined GSB will automatically set its value) - `lastUpdateDate` - Last update timestamp (If a property with this name is defined GSB will automatically set its value) ### Common Property Types Common property definition IDs: - String: c6c34bf3-f51b-4e69-a689-b09847be74b9 - Number: 35efcf9c-fff0-44d4-8972-73a9a32b93fa - Boolean: 7868afdf-2709-45be-87e3-87de8d35f30f - DateTime: 12e647e0-ebd2-4ec2-a4e3-82c1dfe07da2 - Reference: 924acba8-58c5-4881-940d-472ec01eba5f - Enum: 7bf08f4f-7de0-469e-bbfb-f4c43762f4d7 - RichText: e07f578e-2705-49c1-b97f-3ca5963c67c0 - Email: df7ce94b-d59c-4b67-8519-aa4c98ab477c - Password: 7291fbc2-a7cf-4713-a876-0cff085cc035 - ID: 5c0aa76f-9c32-4e7e-a4bc-b56e93877883 ### Reference Properties For reference properties, you must specify: - refEntDef_id: The ID of the referenced entity definition - refEntPropName: The name of the property in the referenced entity that will hold the back-reference - refType: The type of relationship: - 1 = OneToOne - 2 = OneToMany - 3 = ManyToOne - 4 = ManyToMany ### Permissions - Entity-level permissions control overall access to the entity - Property-level permissions can be set in two ways: 1. Using `propertyPermissions` at the entity level to set base permissions for all properties 2. Using `permissions` on individual properties to override the base permissions - If `propertyPermissions` is set: - It applies to all properties that don't have their own `permissions` defined - Properties with their own `permissions` ignore `propertyPermissions` completely - This is useful for setting default access restrictions and then opening up specific properties - Example use cases: - Restricting all properties to admin-only access except for specific public fields - Setting stricter default permissions and selectively allowing access to certain properties - Implementing privacy controls where most data is protected but some fields are public ### Caching and Availability - Upon creation of an entity definition, the system initiates a cache update process across all redundant servers - The cache update process is asynchronous and may take up to 5 seconds to complete - During this time, the new entity definition may not be immediately available for use - It's important to wait for the cache update process to complete before adding new properties or referencing the new entity definition ### System Behavior When creating an entity definition: - The system will automatically create the corresponding database table - Default properties will be added if not explicitly defined - Indexes will be created for searchable and unique properties - For reference properties, appropriate foreign key fields and back-references are created automatically - Access permissions are enforced based on the provided token ### Related Operations - For updating existing entity definitions, use the updateEntityDef operation - For adding new properties to an existing entity, use the addProperty operation - For creating multiple related entities at once, use the createOrUpdateSchema operation --- # updateEntityDef() — Schema Manager Source: /api/updateentitydef # UpdateEntityDef Operation ## General Description The `updateEntityDef` operation modifies an existing entity definition with updated schema information. ## Detailed Description This operation allows you to update the metadata and structure of an existing entity definition. You can modify attributes like the title, description, permissions, and other aspects of the entity definition. Some structural changes may be limited to preserve data integrity, and adding or removing properties should be done using the dedicated addProperty and removeProperty operations. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | entityDef | object | Yes | The entity definition object with updated information. Must include the ID of the existing entity definition. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ### EntityDef Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | id | string | Yes | The ID of the existing entity definition to update. | | name | string | No | The name of the entity definition (usually cannot be changed after creation). | | title | string | No | Display title for the entity definition. | | description | string | No | Description of the entity definition. | | dbTableName | string | No | Database table name (usually cannot be changed after creation). | | publicAccess | boolean | No | Whether entity is publicly accessible. | | activityLogLevel | number | No | Level of activity logging (0=None, 1=Changes, 2=All). | | isActive | boolean | No | Whether the entity definition is active. | | permissions | array | No | Array of permission objects controlling access to the entity. | | propertyPermissions | array | No | Base permissions applied to all properties unless overridden. If set, these permissions are applied to all properties that don't have their own permissions defined. | | workflowTriggers | array | No | Array of workflow trigger objects for the entity. | ## Response ### Success Response ```json { "success": true, "entityDef": // updated entity definition object including id and properties with id } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Update Basic Entity Information with Permissions ```typescript const result = await updateEntityDef({ entityDef: { id: "product-def-123", title: "Product Catalog Item", description: "Updated description for product catalog items", publicAccess: false, permissions: [ {id: "product-team-write-permission-id"}, {id: "all-users-read-permission-id"} ] }, token: "your-auth-token" }); if (result.success) { console.log("Entity definition updated successfully"); console.log("Updated title:", result.data.title); console.log("Updated description:", result.data.description); } else { console.error("Error:", result.error); } ``` ### Update Entity Activity Logging and Workflow Triggers ```typescript const result = await updateEntityDef({ entityDef: { id: "order-def-456", activityLogLevel: 2, // Full logging title: "Customer Order Record", workflowTriggers: [ { id: "notify-on-status-change" } ] }, token: "your-auth-token" }); if (result.success) { console.log("Entity definition updated with new logging level and workflow triggers"); } ``` ### Update Entity Property Permissions with Mixed Access ```typescript const result = await updateEntityDef({ entityDef: { id: "employee-def-456", propertyPermissions: [ {id: "hr-team-permission-id"} // HR team can access all properties by default ], properties: [ { name: "name", permissions: [ {id: "all-users-read-permission-id"} // Everyone can read names ] }, { name: "salary", permissions: [ {id: "finance-team-permission-id"}, // Only finance team can access salary {id: "self-read-permission-id"} // Employees can see their own salary ] } // All other properties will use propertyPermissions (HR team only) ] }, token: "your-auth-token" }); ``` ## Additional Information ### Immutable Properties Some properties may be immutable after creation, particularly: - The name of the entity definition - The database table name - Core structural elements - Primary key configurations - Certain reference property settings ### Property Management - For adding new properties to an entity definition, use the addProperty operation - For removing properties from an entity definition, use the removeProperty operation - For updating existing properties, use the updateProperty operation - Property-level permissions can only be modified through the updateProperty operation ### Permissions - Entity-level permissions control overall access to the entity - Property-level permissions can be set in two ways: 1. Using `propertyPermissions` at the entity level to set base permissions for all properties 2. Using `permissions` on individual properties to override the base permissions - If you update `propertyPermissions`: - The new permissions apply to all properties that don't have their own `permissions` defined - Properties with their own `permissions` remain unaffected ### System Behavior - Setting an entity definition as inactive (isActive: false) prevents new entities from being created but preserves existing data - Changes to activity logging levels take effect immediately for new operations - Updates to workflow triggers are applied to all subsequent events - Access permissions are enforced based on the provided token ### Best Practices - Be cautious when updating entity definitions in production environments - Test changes in a development environment first - Consider the impact on existing data and integrations - Document significant changes for other developers - Coordinate updates with related entity definitions if necessary ### Related Operations - For creating new entity definitions, use the createEntityDef operation - For retrieving entity definitions, use the getEntityDef operation - For creating or updating multiple related entities at once, use the createOrUpdateSchema operation --- # addProperty() — Schema Manager Source: /api/addproperty # AddProperty Operation ## General Description The `addProperty` operation adds a new property to an existing entity definition. ## Detailed Description This operation allows you to extend an entity definition by adding a new property (column). The property can be of various types including primitive types (string, number, boolean, date), reference types (relationships to other entities), or specialized types (email, password, rich text, etc.). When a property is added, the underlying database schema is updated accordingly. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | property | object | Yes | The property definition object. | | entityDef | object | No | Optional. The entity definition object to modify. Should contain either the `id` or `name` of the entity definition. If not provided, the property.ownerEntityDefId must be specified. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ### Property Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | id | string | No | Unique identifier for the property (auto-generated if not provided). | | ownerEntityDefId | string | No | ID of the entity definition that will own this property. Required if entityDef is not provided. | | name | string | Yes | Name of the property (must be unique within the entity). | | title | string | Yes | Human-readable title for the property. | | description | string | No | Description of the property. | | definition_id | string | Yes | Reference to the property definition (data type). | | orderNumber | number | No | Display order for the property. | | isRequired | boolean | No | Whether the property is required. | | isSearchable | boolean | No | Whether the property should be searchable. | | isUnique | boolean | No | Whether the property value must be unique across all entities. | | isPrimaryKey | boolean | No | Whether the property is a primary key. | | isIndexed | boolean | No | Whether the property should be indexed for faster queries. | | maxLength | number | No | Maximum length for string properties. | | defaultValue | any | No | Default value for the property if not specified when creating an entity. | | regex | string | No | Validation regex pattern. | | refEntDef_id | string | No | Referenced entity definition ID (for reference properties). | | refEntPropName | string | No | Property name in referenced entity (for reference properties). | | refType | number | No | Reference type (OneToOne, OneToMany, etc.). | | isEncrypted | boolean | No | Whether the property value should be encrypted. | | isMultiLingual | boolean | No | Whether the property supports multiple languages. | | fullTextIndex | boolean | No | Whether to create a vector index for full text search (for RichText properties). | | cascadeReference | boolean | No | Whether to cascade delete and include in copy operations (for reference properties). | | permissions | array | No | Array of permission objects controlling access to the property. | | formModes | number | No | Form modes where property is visible. | | listScreens | number | No | List screens where property is visible. | ## Response ### Success Response ```json { "success": true, "data": { // The updated entity definition with the new property "id": "string", "name": "string", "properties": [ // All properties including the newly added one ] } } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Add a Simple String Property with Permissions ```typescript const result = await addProperty({ property: { name: "phoneNumber", title: "Phone Number", description: "Customer's contact phone number", definition_id: "c6c34bf3-f51b-4e69-a689-b09847be74b9", // String type isSearchable: true, orderNumber: 10, permissions: [ {id: "sales-team-write-permission-id"}, {id: "all-users-read-permission-id"} ] }, entityDef: { name: "Customer" }, // or { id: "customer-def-123" } token: "your-auth-token" }); if (result.success) { console.log("Property added successfully"); } else { console.error("Error:", result.error); } ``` ### Add a Required Number Property with Encryption ```typescript const result = await addProperty({ property: { name: "price", title: "Price", description: "Product price in USD", definition_id: "35efcf9c-fff0-44d4-8972-73a9a32b93fa", // Number type isRequired: true, isSearchable: false, isEncrypted: true, orderNumber: 20, permissions: [{id: "finance-team-permission-id"}] }, entityDef: { name: "Product" }, token: "your-auth-token" }); ``` ### Add a Reference Property with Cascade Delete Using ownerEntityDefId to specify the owner: ```typescript const result = await addProperty({ property: { name: "customer", title: "Customer", description: "Customer who placed the order", definition_id: "924acba8-58c5-4881-940d-472ec01eba5f", // Reference type refEntDef_id: "customer-def-123", refEntPropName: "orders", refType: 3, // ManyToOne cascadeReference: true, // Enable cascade delete isRequired: true, orderNumber: 30, ownerEntityDefId: "order-def-789" }, token: "your-auth-token" }); ``` ### Add a Rich Text Property with Full Text Search ```typescript const result = await addProperty({ property: { name: "description", title: "Description", description: "Detailed product description", definition_id: "e07f578e-2705-49c1-b97f-3ca5963c67c0", // RichText type isSearchable: true, fullTextIndex: true, // Enable full text search isMultiLingual: true, // Enable multi-language support orderNumber: 40 }, entityDef: { name: "Product" }, token: "your-auth-token" }); ``` ### Add Multiple Properties You can use saveMappedItems to add multiple properties at once. Please refer to the saveMappedItems documentation for more information. Required parameters: - entityId: The ID of the entity definition - entityDef: "GsbEntityDef" - propName: "properties" - items: Array of property objects to add ## Additional Information ### Property Types Common property definition IDs: - String: c6c34bf3-f51b-4e69-a689-b09847be74b9 - Number: 35efcf9c-fff0-44d4-8972-73a9a32b93fa - Boolean: 7868afdf-2709-45be-87e3-87de8d35f30f - DateTime: 12e647e0-ebd2-4ec2-a4e3-82c1dfe07da2 - Reference: 924acba8-58c5-4881-940d-472ec01eba5f - Enum: 7bf08f4f-7de0-469e-bbfb-f4c43762f4d7 - RichText: e07f578e-2705-49c1-b97f-3ca5963c67c0 - Email: df7ce94b-d59c-4b67-8519-aa4c98ab477c - Password: 7291fbc2-a7cf-4713-a876-0cff085cc035 ### Reference Properties For reference properties, you must specify: - refEntDef_id: The ID of the referenced entity definition - refEntPropName: The name of the property in the referenced entity that will hold the back-reference - refType: The type of relationship: - 1 = OneToOne - 2 = OneToMany - 3 = ManyToOne - 4 = ManyToMany ### Permissions - If you don't specify permissions, the property inherits permissions from its entity definition - If you specify permissions, they act as additional restrictions on top of entity permissions - Permissions can be defined in the Admin UI or via API using the "GsbPermission" entity definition - Don't pass permission IDs that don't exist in the system; instead, pass a fully defined GsbPermission object ### Caching and Availability - Upon adding a property, the system initiates a cache update process across all redundant servers - The cache update process is asynchronous and may take up to 5 seconds to complete - During this time, the new property may not be immediately available - It's important to wait for the cache update process to complete before using the new property ### Database Impact When adding a property: - The system automatically updates the database schema - Appropriate indexes are created based on property settings - Existing entities will have null values unless a default value is specified - For required properties, consider providing a default value - For encrypted properties, appropriate encryption infrastructure is set up - For full text search, necessary search indexes are created ### System Behavior - Property names must be unique within an entity definition - Names should follow camelCase convention - The system automatically handles database schema updates - Indexes are created for searchable and unique properties - Reference properties create appropriate foreign key relationships - Access permissions are enforced based on the provided token ### Best Practices - Plan property names carefully as they cannot be changed later - Consider the impact on existing data and queries - Test new properties in a development environment first - Document property purposes and relationships - Use batch operations for adding multiple properties - Consider default values for required properties - Plan permissions carefully before implementation ### Related Operations - For updating properties, use the updateProperty operation - For removing properties, use the removeProperty operation - For updating multiple properties, use the saveMappedItems operation - For complete entity updates, use the updateEntityDef operation --- # updateProperty() — Schema Manager Source: /api/updateproperty # UpdateProperty Operation ## General Description The `updateProperty` operation modifies an existing property in an entity definition. ## Detailed Description This operation allows you to update the attributes and settings of an existing property in an entity definition. You can modify aspects such as the title, description, validation rules, permissions, and other metadata. Some structural changes may be limited to preserve data integrity, and certain core attributes like the property name or data type may have restrictions on modifications. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | property | object | Yes | The property definition object with updated information. It MUST include either the `id` of the property, the `name` of the property, or the `ownerEntityDefId` to identify which property to update. Other attributes to be updated should be included. | | entityDef | object | No | Optional. The entity definition object to modify. Should contain either the `id` or `name` of the entity definition. Not required if property.id or property.ownerEntityDefId is provided. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ### Property Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | id | string | No | ID of the property to update. If provided, entityDef is not required. | | ownerEntityDefId | string | No | ID of the entity definition that owns this property. If provided, entityDef is not required. | | name | string | No | Name of the property (cannot be changed after creation). | | title | string | No | Human-readable title for the property. | | description | string | No | Description of the property. | | definition_id | string | No | Reference to the property definition (data type). | | isRequired | boolean | No | Whether the property is required. | | isSearchable | boolean | No | Whether the property should be searchable. | | isUnique | boolean | No | Whether the property value must be unique across all entities. | | maxLength | number | No | Maximum length for string properties. | | defaultValue | any | No | Default value for the property if not specified when creating an entity. | | regex | string | No | Validation regex pattern. | | refEntDef_id | string | No | Referenced entity definition ID (for reference properties). | | refEntPropName | string | No | Property name in referenced entity (for reference properties). | | refType | number | No | Reference type (OneToOne, OneToMany, etc.). | | isEncrypted | boolean | No | Whether the property value should be encrypted. | | isMultiLingual | boolean | No | Whether the property supports multiple languages. | | fullTextIndex | boolean | No | Whether to create a vector index for full text search (for RichText properties). | | cascadeReference | boolean | No | Whether to cascade delete and include in copy operations (for reference properties). | | permissions | array | No | Array of permission objects controlling access to the property. | | formModes | number | No | Form modes where property is visible. | | listScreens | number | No | List screens where property is visible. | ## Response ### Success Response ```json { "success": true, "data": { // The updated entity definition with the modified property "id": "string", "name": "string", "properties": [ // All properties including the updated one ] } } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Update Property Title and Description ```typescript const result = await updateProperty({ property: { name: "phoneNumber", // Name of the property to update title: "Contact Phone", description: "Primary contact phone number for the customer", permissions: [ {id: "sales-team-write-permission-id"}, {id: "all-users-read-permission-id"} ] }, entityDef: { id: "customer-def-123" }, // or { name: "Customer" } token: "your-auth-token" }); if (result.success) { console.log("Property updated successfully"); } else { console.error("Error:", result.error); } ``` ### Update Property Validation Rules and Security Using property.id to identify the property: ```typescript const result = await updateProperty({ property: { id: "property-id", name: "price", regex: "^[0-9]+(\.[0-9]{1,2})?$", maxLength: 10, isEncrypted: true, permissions: [{id: "finance-team-permission-id"}] }, token: "your-auth-token" }); ``` ### Update Property with Advanced Features Using ownerEntityDefId to specify the owner: ```typescript const result = await updateProperty({ property: { name: "customerReference", title: "Customer Reference Number", isRequired: true, isSearchable: true, isMultiLingual: true, description: "Unique reference number provided by the customer", ownerEntityDefId: "order-def-789" }, token: "your-auth-token" }); ``` ### Update Reference Property Settings ```typescript const result = await updateProperty({ property: { name: "assignedTo", refEntDef_id: "user-def-id", refEntPropName: "assignedTasks", refType: 3, // ManyToOne cascadeReference: true, // Enable cascade delete permissions: [ {id: "task-managers-permission-id"}, {id: "assigned-user-permission-id"} ] }, entityDef: { name: "Task" }, token: "your-auth-token" }); ``` ### Update Multiple Properties You can use saveMappedItems to update multiple properties at once. Please refer to the saveMappedItems documentation for more information. Required parameters: - entityId: The ID of the entity definition - entityDef: "GsbEntityDef" - propName: "properties" - items: Array of property objects to update ## Additional Information ### Property Identification - Properties can be identified using: - property.id: Direct property ID - property.name + entityDef: Property name within an entity - property.name + property.ownerEntityDefId: Property name with owner entity ID ### Immutable Attributes Some property attributes cannot be changed after creation: - Property name - Core data type (definition_id) - Primary key status - Certain reference property configurations ### Permissions - If you update permissions, the new permissions array completely replaces existing permissions - If you don't include permissions in the update, existing permissions remain unchanged - Permissions can be defined in the Admin UI or via API using the "GsbPermission" entity definition - Don't pass permission IDs that don't exist in the system; instead, pass a fully defined GsbPermission object ### Caching and Availability - Upon updating a property, the system initiates a cache update process across all redundant servers - The cache update process is asynchronous and may take up to 5 seconds to complete - During this time, the updated property configuration may not be immediately available - It's important to wait for the cache update process to complete before making additional changes ### Data Integrity When updating a property: - Making a property required may affect existing entities that don't have a value - Making a property unique will validate that all existing values are unique - Adding or modifying validation rules will not automatically validate existing data - Changing the default value only affects new entities created after the change - Enabling encryption will not automatically encrypt existing values ### System Behavior - Changes to searchability may trigger index updates - Modifying reference properties may affect related entities - Enabling full text search will create necessary database indexes - Permission changes take effect immediately for new operations ### Best Practices - Test changes in a development environment first - Consider the impact on existing data and queries - Document significant changes for other developers - Coordinate updates with related entity definitions - Use batch updates when modifying multiple properties ### Related Operations - For adding new properties, use the addProperty operation - For removing properties, use the removeProperty operation - For updating multiple properties, use the saveMappedItems operation - For complete entity updates, use the updateEntityDef operation --- # removeProperty() — Schema Manager Source: /api/removeproperty # RemoveProperty Operation ## General Description The `removeProperty` operation removes a property (column) from an existing entity definition. ## Detailed Description This operation allows you to remove a property from an existing entity definition. Removing a property modifies the entity definition schema and alters the underlying database structure. This operation is permanent and will result in the loss of all data stored in that property across all entities of this type, so it should be used with caution. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | property | object | Yes | The property object to remove. Must contain the `name` of the property. Optionally, it can contain the `id` of the property. | | entityDef | object | No | Optional. The entity definition object to modify. Should contain either the `id` or `name` of the entity definition. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ## Response ### Success Response ```json { "success": true, "data": { // The updated entity definition without the removed property "id": "string", "name": "string", "properties": [ // Remaining properties ] } } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Remove a Simple Property ```typescript const result = await removeProperty({ property: { name: "faxNumber" }, entityDef: { id: "customer-def-123" }, // or { name: "Customer" } token: "your-auth-token" }); if (result.success) { console.log("Property removed successfully"); } else { console.error("Error:", result.error); } ``` ### Remove a Property with Error Handling we can just provide property id to remove the property ```typescript try { const result = await removeProperty({ property: { id: "property-id" }, token: "your-auth-token" }); if (result.success) { console.log("Legacy code property removed successfully"); } else { console.error("Error removing property:", result.error); } } catch (error) { console.error("Exception occurred:", error); } ``` ### Remove Multiple Properties Sequentially you can user removeMappedItems to remove multiple properties all at once, pls refer to the removeMappedItems docs for more information you ill need to provide the entityId: the id of entity definition and entityDef as "GsbEntityDef" and propName as "properties" and items as property array with ids like {id: "property-id"} ## Additional Information - The removeProperty operation permanently removes a property from the entity definition. - All data stored in the removed property will be lost across all entities of this type. - This operation cannot be undone, so use it with caution. - Some system properties may be protected and cannot be removed. - Properties that are part of relationships or referenced by other entity definitions may require additional steps to remove. - Required properties that are in use by existing entities may need special handling. - For adding new properties, use the addProperty operation. - For updating existing properties, use the updateProperty operation. - Access permissions are enforced based on the provided token. - In production environments, it's recommended to: 1. Back up your data before removing properties 2. Consider the impact on existing integrations and code 3. If possible, deprecate properties before removing them 4. Communicate changes to users and other stakeholders --- # createOrUpdateSchema() — Schema Manager Source: /api/createorupdateschema # createOrUpdateSchema Creates or updates multiple entity definitions and their properties in a single operation, with intelligent handling of reference properties between entities. ## Parameters | Parameter | Type | Description | |-----------|------|-------------| | entityDefs | GsbEntityDef[] | Array of entity definitions to create or update | | token | string | (Optional) Authentication token | | tenantCode | string | (Optional) Tenant code | ## Returns ```typescript { createdEntities: GsbEntityDef[]; // List of entities created updatedEntities: GsbEntityDef[]; // List of entities updated errors: string[]; // Any errors that occurred during processing success: boolean; // Whether the operation succeeded } ``` ## Example ```typescript // Define multiple related entities const customerDef = { id: "customer-entity-definition-id", name: "Customer", title: "Customer Information", description: "Stores customer data", permissions:[{id:"all-users-read-permission-id"}, {id:"sales-team-write-permission-id"}] // If you don't pass permissions, all users can read and write properties: [ { id:"customer-id-property-id", name: "id", title: "ID", description: "Unique identifier for the customer", definition_id: "5c0aa76f-9c32-4e7e-a4bc-b56e93877883", // Every definition must have an id property isRequired: true, }, { id:"customer-name-property-id", name: "name", title: "Name", description: "Customer name", definition_id: "c6c34bf3-f51b-4e69-a689-b09847be74b9", // String type isRequired: true, isSearchable: true }, { id:"customer-password-property-id", name: "password", title: "Password", description: "Customer password", definition_id: "c6c34bf3-f51b-4e69-a689-b09847be74b9", // String type, isEncrypted: true, // Encrypted property permissions:[{id:"only-self-read-permission-id"}] // only the owner can read the property } ] }; const orderDef = { id: "order-entity-definition-id", name: "Order", title: "Order Information", description: "Stores order data", permissions:[{id:"all-users-read-permission-id"}, {id:"sales-team-write-permission-id"}] // If you don't pass permissions, all users can read and write properties: [ { id:"order-id-property-id", name: "id", title: "ID", description: "Unique identifier for the order", definition_id: "5c0aa76f-9c32-4e7e-a4bc-b56e93877883", // Id type isRequired: true, isSearchable: true }, { id:"order-notes-property-id", name: "notes", title: "Notes", description: "Notes of the order", definition_id: "e07f578e-2705-49c1-b97f-3ca5963c67c0", // RichText type isRequired: true, isSearchable: true, fullTextIndex: true // Create vector index for full text search }, { id:"order-customer-property-id", name: "customer", title: "Customer", description: "Customer who placed the order", definition_id: "924acba8-58c5-4881-940d-472ec01eba5f", // Reference type refEntDef_id: "customer-entity-definition-id", // Will be replaced with actual Customer entity ID refEntPropName: "orders", // Creates a back-reference property in Customer refType: 2 // OneToMany relationship }, { id:"order-items-property-id", name:"items", title:"Items", description:"Items in the order", definition_id:"924acba8-58c5-4881-940d-472ec01eba5f", // Reference type refEntDef_id:"item-entity-definition-id", // Will be replaced with actual Item entity ID refEntPropName:"order", // Creates a back-reference property in Item refType: 3, // ManyToOne relationship cascadeReference: true // Cascade delete, also include in copy operation }, { id:"order-tags-property-id", name:"tags", title:"Tags", description:"Tags in the order", definition_id:"924acba8-58c5-4881-940d-472ec01eba5f", // Reference type refEntDef_id:"tag-entity-definition-id", // Will be replaced with actual Tag entity ID refEntPropName:"orders", // Creates a back-reference property in Tag refType: 4 // ManyToMany relationship }, { id:"order-invoice-property-id", name:"invoice", title:"Invoice", description:"Invoice in the order", definition_id:"924acba8-58c5-4881-940d-472ec01eba5f", // Reference type refEntDef_id:"invoice-entity-definition-id", // Will be replaced with actual Invoice entity ID refEntPropName:"order", // Creates a back-reference property in Invoice refType: 1 // OneToOne relationship } ] }; // Create or update both entity definitions with reference handling in one operation const result = await mcp.createOrUpdateSchema({ entityDefs: [customerDef, orderDef] }); if (result.success) { console.log(`Created ${result.createdEntities.length} entities`); console.log(`Updated ${result.updatedEntities.length} entities`); } else { console.error("Errors:", result.errors); } ``` ## Description The `createOrUpdateSchema` tool provides a way to create or update multiple entity definitions in a single operation. This is particularly useful when creating a set of related entities with reference properties between them. ### Schema Creation Best Practices When creating an initial schema with multiple related entity definitions: 1. **Define all entity definitions in a single operation**: - Using createOrUpdateSchema, you can define the entire schema structure at once - The service will manage dependency order and relationships automatically - İf its first time to create the schema, its essential to pass all entity definitions in a single operation, so GSB can manage the dependencies between entities correctly. - If you want to add new entity definitions to the schema, you can use the createEntityDef method. 2. **Reference Property Management**: - Specify the correct `refEntDef_id`, `refEntPropName` and `refType` - For single relationships (OneToOne, ManyToOne), foreign keys properties(ending with _id) are automatically created - For example, adding `customer` ref property to an Order as OneToMany relationship(refType: 2) with refentpropname: orders * `customer_id` field will be automatically created in the Order entity definition * `orders` field will be automatically created in the Customer entity definition 3. **ID management**: - Every definition and property must have an id property. - If you don't pass an id, it will be generated by the system, and will be included in the response. - Its essential that every ID you provide is globally unique. 4. **Caching and availability**: - Upon creation or editing of an entity definition, the system will initiate a cache update process across all redundant servers. - The cache update process is asynchronous and may take up to 5 seconds to complete. - During this time, the new or updated entity definitions may not be immediately available for use. - Its also important to wait for the cache update process to complete before adding new properties or referencing the new entity definitions. 5. **Permissions**: - If you don't pass permissions, all users can read and write the entity definitions and properties. - If you pass permissions, the permissions will act as policies, if users cridentials match any policy, they will be able to execute the operation of the policy. - Permissions can be defined in the Admin UI, or with API by using the entity definition named : "GsbPermission" - Dont pass permision ids that dont exist in the system, instead you can pass a fully defined GsbPermission object. ### Reference Types The `refType` property defines the relationship type: ```typescript enum RefType { OneToOne = 1, OneToMany = 2, ManyToOne = 3, ManyToMany = 4 } ``` --- # runWfFunction() — Workflow Service Source: /api/runwffunction # RunWfFunction Operation ## General Description The `runWfFunction` operation executes a specific workflow function directly without needing to start or run a complete workflow. ## Detailed Description The `runWfFunction` operation allows you to execute a specific GSB serverless function by its ID or name. This is useful for: - Executing utility functions from other functions - Testing function behavior in isolation - Building modular function architectures where complex operations are broken into smaller, reusable functions - Implementing function-to-function communication patterns When calling a function using `runWfFunction`, you can pass: - An entity context - Custom parameters via the `prms` object - Other execution context information ## Input Parameters The `runWfFunction` operation accepts a request object with the following structure: ```javascript { "function": { // Either use ID "id": "function-uuid-here", // OR use name (one of these is required) "name": "Function Name Here" }, "instance": { // The entity to pass to the function (optional) "entity": { // Entity data }, // Additional parameters to pass (optional) "prms": { "param1": "value1", "param2": "value2" } } } ``` ## Response The response from `runWfFunction` contains: 1. A `response` field with whatever was set in the called function using: - `_instance.response = {...}` - `_runtime.success(result, response, action)` - `_runtime.end(statusCode, message, result, response, action)` 2. Status information and execution results from the function Non-2xx responses reject with `GsbHttpError`. Core callers can inspect `status`, `message`, `code`, `issues`, `details`, `logId`, and `systemLogId`. Business error fields are present when the backend returns the structured response envelope. MCP and CLI callers receive the equivalent failure result. Example response structure: ```javascript { "response": { // Whatever was set by the function "success": true, "data": { "id": "123", "status": "completed" } }, "status": 200, "message": "Operation completed successfully" } ``` ## Example Usage ### Example 1: Basic Function Call ```javascript // Define the function request let functionRequest = { function: { name: "Calculate Order Total" }, instance: { entity: myOrder, prms: { applyDiscounts: true } } }; // Call the function let entityService = new GsbEntityService(_runtime); let result = await entityService.runWfFunction(functionRequest); // Access the response if (result.response && result.response.success) { let calculatedTotal = result.response.totalPrice; // Continue processing... } ``` ### Example 2: Function Chain ```javascript async function processOrder() { try { // Step 1: Validate order let validateResult = await entityService.runWfFunction({ function: { name: "Validate Order" }, instance: { entity: _instance.entity } }); if (!validateResult.response.isValid) { _runtime.error(validateResult.response.validationErrors.join(", ")); return; } // Step 2: Calculate totals let totalsResult = await entityService.runWfFunction({ function: { name: "Calculate Order Totals" }, instance: { entity: _instance.entity } }); // Step 3: Process payment let paymentResult = await entityService.runWfFunction({ function: { name: "Process Payment" }, instance: { entity: _instance.entity, prms: { calculatedTotals: totalsResult.response.totals } } }); _runtime.success("Order processed successfully", paymentResult.response); } catch (error) { _runtime.error(error); } } ``` ## Additional Information ### Best Practices 1. **Error Handling**: Always implement proper error handling when calling functions: ```javascript try { let result = await entityService.runWfFunction(request); // Non-2xx function responses reject with GsbHttpError. } catch (error) { if (error instanceof GsbHttpError) { // Use error.status, error.code, error.issues, and error.details. } } ``` 2. **Data Passing**: Be consistent in how you structure function responses to make function chains more maintainable. 3. **Function Isolation**: Design functions to be self-contained units that can be tested and executed independently. 4. **Performance**: Be mindful of function call overhead in high-volume scenarios. Consider consolidating multiple small function calls if performance becomes an issue. ### Security Considerations Functions called via `runWfFunction` execute with the permissions of the calling context. Ensure that sensitive operations have appropriate authorization checks within the called function itself. --- # testWfFunction() — Workflow Service Source: /api/testwffunction # TestWfFunction Operation ## General Description The `testWfFunction` operation allows you to test a workflow function without saving it to the GSB backend. ## Detailed Description The `testWfFunction` operation is a powerful tool for developing and testing GSB serverless functions. It executes the provided function code with the specified context (entity and parameters) and returns the result, but does not persist the function definition to the GSB backend. This makes it ideal for: - Developing and debugging new functions - Testing function behavior with different inputs - Validating function logic before deployment - Experimenting with function modifications You can provide both the function code directly and an execution context with entity and parameters, allowing for comprehensive testing of function behavior. ## Input Parameters The `testWfFunction` operation accepts a request object with the following structure: ```javascript { "function": { // Required: Function details "name": "myFunction", // Name for the function (for reference only) "code": "// JavaScript code for the function _runtime.success('Success', {result: _instance.entity?.a + _instance.prms?.b});", // Optional: Operations array as a JSON string "operations": "[{...operation objects...}]" }, "instance": { // Optional: Entity context to pass to the function "entity": { // Entity data properties "a": 1 }, // Optional: Parameters to pass to the function "prms": { "b": 2 } } } ``` ### Key Parameters: - **function.name**: A name for the function (for reference only, not saved) - **function.code**: The JavaScript code for the function to test - **function.operations**: Optional JSON string containing declarative operations - **instance.entity**: Optional entity object to pass as `_instance.entity` to the function - **instance.prms**: Optional parameters object to pass as `_instance.prms` to the function ## Response The response from `testWfFunction` contains: 1. The function's execution result, including: - Whatever was set by `_runtime.success()`, `_runtime.error()`, or `_runtime.end()` - Any values assigned to `_instance.response` 2. Status information and execution details Example response structure: ```javascript { "response": { // Data returned by the function "ret": 3 // Example: result of _instance.entity.a + _instance.prms.b }, "status": 200, "message": "success" } ``` ## Example Usage ### Example 1: Testing a Simple Function ```javascript // Request to test a simple calculation function let testRequest = { "function": { "name": "addValues", "code": "let result = _instance.entity.value1 + _instance.prms.value2; _runtime.success('Calculation complete', {sum: result});" }, "instance": { "entity": { "value1": 10 }, "prms": { "value2": 20 } } }; // Using GsbEntityService to test the function let entityService = new GsbEntityService(_runtime); let testResult = await entityService.testWfFunction(testRequest); // testResult.response would contain {sum: 30} ``` ### Example 2: Testing a Function with Error Handling ```javascript // Request to test a function with validation and error handling let testRequest = { "function": { "name": "validateOrder", "code": ` // Get order from entity context let order = _instance.entity; // Validate required fields let errors = []; if (!order.customer_id) errors.push("Customer is required"); if (!order.items || order.items.length === 0) errors.push("Order must have at least one item"); // Return validation result if (errors.length > 0) { _runtime.error("Validation failed", {errors: errors}); } else { _runtime.success("Validation passed"); } ` }, "instance": { "entity": { "id": "order123", "customer_id": "", // Invalid - empty customer ID "items": [] // Invalid - empty items array } } }; // Test the function let testResult = await entityService.testWfFunction(testRequest); // testResult would contain validation errors ``` ### Example 3: Testing a Function with Declarative Operations ```javascript // Request to test a function with both code and operations let testRequest = { "function": { "name": "processOrder", "code": "// Custom pre-processing code let order = _instance.entity; order.preprocessed = true;", "operations": "[{\"id\":\"op1\",\"orderNumber\":1,\"operationType\":10,\"title\":\"Set Order Status\",\"setEntityOptions\":{\"setProps\":[{\"name\":\"status\",\"value\":2}]}}]" }, "instance": { "entity": { "id": "order456", "status": 1 } } }; // Test the function let testResult = await entityService.testWfFunction(testRequest); ``` ## Additional Information ### Best Practices for Testing Functions 1. **Incremental Testing**: Start with simple test cases and gradually add complexity. 2. **Test Edge Cases**: Include tests for boundary conditions, invalid inputs, and error scenarios. 3. **Isolate Dependencies**: When testing functions that call other functions or services, consider mocking those dependencies in your test code. 4. **Comprehensive Validation**: Check both the happy path (successful execution) and error paths. 5. **From Test to Production**: Once a function passes testing, you can save it to the GSB backend using `GsbEntityService.save()` with `entDefName: "GsbWfFunction"`. ### Converting Test Functions to Production After successful testing, you can save the function to the GSB backend: ```javascript // Save the tested function to the backend let saveRequest = { "entDefName": "GsbWfFunction", "entity": { "name": "myFunction", // Required "title": "My Function", // Required "code": "// The function code that was tested", "operations": "[{...operations that were tested...}]" } }; let saveResult = await entityService.save(saveRequest); let savedFunctionId = saveResult.id; ``` ### Testing vs. Running Functions - **testWfFunction**: Tests function code without saving it to the backend - **runWfFunction**: Executes a function that's already saved in the backend - **saveEnt with GsbWfFunction**: Saves a function to the backend for later use --- # runWorkflow() — Workflow Service Source: /api/runworkflow # RunWorkflow Operation ## General Description The `runWorkflow` operation executes a workflow and waits for the backend to finish processing before returning. ## Detailed Description `runWorkflow` posts the request to the workflow endpoint `/api/workflow/runWorkflow` and returns the backend response to the caller. Unlike entity reads and writes, workflow calls are never batched: the transport sends them immediately rather than folding them into a bulk request, because a workflow can trigger external side effects. A workflow is a `GsbWorkflow` record in the tenant, composed of `GsbActivity` nodes and `GsbTransition` edges. Executing it creates a `GsbWorkflowInstance` that records the run. Use `runWorkflow` when the caller needs the outcome of the run. Use [startWorkflow](/api/startworkflow) when the workflow contains human tasks, long waits, or anything else the caller should not block on. In the tool registry this operation is classified `external-side-effect`. It is disabled by default and requires explicit approval before it can be invoked through the CLI (`gsb call runWorkflow --yes`) or an MCP client. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | request | object | Yes | The workflow execution request, forwarded to the backend without transformation. | | token | string | No | Authentication token for the request. Falls back to the configured credentials when omitted. | | tenantCode | string | No | Tenant whose data to operate on. Falls back to the tenant encoded in the token or the configured tenant. | ### Request Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | workflow_id | string | Yes* | ID of the `GsbWorkflow` to execute. Required when `workflow` is not supplied. | | workflow | object | Yes* | Workflow reference carrying `id` and/or `name`. Required when `workflow_id` is not supplied. | | entity_id | string | No | ID of the entity the run operates on. Recorded on the resulting workflow instance. | | data | object | No | Payload made available to the workflow activities and their functions. | The request object is forwarded verbatim, so any additional field a specific workflow design expects is passed through unchanged. ## Response ### Success Response The backend response is returned with `success: true` merged in. ```json { "success": true, "instance": { "id": "string", "workflow_id": "string", "status": 0, "result": "string" }, "status": 200 } ``` The exact payload depends on the workflow design. The fields that are always meaningful come from `GsbWorkflowInstance`: `id`, `workflow_id`, `activity_id`, `entity_id`, `status`, `result`, `responseStr`, `startDate`, and `lastUpdateDate`. ### Error Response Every failure is normalised to the same shape; the operation does not throw across the tool boundary. ```json { "success": false, "message": "Error message describing what went wrong" } ``` | Condition | `message` | |---|---| | No workflow matches the supplied id or name | Backend error text describing the missing workflow | | Caller lacks permission on the workflow or tenant | Authorization error text from the backend | | Token missing, expired, or issued for another tenant | Authentication error text from the backend | | An activity function throws during the run | The error raised by that function | | Network or transport failure | The underlying transport error message | ## Example Usage ### Run a workflow by ID ```typescript const result = await entityService.runWorkflow( { workflow_id: "workflow-uuid", data: { orderId: "order-123", reason: "manual reprocess" }, }, token, tenantCode, ); if (result.success) { console.log("Instance:", result.instance?.id); } else { console.error("Workflow failed:", result.message); } ``` ### Run a workflow by name from the CLI ```bash gsb call runWorkflow --yes --raw --input '{ "request": { "workflow": { "name": "Order Approval" }, "entity_id": "order-123", "data": { "reason": "manual reprocess" } } }' ``` `--yes` is required: the tool is registered as an external side effect and is otherwise refused. ## Additional Information - Prefer `startWorkflow` for anything that waits on a human. A synchronous run holds the connection open for the whole duration. - Inspect a run afterwards by querying `GsbWorkflowInstance` with [query](/api/query), filtering on `workflow_id` and sorting by `lastUpdateDate`. - Advance a task that the run left waiting with [iterateTask](/api/iteratetask). - Never invoke this operation from model-generated code without an explicit user confirmation step. --- # startWorkflow() — Workflow Service Source: /api/startworkflow # StartWorkflow Operation ## General Description The `startWorkflow` operation creates a workflow instance and returns as soon as the run has been accepted, without waiting for the workflow to complete. ## Detailed Description `startWorkflow` posts the request to `/api/workflow/startWorkflow` and returns the backend acknowledgement. Like every workflow call, it bypasses request batching and is sent immediately, because starting a workflow is an external side effect. The started run is a `GsbWorkflowInstance` bound to a `GsbWorkflow`. Activities that require a person, a timer, or an external event leave the instance in a waiting state; the caller does not block on them. Use `startWorkflow` for approvals, onboarding, batch processing, and anything with a human task. Use [runWorkflow](/api/runworkflow) only when the caller genuinely needs the result inline. In the tool registry this operation is classified `external-side-effect`. It is disabled by default and requires explicit approval before it can be invoked through the CLI (`gsb call startWorkflow --yes`) or an MCP client. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | request | object | Yes | The workflow start request, forwarded to the backend without transformation. | | token | string | No | Authentication token for the request. Falls back to the configured credentials when omitted. | | tenantCode | string | No | Tenant whose data to operate on. Falls back to the tenant encoded in the token or the configured tenant. | ### Request Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | workflow_id | string | Yes* | ID of the `GsbWorkflow` to start. Required when `workflow` is not supplied. | | workflow | object | Yes* | Workflow reference carrying `id` and/or `name`. Required when `workflow_id` is not supplied. | | entity_id | string | No | ID of the entity the run operates on. Recorded on the resulting workflow instance. | | data | object | No | Payload made available to the workflow activities and their functions. | The platform workflow service calls this operation with exactly `{ workflow_id, data }`; any additional field a specific workflow design expects is forwarded unchanged. ## Response ### Success Response The backend response is returned with `success: true` merged in. ```json { "success": true, "instance": { "id": "string", "workflow_id": "string", "activity_id": "string", "status": 0, "startDate": "2026-01-01T00:00:00Z" }, "status": 200 } ``` The response acknowledges the start; it does not carry the workflow outcome. Read the outcome later from the `GsbWorkflowInstance` record. ### Error Response Every failure is normalised to the same shape; the operation does not throw across the tool boundary. ```json { "success": false, "message": "Error message describing what went wrong" } ``` | Condition | `message` | |---|---| | No workflow matches the supplied id or name | Backend error text describing the missing workflow | | Caller lacks permission on the workflow or tenant | Authorization error text from the backend | | Token missing, expired, or issued for another tenant | Authentication error text from the backend | | The first activity rejects the supplied `data` | The validation error raised by that activity | | Network or transport failure | The underlying transport error message | ## Example Usage ### Start a workflow and record the instance ```typescript const result = await entityService.startWorkflow( { workflow_id: "workflow-uuid", data: { orderId: "order-123" }, }, token, tenantCode, ); if (!result.success) { throw new Error(result.message); } const instanceId = result.instance?.id; ``` ### Poll the instance for completion ```typescript const instances = new QueryParams("GsbWorkflowInstance") .filter("workflow_id", "workflow-uuid") .sortBy("lastUpdateDate", QuerySortType.Descending) .select(["id", "status", "result", "lastUpdateDate"]) .skip(0) .take(25); const page = await entityService.query(instances, token, tenantCode); ``` ### Start a workflow from the CLI ```bash gsb call startWorkflow --yes --raw --input '{ "request": { "workflow": { "name": "Employee Onboarding" }, "data": { "employeeId": "emp-42" } } }' ``` ## Additional Information - The returned instance id is the handle for everything that follows: status polling, task iteration, and audit. - Advance a waiting human task with [iterateTask](/api/iteratetask). - Starting the same workflow twice creates two instances. Guard against duplicate starts in the caller. - Never invoke this operation from model-generated code without an explicit user confirmation step. --- # iterateTask() — Workflow Service Source: /api/iteratetask # IterateTask Operation ## General Description The `iterateTask` operation advances a workflow task to its next state or provides input to a waiting task. ## Detailed Description This operation allows you to interact with tasks in running workflows, particularly human tasks or tasks that require external input. It can be used to approve or reject tasks, provide data to waiting tasks, or trigger the next step in a workflow. This is essential for workflows that include human approvals, decision points, or tasks that need to wait for external events. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | request | object | Yes | The task iteration request object specifying the task to interact with and the action to take. | | token | string | No | Authentication token for your request. If not provided, the system will use the default API key from environment variables. | | tenantCode | string | No | Tenant code to specify which tenant's data to access. If not provided, the system will extract it from the token or use the default tenant code from environment variables. | ### Request Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | taskId | string | Yes | The ID of the task to iterate. | | action | string | Yes | The action to perform on the task (e.g., "approve", "reject", "complete", "skip"). | | input | object | No | Optional input data for the task. The structure depends on what the task expects. | ## Response ### Success Response ```json { "success": true, "data": { "taskStatus": "string", // New status of the task "workflowStatus": "string", // Current status of the parent workflow // Additional task-specific result data } } ``` ### Error Response ```json { "success": false, "error": "Error message describing what went wrong" } ``` ## Example Usage ### Approve a Task ```typescript const result = await iterateTask({ request: { taskId: "task-123", action: "approve", input: { comments: "Looks good, approved.", approvedBy: "user-456" } }, token: "your-auth-token" }); if (result.success) { console.log("Task approved successfully"); console.log("Task status:", result.data.taskStatus); console.log("Workflow status:", result.data.workflowStatus); } else { console.error("Error:", result.error); } ``` ### Reject a Task with Reason ```typescript const result = await iterateTask({ request: { taskId: "task-123", action: "reject", input: { reason: "Budget exceeds department limit", suggestedChanges: "Please reduce the amount or get additional approval", rejectedBy: "user-456" } }, token: "your-auth-token" }); ``` ### Provide Data to a Waiting Task ```typescript const result = await iterateTask({ request: { taskId: "task-456", action: "complete", input: { shippingCarrier: "FedEx", trackingNumber: "1234567890", estimatedDelivery: "2023-12-15" } }, token: "your-auth-token" }); ``` ## Additional Information - The iterateTask operation is used to interact with tasks in running workflows. - Common actions include: - "approve": Approve a task that requires approval - "reject": Reject a task that requires approval - "complete": Mark a task as completed and provide any required data - "skip": Skip a task (if allowed by the workflow) - "reassign": Reassign the task to another user or role - The available actions and required input depend on the specific task type and configuration. - Tasks can be part of: - Approval workflows - Multi-step business processes - Data collection workflows - Decision workflows - The operation returns the new status of the task and the current status of the parent workflow. - Access permissions are enforced based on the provided token. - Users can only iterate tasks they have permission to access. - For starting new workflows, use the startWorkflow operation instead. - For running simple workflows synchronously, use the runWorkflow operation instead. --- # iterateOnce() — Workflow Service Source: /api/iterateonce # IterateOnce Operation ## General Description The `iterateOnce` operation advances a workflow instance by **exactly one activity and then stops**. It is the stepper: the call a workflow builder uses to walk a design activity by activity while debugging. ## Detailed Description `iterateOnce` posts to `/api/workflow/iterateOnce` and is **administrator-only**. It takes the same instance input as [startWorkflow](/api/startworkflow) and [runWorkflow](/api/runworkflow), performs a single activity, and answers with the instance as it now stands. The three ways a parked run can move differ only in **where they stop**: | Operation | Caller | Stops | |---|---|---| | [iterateOnce](/api/iterateonce) | administrator | after one activity | | [iterateTask](/api/iteratetask) | integration or server function | when the run finishes or parks again | | [submitWorkflowTask](/api/submitworkflowtask) | the task's assignee | when the run finishes or parks again | `iterateOnce` is intended to clear a **user task** and halt on the next activity. The exact outcome selector remains contract-gated: live dev1 task creation is verified, but result label, transition id, and configured activity-result id were all rejected by the current engine. Do not claim successful task completion until the backend publishes and verifies that selector. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | request | object | Yes | The step request, forwarded to the backend without transformation. | | token | string | No | Authentication token for the request. Falls back to the configured credentials when omitted. | | tenantCode | string | No | Tenant whose data to operate on. Falls back to the tenant encoded in the token or the configured tenant. | ### Request Object Structure | Property | Type | Required | Description | |----------|------|----------|-------------| | instance | object | Yes | The workflow instance to step. | | instance.workflow_id | string | **Yes** | Id of the `GsbWorkflow`. The step resolves the definition from the instance payload, so this must be present on every call — including calls that resume an existing instance. | | instance.id | string | No | Id of an existing instance. Omit it to step a fresh run from its start activity. | | instance.entity_id | string | No | Id of the entity the run operates on. | | instance.entityDefinition_id | string | No | `GsbEntityDef` id of that entity. | | instance.entity | object | No | Entity payload for a run that has no stored row yet. | | instance.parameters | string | No | Instance parameters. This is a **string** column; sending an object fails the whole call. | When resuming, send the instance the previous step returned, with `workflow_id` set. Preserve the returned `currentTask` envelope; sending only `currentTask_id` creates another task instead of submitting the existing one on the current dev1 engine. ## Response The response body carries the live instance, which is richer than a `GsbWorkflowInstance` row read back by query: ```json { "instance": { "id": "string", "workflow_id": "string", "activity_id": "string", "parentActivity_id": "string", "parent_id": "string", "entity_id": "string", "entityDefinition_id": "string", "starter_id": "string", "lastProcessor_id": "string", "locker_id": "string", "name": "string", "title": "string", "message": "string", "result": "string", "parameters": "string", "status": 0, "trigger": {}, "followers": [], "currentTask": {}, "continiumChecked": false }, "status": 200 } ``` | Field | What it answers | |---|---| | `activity_id` | where the run is now | | `result` | what was selected on the activity just performed | | `currentTask` | the task to submit if the run parked on a person | | `lastProcessor_id` | who moved it | | `locker_id` | what is holding it | | `message` | why it stopped, including failure text a function set | | `parent_id`, `parentActivity_id` | the parent run and activity when this is a sub-flow | | `status` | bit field: `OnHold 1`, `Started 2`, `Completed 4`, `Cancelled 8`, `ReAssigned 16`, `Error 32` | Administrators receive this same envelope from `runWorkflow`, `startWorkflow`, `iterateTask` and `submitWorkflowTask`. A non-administrator receives only what their permissions allow, so a client must tolerate a thinner response. `parameters` can hold credentials and personal data. Do not log it and do not display it without an explicit reveal. ## Example Usage ### Step a run one activity at a time ```typescript let instance = { workflow_id: workflowId, entity: { employeeId: "emp-42" } }; for (;;) { const step = await entityService.iterateOnce({ instance }, token, tenantCode); instance = { ...step.instance, workflow_id: workflowId }; if ((instance.status & (4 | 8 | 32)) !== 0) break; // Completed, Cancelled or Error if (instance.currentTask) break; // parked on a person } ``` ### Step through a user task ```typescript const step = await entityService.iterateOnce( { instance: { ...instance, workflow_id: workflowId, /* task outcome */ } }, token, tenantCode, ); ``` The run continues to the next activity and halts there, rather than running to completion. ## Additional Information - Omitting `instance.workflow_id` is the difference between a step and a rejected call. - Step-by-step traces are also written to `GsbWfLog`, but only when the workflow's `enableLog` flag is on. With it off a healthy run records nothing. - This operation is an external side effect on live data. It is disabled by default and requires explicit approval before it can be invoked through the CLI or an MCP client. --- # submitWorkflowTask() — Workflow Service Source: /api/submitworkflowtask # SubmitWorkflowTask Operation ## General Description The `submitWorkflowTask` operation is how a **standard user** acts on the workflow task assigned to them: approve it, reject it, add a note, or hand it to someone else. ## Detailed Description `submitWorkflowTask` posts to `/api/workflow/submitWorkflowTask`. It is the end-user counterpart to the operator and debugging calls: | Operation | Who calls it | What it does | |---|---|---| | [submitWorkflowTask](/api/submitworkflowtask) | the task's assignee | Submits a decision on that user's own task and lets the workflow continue | | [iterateTask](/api/iteratetask) | integrations, server functions | Completes a user task programmatically and continues the rest of the workflow | | [iterateOnce](/api/iterateonce) | administrators | Advances exactly one activity and halts, for stepping and debugging | The decision is expressed as a **transition choice**: `selection_id` is the id of the `GsbTransition` leaving the current activity. "Approve" and "Reject" are not statuses — they are two outgoing transitions of the same approval activity, so a client must read the available transitions and present them as the choices. Reassignment is supported by the same call: instead of choosing a transition, the task is handed to another user or position and the instance stays parked on the same activity. ## Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | request | object | Yes | The task submission, forwarded to the backend without transformation. | | token | string | No | Authentication token for the request. Falls back to the configured credentials when omitted. | | tenantCode | string | No | Tenant whose data to operate on. Falls back to the tenant encoded in the token or the configured tenant. | ### Request Object Structure The request carries a **task object**. | Property | Type | Required | Description | |----------|------|----------|-------------| | task | object | Yes | The task being submitted. | | task.id | string | Yes | Id of the task assigned to the caller. | | task.selection_id | string | Yes* | Id of the `GsbTransition` the user chose. Required for a decision; omitted when reassigning. | | task.note | string | No | The user's comment, recorded on the human trail (`GsbWfInstanceHistory`). | Reassignment fields are part of the same task object. Their exact names are **not yet verified against a live tenant** and are deliberately not invented here; verify before relying on them. ## Response ### Success Response ```json { "success": true, "instance": { "id": "string", "workflow_id": "string", "activity_id": "string", "status": 0, "result": "string", "currentTask": {} }, "status": 200 } ``` When the caller is an administrator, `instance` is the full envelope described in [iterateOnce](/api/iterateonce). A standard user receives the fields their permissions allow. After a successful submission the instance has either moved on to the next activity, finished, or parked again on the next human task. Read `activity_id` and `status` to tell which. ### Error Response ```json { "success": false, "message": "Error message describing what went wrong" } ``` | Condition | `message` | |---|---| | The task is not assigned to the caller | Authorization error text from the backend | | The task was already completed or reassigned | Backend error describing the stale task | | `selection_id` is not a transition leaving the current activity | Backend validation error | | Token missing, expired, or issued for another tenant | Authentication error text from the backend | ## Example Usage ### Approve a task ```typescript const result = await entityService.submitWorkflowTask( { task: { id: taskId, selection_id: approveTransitionId, note: "Approved — cover arranged for the period.", }, }, token, tenantCode, ); if (!result.success) { throw new Error(result.message); } ``` ### Offer the real choices, not hardcoded ones ```typescript const transitions = new QueryParams("GsbTransition") .filter("from_id", instance.activity_id) .select(["id", "title", "name", "route"]) .take(20); const options = await entityService.query(transitions, token, tenantCode); ``` Each returned transition is one button in the task UI, and its `id` is the `selection_id` to submit. ## Additional Information - The note is the only place a human explanation is recorded. Treat it as auditable text, not as a scratch field. - A task is a decision on **one** activity. Anything that needs to move a whole run belongs to [iterateTask](/api/iteratetask) or, for administrators, [iterateOnce](/api/iterateonce). - Never submit a task on a user's behalf from model-generated code without an explicit confirmation step. --- # addResourcePack() — Application Service Source: /api/addresourcepack # addResourcePack Builds a new immutable pack from a `GsbResourcePackTmpl`. The verified tenant action calls `POST /api/app/addResourcePack` after explicit confirmation. ## Request ```json { "note": "ERP source release", "version": "1.0.0", "templateId": "resource-pack-template-id", "moduleFields": ["entityDefs", "workflows", "functions"] } ``` | Field | Type | Description | |---|---|---| | `note` | string | Operator-authored release note stored with the built pack. | | `version` | string | Version assembled by the action from its major, minor, and build controls. | | `templateId` | string | Id of the authoritative `GsbResourcePackTmpl` being built. | | `moduleFields` | string[] | Optional explicit asset-family allowlist. Omission includes every supported family; an empty array includes none. | ## Verified action source The live `GsbResourcePackTmpl.actionsStr` decodes to **Start New Pack**, which opens widget `7d32171c-fec4-4f75-8756-98c47c1553a4`. Its nested **Start Packing** action defines `apiEndPoint: "/app/addResourcePack"` and transposes the current template into the request above. Direct HTTP callers use the platform's `/api` prefix. This is an operator packaging mutation. The server must authorize the template and tenant; `templateId` is selection input, not authority. Canonical release builders send an explicit `moduleFields` allowlist so a backend default change cannot alter the artifact. Supported values are `uiResources`, `permissions`, `workflows`, `entityDefs`, `enums`, `widgets`, `propertyDefs`, `pages`, `widgetTemplates`, `roles`, `positions`, `groups`, `departments`, `codeGenerators`, `codeLibraries`, `docTemplates`, `layouts`, `importTemplates`, `plugins`, `userQueries`, `actions`, `themes`, `functions`, `mlDictionary`, `uiServices`, and `recurringJobs`. When `entityDefs` is included and `widgets` is omitted, canonical release validation checks `defaultCreateForm_id`, `defaultViewForm_id`, and `defaultUpdateForm_id`. Mutable references to excluded widgets must be cleared before the build. Immutable system references remain inventoried as deferred evidence while launch editions ship no low-code forms. A reference that resolves to neither an included nor an excluded widget fails the build; it must not emit an unknown reference. --- # installResourcePack() — Application Service Source: /api/installresourcepack # installResourcePack Installs a built resource pack into an authorized target tenant. The verified transport is `POST /api/app/installResourcePack`; the request is asynchronous when `runAsync` is true. ## Request ```json { "skipUserModified": true, "sourceTenant": "dev1", "templateName": "erp", "runAsync": true, "tenantCode": "customer-workspace" } ``` | Field | Type | Description | |---|---|---| | `skipUserModified` | boolean | Preserves supported user-modified assets when true. The action sets this to the inverse of **Overwrite My Changes**. | | `sourceTenant` | string | Authorized tenant that owns the built pack. | | `packId` | string | Optional exact built `GsbResourcePack` id. | | `templateId` | string | Optional `GsbResourcePackTmpl` id used for latest-pack resolution. | | `templateName` | string | Optional `GsbResourcePackTmpl.name` used for latest-pack resolution. | | `runAsync` | boolean | Returns a job id instead of waiting for installation. | | `tenantCode` | string | Authorized installation target. | Select one source artifact. `packId` pins an exact built pack. When `packId` is absent, `templateId` or `templateName` resolves the latest pack built from that template. Do not send both template selectors. Use `packId` whenever reproducibility requires an exact artifact rather than the latest build. ## Response and status An accepted asynchronous request returns a `jobId`. Poll [getJobStatus](/api/getjobstatus) until the task reaches a terminal state. Treat installation as a privileged mutation: the server must authorize the source and target tenants; a browser-supplied tenant code is never authority. ## Entity actions The live `GsbResourcePack.actionsStr` decodes to **Install Pack**, which opens widget `07dc76f9-cd99-4b2b-9f87-13ff51d9f3c8`. Its nested **Start Install** action defines `apiEndPoint: "/app/installResourcePack"`, sends the request above, then polls `/api/task/getJobStatus` every five seconds until `Succeeded`, `Failed`, or `Deleted`. --- # getJobStatus() — Application Service Source: /api/getjobstatus # getJobStatus Reads the state of an asynchronous platform task. Use it after operations such as [installResourcePack](/api/installresourcepack) return a `jobId`. ## Endpoint `POST /api/task/getJobStatus` ```json { "jobId": "returned-job-id" } ``` The caller must use the same authorized tenant context that created or owns the task. A job identifier is a correlation value, not authorization. ## Polling Poll with bounded backoff and stop at a terminal success or failure state. Do not treat request acceptance as installation success, and do not start a duplicate installation merely because a task is still pending. Surface the server's terminal failure without logging tokens, credentials, or tenant-sensitive payloads. ## Cancellation Where the task type supports cancellation, use `POST /api/task/cancelTask` with the same `jobId`. Cancellation is a separate mutation and may be rejected after the task commits.