• 首页
  • 产品中心
    • 数式Oinone四大产品

      低代码开发平台无代码开发平台集成开发平台AI大模型开发
    • 数式Oinone体系能力

      用户与组织权限管理文件管理消息中心国际化业务审计
    • 数式Oinone核心产品特性

      低无一体面向软件公司场景无限制应用级扩容可分可合
  • 服务中心
    • 客户服务

      预约演示方案咨询私有部署找人定制
    • 开发者

      问答下载
    • Oinone学院

      社区学习

    《精讲面向软件公司的低代码平台——以Oinone为例》

  • 合作伙伴
    渠道申请伙伴名录专家库
  • 关于数式
0571-88757863

GraphQL Service


Tips

This chapter covers all content related to GraphQL Service in Oinone Kunlun. In addition to this chapter, you can also find content about GraphQL in the following chapters:

  • The GraphQL Protocol section in "Mastering the Frontend Framework - Frontend Framework Overview"
  • The GraphQL Protocol section in Customize GraphQL Request

The GraphQL protocol is the standard protocol used for frontend-backend interaction in Oinone. Understanding and learning GraphQL syntax, generation rules, and usage methods is of great significance for learning Oinone.

I. GraphQL Protocol

(I) What is GraphQL?

GraphQL is a graph query language represented by structured text. It is not tied to any specific database or storage engine; instead, it is supported by your existing code and data.

For more reference materials:

  • Oinone Gateway Protocol API
  • How to GraphQL
  • GraphQL Introduction

(II) GQL vs. REST

From the perspective of requests initiated by the browser, a GQL request is essentially a request transmitted in JSON format using the POST method. This is almost identical to how RESTful requests are transmitted in JSON format.

The only differences are:

  • GQL requests use fixed-structure requests and flexible responses, where the response structure is defined at the time of the request.
  • RESTful requests use arbitrary-structure requests and arbitrary-structure responses, where the response structure is defined on the backend.

For more reference materials:

  • What's the Difference Between GraphQL and REST?

(III) Why Does Oinone Choose GQL?

Since the development of web frameworks, various frontend-backend architectures have emerged. Oinone has referenced the typical frontend architecture MVVM (Model-View-ViewModel) and combined it with the "metadata-driven" feature. Eventually, it was found that the standardized structure and server-side routing capabilities provided by GQL make it a suitable frontend-backend protocol for Oinone.

1. Everything Revolves Around the Model

In the practice of "Management Information Systems (MIS)", we have found that any list, form, or detail page can be simply abstracted into the corresponding concepts in the MVVM architecture:

  • DSL: The model metadata contained in a view, which corresponds to the runtime model and fields (i.e., the first "M" in MVVM).
  • View: This concept is easy to understand—any page can consist of a main view, and views can be arranged in parallel, nested, etc. The view is also a key concept for frontend metadata isolation.
  • Data Source: In Oinone Kunlun, built-in variables such as dataSource and activeRecord correspond to the ViewModel. It adheres to business types but not entirely; the runtime ViewModel can separate actual values and submitted values, enabling diverse display and submission methods.

2. Model Functions

Let’s take a look at the backend definition structure of viewAction#load:

// Model code (model): ViewAction.MODEL_MODEL = base.ViewAction
// Model name (modelName): viewAction
@Model.model(ViewAction.MODEL_MODEL)
public class PageLoadAction {
    /**
     * Obtain page metadata
     *
     * @param request Request parameters; required parameter: id
     * @return Homepage metadata
     */
    @Function(openLevel = FunctionOpenEnum.API)
    @Function.Advanced(type = FunctionTypeEnum.QUERY, displayName = "Load Page")
    public ViewAction load(ViewAction request) {
        // Metadata loading process
        return request;
    }
}

Only key content is listed here. As a frontend developer, you only need to understand the corresponding relationships, which are very similar to TypeScript Class.

Secondly, frontend-backend communication is also crucial—if there is a "language barrier", communication efficiency will be greatly reduced.

Finally, let’s compare the viewAction#load request initiated by the browser and understand the meaning of each parameter in the standard format.

(IV) Standard Request Format

In Oinone, all requests are defined in the following standard format, and its rules are completely consistent with the model and function metadata.

${query/mutation} {
  ${modelName}${Query/Mutation} {
    ${functionName} (${arg1Name}: ${arg1Value}) {
      ${responseParameters}
    }
  }
}

Any GraphQL request will only be one of two types: Query or Mutation. This depends on how the backend service defines the function. Generally, we require that requests that do not manipulate data use Query, while other operations (create/update/delete, etc.) use Mutation.

Parameter Meanings

  • query/mutation: Specifies the GraphQL request type; defaults to query.
  • modelName: The name of the model.
  • Query/Mutation: Uses different suffixes based on the GraphQL request type.
  • functionName: The name of the function.
  • arg1Name/arg1Value: Specifies the input parameters of the function; multiple parameters can be used, separated by commas.
  • responseParameters: Defines the response parameters. Starting from the current model, the response format of the interface is defined in a "graph" form. Parameters can be separated by line breaks or commas. When a field is an object or array, use "{}" to continue defining fields of the associated model.

(V) Standard Response Format

{
    "data": {
        "${modelName}${Query/Mutation}": {
            "${functionName}": [
                {
                    "id": "1"
                }
            ]
        }
    },
    "errors": [],
    "extensions": {
        "success": true
    }
}

In Oinone, any GraphQL request is returned to the frontend in this format.

Parameter Meanings

  • data: Fixed key.
  • modelName: Model name; consistent with the request parameter.
  • Query/Mutation: Uses different suffixes based on the GraphQL request type; consistent with the request parameter.
  • functionName: Function name; consistent with the request parameter.
  • errors: Possible error information.
  • extensions: Extended information.

(VI) Variables

A GraphQL request body consists of two parts: query and variables. The content in the previous section is transmitted using the query parameter, and the other part (variable values) is transmitted via variables.

query

${query/mutation} (${var1Name}: ${var1Type} [?=${var1DefaultValue}]) {
  ${modelName}${Query/Mutation} {
    ${functionName} (${arg1Name}: ${var1Name}) {
      ${responseParameters}
    }
  }
}

variables

{
  "var1Name": "var1Value"
}
  • var1Name/var1Type: Defines the variable name and type.
  • var1DefaultValue: Defines the default value of the variable (optional).
  • var1Value: The value corresponding to var1Name defined in variables.

Example

query

query ($model: String, $name: String, $needCompileView: Boolean = true, $needCompileMask: Boolean = true) {
  viewActionQuery {
    load(
      request: {model: $model, name: $name, needCompileView: $needCompileView, needCompileMask: $needCompileMask}
    ) {
      id
    }
  }
}

variables

{
  "model": "resource.ResourceCountryGroup",
  "name": "resource#国家分组"
}

Tips

In Oinone, after a GQL request is sent to the backend, it undergoes structural parsing. Generally, we consider that the parsing overhead is far lower than the convenience brought by routing flexibility. To minimize parsing overhead, we cache the GQL parsing results. However, due to parameter variables, cache hits are completely impossible—and using Variables can effectively solve this cache hit issue.

It is worth noting that although using Variables can improve cache hit rates, the number of parameters should not be excessive. Too many parameters will increase later maintenance costs. Generally, we recommend using variables in core requests with few and fixed parameters to improve overall system performance. For example, the viewAction#load interface is a typical use case.

(VII) Fragments

When a GraphQL request is very long or contains repeated definitions, we usually define it as a Fragment so that it can be reused in multiple places. Taking the ViewAction#load interface as an example:

query ($model: String, $name: String, $needCompileView: Boolean = true, $needCompileMask: Boolean = true) {
  viewActionQuery {
    load(
      request: {model: $model, name: $name, needCompileView: $needCompileView, needCompileMask: $needCompileMask}
    ) {
      ...ViewAction
    }
  }
}

fragment ViewAction on ViewAction {
	id
  model
  modelName
  modelDefinition {
    ...Model
  }
  resModel
  resModelDefinition {
    ...Model
  }
}

fragment Model on ModelDefinition {
  id
  model
  name
}

Tips

Flexible use of Fragments can make code more concise.

(VIII) Unified Terminology

Taking the "Obtain Global Configuration" interface as an example, we use expressions like "appConfig#queryListByWrapper interface" to locate an interface request. Compared with Chinese descriptions, this is more accurate and intuitive.

Let’s get used to this representation method:

  • topBarUserBlock#construct: Top Bar - Obtain User Avatar and Actions
  • viewAction#load: Load Page Metadata
  • model#loadModelField: Obtain Model Field Metadata by Model Code
  • module#load: Obtain by Module Code
  • resourceCountryGroup#queryPage: Query Country Groups by Pagination

II. Initiating Requests with Visual Tools

Requests for any backend service can be initiated using visual tools. Similar tools include:

  • Insomnia: Click to Download
  • Postman: Click to Download

You can freely choose any visual tool that supports the GraphQL protocol.

1. Initiating Requests with Insomnia

2. Initiating Requests with Postman

3. Solving Login Issues with Visual Request Tools

When we initiated the "Obtain Global Configuration" request earlier, we noticed that there was no user login verification. This is because this interface still needs to be used on the login page, so it does not verify whether the user is logged in. However, this is not the case for all other interfaces—you can verify this with other requests in the browser.

So, how to log in? It’s actually very simple: we just need to initiate the login request (sent by the login page) in the request tool, like this:

mutation {
	pamirsUserTransientMutation {
		login(user: { login: "admin", password: "admin" }) {
			redirect {
				id
			}
			broken
			errorMsg
			errorCode
			errorField
		}
	}
}

Tips

The password field of the login interface allows plaintext transmission, which facilitates our debugging. However, on the actual page, the password is transmitted in ciphertext.

Tips

Request URL: Can be obtained from the value of Headers - General - Request URL in the browser.

Request Method: POST (used for most requests).

Obtaining the query parameter: Right-click the query, select "Copy value", and paste it directly into the Query section of the visual request tool.

Obtaining the variables parameter: Right-click the variables, select "Copy object", and paste it directly into the Variables section of the visual request tool.

4. Initiating Batch Requests

In the browser, we also find a request named "batch", which represents a batch request. It can process a batch of unrelated GraphQL requests at once and return the corresponding results separately.

In the request tool, we can initiate such requests using JSON format.

Tips

Batch requests leverage the automatic batching capability provided by the apollo-client toolkit. When using Promise asynchronously, requests for the same module within the same clock cycle will be merged and automatically combined into such batch requests.

For more content about HTTP Requests, please refer to: HttpClient Service

III. Using GraphQL in Oinone

(I) Starting from the Model

The model is the starting point of all functions. Let’s review the use of models in "Mastering the Frontend Framework" and start with the GanttDemoModel model.

The following is information about the model (GanttDemoModel) used in this chapter: Model Code: demo.gantt.GanttDemoModel Model Name: ganttDemoModel Module Name of the Model: demo Model Fields: (See the table below)

NameAPI NameField TypeMulti-valueLength (Single-value Length)
CodecodeText (String)No128
NamenameText (String)No128
Task Start DatetaskStartDateDate (Date)No-
Task End DatetaskEndDateDate (Date)No-

Tips

In most cases, the model name is automatically generated from the model code. The generation rule is: split the model code by "." , take the last segment, and convert it to camelCase format—just like the model information shown above.

(II) Built-in Functions

Any model inherited from IdModel has some basic built-in CRUD functions. As a frontend developer, you don’t need to know too much about backend knowledge. To facilitate the description of subsequent content, let’s briefly understand them:

GQL TypeFunction Code (fun)Function Name (name)Description
QueryconstructconstructConstructor; initializes the page;
queryPagequeryPagePagination query;
queryOnequeryOneSingle data query; Entity parameter;
queryListByWrapperqueryListByWrapperConditional list query;
queryByWrapperqueryOneByWrapperConditional single data query;
countByWrappercountByWrapperCount data by conditions;
countcountCount data; Entity parameter;
MutationcreatecreateCreate function;
updateupdateUpdate function;
deletedeleteDelete function;

Tips

Here, we list some commonly used default functions that can be called by the frontend. All functions are ultimately initiated via the Function Name (name). Note a special case here:

  • queryByWrapper and queryOneByWrapper call the same function, but their fun and name are different.

For more details about function input parameters, output parameters, etc., please refer to: ORM API - Common ORM Methods

(III) GQL Syntax with countByWrapper as an Example

In the chapter "Exploring the Frontend Framework - Building a Dashboard", we initially learned how to initiate backend requests. However, we did not elaborate on the GQL syntax and parameter content corresponding to the built-in countByWrapper function. Let’s take a look at the GQL syntax of this function:

{
  ganttDemoModelQuery {
    countByWrapper(queryWrapper: {rsql: "1==1"})
  }
}

In this GQL, the countByWrapper function of the ganttDemoModel model is called. This function only includes one input parameter (an object named queryWrapper), and this object only passes a property named rsql.

In GQL, empty return values and basic data types do not require declaring response parameters. The return value of countByWrapper is a number, so no response parameters are declared in this GQL.

It is worth mentioning that for numeric types, the backend type definitions include Integer, Long, Float, Double, and BigDecimal. When transmitted to the frontend, due to differences in language precision, to prevent precision loss, Long and BigDecimal types are returned to the frontend as string types.

Tips

In the declaration of the countByWrapper function, its return value is of Long type. This means we need to convert it to a number type using the Number() function before returning. Although this processing has certain flaws, in existing scenarios, the maximum integer value returned by countByWrapper generally does not exceed the safe integer value of JavaScript. If the business scenario clearly exceeds the safe value range, tools like bignumber.js (which encapsulate data types) should be used for processing.

For more content about type mapping, please refer to: Data Type Mapping

Theory: Function Definition and GQL Syntax As the first example, let’s first understand the mapping relationship between function definitions and GQL syntax. This will help us better understand the subsequent examples.

The Oinone backend is written in Java, which is very similar to the TypeScript syntax we use. Learning Java syntax may be difficult for frontend developers, so let’s use TypeScript syntax to see what defining such a function would look like:

import { QueryWrapper } from '@oinone/kunlun-dependencies';

/* Built-in data type definition
interface QueryWrapper {
  rsql?: string;
  queryData?: ActiveRecord;
}
*/

export class GanttDemoModelAction {
  public countByWrapper(queryWrapper: QueryWrapper): number {
    // Ignored code
  }
}

We have simplified many declaration details here, focusing on key points such as the function name, input parameters, and output parameters. It is obvious that this corresponds one-to-one with GQL syntax.

In Oinone, there are backend-consistent type declarations for the input and output parameters of built-in functions. We can also use these type declarations in practice. In this example, queryWrapper is the input parameter name, QueryWrapper is the input parameter type of the built-in function, and rsql is a property value in the QueryWrapper type.

Tips

For more content about RSQL, please refer to: RSQL Service

(IV) GQL Syntax with queryListByWrapper as an Example

{
  ganttDemoModelQuery {
    queryListByWrapper(queryWrapper: {rsql: "1==1"}) {
      id
      code
      name
      taskStartDate
      taskEndDate
    }
  }
}

In this GQL, the queryListByWrapper function of the ganttDemoModel is called. This function includes only one input parameter (an object named queryWrapper), and this object only passes a property named rsql.

In the response, we declare that we need to retrieve five fields of the ganttDemoModel for return. It is worth noting that whether the response data is a single object or an array, GQL response parameters are always flattened—there is no need to distinguish between specific data types.

The corresponding function definition is as follows:

import { QueryWrapper } from '@oinone/kunlun-dependencies';

export interface GanttDemoModel {
  id: string;
  code: string;
  name: string;
  taskStartDate: string;
  taskEndDate: string;
}

export class GanttDemoModelAction {
  public queryListByWrapper(queryWrapper: QueryWrapper): GanttDemoModel[] {
    // Ignored code
  }
}

Unlike the previous example, the return value here is no longer a primitive type, but an array of model objects. Therefore, the response parameters we declare in GQL correspond one-to-one with the model fields.

Notably, for datetime types, the backend transmits data to the frontend as standard-format datetime strings. Thus, the taskStartDate and taskEndDate properties declared in our type definition are of string type, not Date type.

Tips

Functions are defined by the backend. As the caller, the frontend uses GQL to specify the model, function, input parameters, and output parameters for the call.

For more content about RSQL, please refer to: RSQL Service

For more content about type mapping, please refer to: Data Type Mapping

(V) GQL Syntax with queryPage as an Example

In the previous example, we introduced GQL syntax for functions with a single parameter. Below, we use queryPage to explain multiple input parameters and return value structures.

{
  ganttDemoModelQuery {
    queryPage(page: {currentPage: 1, size: 15}, queryWrapper: {rsql: "1==1"}) {
      content {
        id
        code
        name
        taskStartDate
        taskEndDate
      }
      totalPages
      totalElements
    }
  }
}

The corresponding function definition is as follows:

import { QueryPageResult, QueryPagination, QueryWrapper } from '@oinone/kunlun-dependencies';

export interface GanttDemoModel {
  id: string;
  code: string;
  name: string;
  taskStartDate: string;
  taskEndDate: string;
}

/* Built-in data type definition
interface QueryPagination {
  currentPage: number;
  size: number;
  sort?: { orders: QuerySort[] };
  groupBy?: string;

  totalPages?: number;
  totalElements?: number;
}

interface QueryPageResult<T> {
  content: T[];  // Paginated data list
  totalPages: number;  // Total number of pages
  totalElements: number;  // Total number of data entries
}
*/

export class GanttDemoModelAction {
  public queryPage(page: QueryPagination, queryWrapper: QueryWrapper): QueryPageResult<GanttDemoModel> {
    // Ignored code
  }
}

GQL syntax maps parameters by parameter name, not by parameter order:

  • If the name of the queryWrapper input parameter changes, the queryWrapper in the corresponding GQL syntax must also be renamed to the same name for the request to work properly.
  • If the order of page and queryWrapper is swapped, the request result remains identical.

Tips

For more content about RSQL, please refer to: RSQL Service

(VI) GQL Syntax with create as an Example

All previous examples use Query-type GQL syntax, and we omitted the query keyword by default. In this example, we will look at Mutation-type GQL syntax.

mutation {
  ganttDemoModelMutation {
    create(data: {code: "test", name: "test"}) {
      id
      code
      name
      taskStartDate
      taskEndDate
    }
  }
}

The corresponding function definition is as follows:

export interface GanttDemoModel {
  id: string;
  code: string;
  name: string;
  taskStartDate: string;
  taskEndDate: string;
}

export class GanttDemoModelAction {
  public create(data: Partial<GanttDemoModel>): GanttDemoModel {
    // Ignored code
  }
}

/*
Partial<GanttDemoModel> is equivalent to:
{
  id?: string;  // Optional property
  code?: string;
  name?: string;
  taskStartDate?: string;
  taskEndDate?: string;
}
*/

During creation, we are allowed to pass an object without the primary key (id). The primary key (id) is returned in the response to verify that the data was created successfully.

Tips

Partial is a TypeScript utility type that converts all properties of an object type into optional properties.

(VII) GQL Syntax with update as an Example

The syntax is nearly identical to the create function, but it is important to emphasize that the primary key (id) must be passed for a successful update. In this example, we can use template syntax to pass the incoming primary key (id) into the GQL input parameters and update the name to newName.

const gql = `mutation {
  ganttDemoModelMutation {
    update(data: {id: "${id}", name: "newName"}) {
      id
      code
      name
      taskStartDate
      taskEndDate
    }
  }
}`

The corresponding function definition is as follows:

import { IdModel } from '@oinone/kunlun-dependencies';

export interface GanttDemoModel {
  id: string;
  code: string;
  name: string;
  taskStartDate: string;
  taskEndDate: string;
}

/* Built-in data type definition
interface IdModel {
  id?: string;
}
*/

export class GanttDemoModelAction {
  public update(data: Partial<GanttDemoModel> & Required<IdModel>): GanttDemoModel {
    // Ignored code
  }
}

/*
Required<IdModel> is equivalent to:
{
  id: string;  // Mandatory property
}
*/

Tips

The rule "the primary key (id) must be passed for a successful update" is the update logic provided by the default data manager function. It can be adjusted based on actual business scenarios.

Required is a TypeScript utility type that converts all properties of an object type into mandatory (non-nullable) properties. Alternatively, an anonymous type like { id: string } can be used directly here.

Update logic of the default data manager function:

  • Update condition: Update the object using the primary key.
  • Update data: Passed values determine the updated content; unpassed values remain unchanged.
  • Associated object creation/update:
    • Many-to-one: Associated fields must be flattened in the main model; passing objects is meaningless.
    • One-to-many: Must be passed as an array of objects; the primary key (id) is mandatory; unpassed values remain unchanged.
    • Many-to-many: Must be passed as an array of objects; the primary key (id) is mandatory; other passed values are meaningless.

For more content about association types, please refer to: ORM API - Relationship Types

(VIII) GQL Syntax with delete as an Example

Earlier, we introduced GQL syntax for functions with object-type input parameters. The delete function supports single or multiple data deletion and accepts an array of objects as input. By default, only the primary key (id) needs to be passed to complete the deletion.

GQL Syntax for Deleting a Single Object

const gql = `mutation {
  ganttDemoModelMutation {
    delete(dataList: [{id: "${id}"}]) {
      id
      code
      name
      taskStartDate
      taskEndDate
    }
  }
}`

GQL Syntax for Deleting Multiple Objects

const ids: string[] = []; // Array of primary keys to delete

const gql = `mutation {
  ganttDemoModelMutation {
    delete(dataList: [${ids.map((id) => `{id: "${id}"}`).join(', ')}]) {
      id
      code
      name
      taskStartDate
      taskEndDate
    }
  }
}`

As shown, deleting multiple objects simply repeats the structure of each object in the array, and finally joins them with commas to form an object array.

The corresponding function definition is as follows:

export interface GanttDemoModel {
  id: string;
  code: string;
  name: string;
  taskStartDate: string;
  taskEndDate: string;
}

export class GanttDemoModelAction {
  public delete(dataList: Required<IdModel>[]): GanttDemoModel[] {
    // Ignored code
  }
}

Here, we do not use the full model for the input parameter, but instead use the IdModel type (which only contains the id field). This type declaration enables static type checking for the caller when passing data, preventing parameter errors and reflecting the backend function definition.

Tips

The input parameter name of the delete function is dataList (an array of objects), which differs from the create and update functions.

The use of IdModel assumes that the backend defines the GanttDemoModel by inheriting IdModel.

Both the frontend and backend have IdModel and CodeModel, with identical inheritance relationships and field definitions.

(IX) Data Type Mapping

In Oinone, there are clear standards and specifications for handling different business field types in GQL syntax, input parameters, and return values. This is also a key part of the frontend-backend interaction boundary. Understanding these data type relationships is crucial for using Oinone.

1. Basic Data Types

The table below shows the mapping between business field types, backend Java types, frontend TypeScript types, and GQL input syntax (excluding multi-value types, composite types, and relationship types):

Business Field TypeJava TypeTypeScript TypeGQL Input SyntaxDescription
IntegerIntegernumberdata: 123No double quotes
Longstringdata: "123"With double quotes (prevents precision loss)
FloatFloatnumberdata: 123.12No double quotes
Doublestringdata: "123.12"With double quotes (prevents precision loss)
AmountBigDecimalstringdata: "123.12"Returned as a text type with 6 decimal places
BooleanBooleanbooleandata: true/falseNo double quotes; only `true` or `false` are allowed
TextStringstringdata: "test"With double quotes
Mobile PhoneStringstringdata: "11111111111"Same as Text type
EmailStringstringdata: "admin@shushi.pro"Same as Text type
Multi-line TextStringstringdata: "test"Same as Text type
Rich TextStringstringdata: "test"Same as Text type (stores HTML strings)
DatetimeDatestringdata: "2019-04-09 13:00:00"Formatted as "YYYY-MM-DD HH:mm:ss"
DateDatestringdata: "2019-04-09"Formatted as "YYYY-MM-DD"
TimeDatestringdata: "13:00:00"Formatted as "HH:mm:ss"
YearDatestringdata: "2019"Formatted as "YYYY"

2. Enum Types (Data Dictionaries)

Enums are a special data type in Oinone, containing four properties: name (logical identifier), value (storage value), displayName (user-facing name), and help (tooltip text).

Let’s take an enum with different name and value as an example (base.Active):

NameValueDisplayNameHelp
ACTIVEtrueActiveActive status
INACTIVEfalseInactiveInactive status

In GQL, frontend-backend interaction uniformly uses name for both input and output.

For example, if we add a status field to the GanttDemoModel and set its type to ActiveEnum, the corresponding GQL syntax would be status: ACTIVE—using the name as the value without double quotes.

The TypeScript type definition for this scenario is:

import { ActiveEnum } from '@oinone/kunlun-dependencies';

export interface GanttDemoModel {
  id: string;
  code: string;
  name: string;
  taskStartDate: string;
  taskEndDate: string;
  status: ActiveEnum; // Enum type
}

/* Built-in data type definition
enum ActiveEnum {
  ACTIVE = 'ACTIVE', // Name and value are consistent (common practice)
  INACTIVE = 'INACTIVE'
}
*/

Tips

In TypeScript, enum definitions corresponding to Oinone enums usually use consistent name and value to simplify data transmission.

As a frontend developer, you do not need to care about how the value is used in the backend. However:

  • displayName is the user-facing option shown in UI components (e.g., dropdowns).
  • help is typically used as tooltip text when the user hovers over the option.

These two preset values may behave differently in different components.

3. Map Data Type (Key-Value Pairs)

Key-value pairs are usually stored in JSON format. During frontend-backend interaction, GraphqlHelper#serializableObject (for JSON objects) or GraphqlHelper#serializableObjectArray (for JSON object arrays) must be used for processing.

When passing data to GQL, we can serialize objects using template syntax:

import { GraphqlHelper } from '@oinone/kunlun-dependencies';

const jsonData = {
  "key1": "value1",
  "key2": "value2"
}

const gql = `mutation {
  ganttDemoModelMutation {
    update(
      data: {
        id: "${id}", 
        jsonData: "${GraphqlHelper.serializableObject(jsonData)}", 
        jsonArrayData: "${GraphqlHelper.serializableObjectArray([jsonData, jsonData])}"
      }
    ) {
      id
      code
      name
      taskStartDate
      taskEndDate
      jsonData
      jsonArrayData
    }
  }
}`

4. Multi-Value Types for Basic Data Types

To pass multi-value data, simply convert any single-value type into an array format. The format of each single value in the array is identical to that of the basic data type.

Array format: [${value1}, ${value2}, ${value3}...]

Business Field TypeJava TypeTypeScript TypeGQL Input Syntax
IntegerList<Integer>number[]data: [1, 2, 3]
List<Long>string[]data: ["1", "2", "3"]
FloatList<Float>number[]data: [1.12, 2.12, 3.12]
List<Double>string[]data: ["1.12", "2.12", "3.12"]
TextList<String>string[]data: ["test1", "test2", "test3"]

(X) Data Submission for Relationship Types

1. Many-to-One (M2O)

Field Definition

Assume the GanttDemoModel has a task field that establishes a many-to-one relationship with the GanttTask model. (Here, we only need to know that GanttTask contains an id field to establish the relationship between the two models.)

NameAPI NameField TypeRelated Model (references)Relation Field (relationFields)Reference Field (referenceFields)
TasktaskMany-to-One (M2O)GanttTasktaskIdid
Task IDtaskIdInteger

Update Relationship via Relation Field taskId

const gql = `mutation {
  ganttDemoModelMutation {
    update(data: {id: "${id}", taskId: "${taskId}"}) {
      id
      code
      name
      task {
        id
        code
        name
      }
    }
  }
}`

Since many-to-one relationships are established by storing a relation field in the main model, we can directly update the relationship by modifying the taskId field.

Update Relationship via Reference Field task

export interface GanttDemoModel {
  id: string;
  code: string;
  name: string;
  taskStartDate: string;
  taskEndDate: string;
  taskId?: string;
  task?: Partial<GanttTask>; // Note: M2O returns a single object, not an array
}

export interface GanttTask {
  id: string;
  code: string;
  name: string;
}

const gql = `mutation {
  ganttDemoModelMutation {
    update(data: {id: "${id}", task: {id: "${taskId}"}}) {
      id
      code
      name
      task {
        id
        code
        name
      }
    }
  }
}`

Although we do not directly pass taskId to the backend, the backend will automatically assign the id value from the task object to the taskId field of the main model (as M2O relationships rely on relation fields) and save it. This also updates the relationship between the two models.

Tips

If both taskId and task.id are provided, task.id takes precedence. In actual business use, we recommend using only one method to update relationships to avoid misunderstandings caused by conflicting rules.

2. One-to-Many (O2M)

Field Definition

Assume the GanttDemoModel has a children field that establishes a one-to-many relationship with the GanttDemoModel (self-reference).

NameAPI NameField TypeRelated Model (references)Relation Field (relationFields)Reference Field (referenceFields)
Children TaskschildrenOne-to-Many (O2M)GanttDemoModelidparentId
Parent Task IDparentIdInteger

Update Relationship via Reference Field children

const gql = `mutation {
  ganttDemoModelMutation {
    update(
      data: {
        id: "${id}", 
        children: [
          {code: "c1", name: "c1"}, 
          {code: "c2", name: "c2"}
        ]
      }
    ) {
      id
      code
      name
      taskStartDate
      taskEndDate
      children {
        id
        parentId
        code
        name
      }
    }
  }
}`

Since no primary key (id) is passed for the children objects, the backend will create new records for these objects and bind the relationship via the parentId field when the main model is updated.

For repeated updates:

  1. First query the original values of the children array.
  2. Modify the specific content and submit with the primary key (id) of the child objects.
    Failure to include the id will result in duplicate data errors.

A common way to dynamically splice GQL for both create and update scenarios is shown below:

export interface GanttDemoModel {
  id: string;
  code: string;
  name: string;
  taskStartDate: string;
  taskEndDate: string;
  parentId?: string;
  children?: Partial<GanttDemoModel>[];
}

const data: Partial<GanttDemoModel> & Required<IdModel>; // Data with main model ID

const gql = `mutation {
  ganttDemoModelMutation {
    update(data: {
      id: "${data.id}", 
      children: [
        ${
          data.children?.map((child) => 
            `{
              code: "${child.code}", 
              name: "${child.name}"
              ${child.id ? `, id: "${child.id}"` : ''} // Add ID only if it exists
            }`
          ).join(', ') || ''
        }
      ]
    }) {
      id
      code
      name
      taskStartDate
      taskEndDate
      children {
        id
        parentId
        code
        name
      }
    }
  }
}`

This uses template syntax to split the children array into individual object snippets. The id is appended dynamically (if present) and joined with commas to form a valid object array string.

Tips

While template syntax is convenient for splicing GQL for objects/arrays, complex splicing logic can become confusing.
Don’t worry—later in the tutorial, we will introduce GQL utility classes to simplify this dynamic construction process, making it clearer and more concise.

Default Update Logic for O2M Relationship Fields

  • If no primary key (id) exists in the reference field: Create new related objects and bind the relationship.
  • If a primary key (id) exists in the reference field: Update the existing related objects and refresh the relationship.
  • If an existing object is missing from the reference field: Unbind the relationship (the child object itself is not deleted unless configured otherwise).

3. Many-to-Many (M2M)

Field Definition

Assume the GanttDemoModel has a tasks field that establishes a many-to-many relationship with the GanttTask model via an intermediate table. (We only need to know GanttTask contains an id field to establish the relationship.)

NameAPI NameField TypeRelated Model (references)Intermediate Model (through)Relation Field (relationFields)Intermediate Relation Field (throughRelationFields)Reference Field (referenceFields)Intermediate Reference Field (throughReferenceFields)
Task SettasksMany-to-Many (M2M)GanttTaskGanttDemoModelRelGanttTaskidganttIdidtaskId

Intermediate Model (GanttDemoModelRelGanttTask)

NameAPI NameField Type
Gantt IDganttIdInteger
Task IDtaskIdInteger

Update Relationship via Reference Field tasks

The syntax is nearly identical to one-to-many. A common dynamic GQL splicing example is shown below:

export interface GanttDemoModel {
  id: string;
  code: string;
  name: string;
  taskStartDate: string;
  taskEndDate: string;
  tasks?: Partial<GanttTask>[];
}

export interface GanttTask {
  id: string;
  code: string;
  name: string;
}

const data: Partial<GanttDemoModel> & Required<IdModel>; // Data with main model ID

const gql = `mutation {
  ganttDemoModelMutation {
    update(data: {
      id: "${data.id}", 
      tasks: [
        ${
          data.tasks?.map((task) => 
            `{
              code: "${task.code}", 
              name: "${task.name}"
              ${task.id ? `, id: "${task.id}"` : ''} // Add ID only if it exists
            }`
          ).join(', ') || ''
        }
      ]
    }) {
      id
      code
      name
      taskStartDate
      taskEndDate
      tasks {
        id
        code
        name
      }
    }
  }
}`

Default Update Logic for M2M Relationship Fields

  • If no primary key (id) exists in the reference field: Create new related objects and insert association records into the intermediate table.
  • If a primary key (id) exists in the reference field: Update the existing related objects and refresh the intermediate table records.
  • If only the primary key (id) is passed: Skip updating the related object itself and only refresh the intermediate table association.
  • If an existing object is missing from the reference field: Delete the corresponding association record from the intermediate table (unbind the relationship, but do not delete the related object).

IV. Usage of GQL Utility Classes

In Oinone, while using the native HttpClient allows direct request initiation, its low-level capabilities are not very user-friendly without encapsulation. To address this, we provide utility classes to simplify GQL request workflows.

(I) GQLBuilder

Taking ganttDemoModel#queryListByWrapper as an example:

const MODULE_NAME = 'demo';
const MODEL_NAME = 'ganttDemoModel';

public static queryListByWrapperByGQLBuilder(): Promise<GanttDemoModel[]> {
  return GQL.query(MODEL_NAME, 'queryListByWrapper') // Initialize Query-type GQL
    .buildRequest((builder) => { // Build request parameters
      builder.buildObjectParameter('queryWrapper', (builder) => {
        builder.stringParameter('rsql', '1==1'); // Add string parameter "rsql"
      });
    })
    .buildResponse((builder) => { // Build response parameters
      builder.parameter('id', 'code', 'name', 'taskStartDate', 'taskEndDate');
    })
    .request(MODULE_NAME); // Initiate request with module name
}

When using raw GQL, string splicing for complex types (e.g., objects/arrays) can be error-prone. GQLBuilder handles these types natively, eliminating the need to manually manage splicing logic. This is the closest approach to using HttpClient directly while remaining user-friendly.

1. GQLBuilder API

GQL.query

  • Description: Starts building a Query-type GQL request.
  • Type: (modelName: string, name: string, fragments?: GQLFragment[]) => QueryGQL
  • Parameters:
    • modelName: Name of the target model.
    • name: Name of the target function.
  • Return Value: A GQLBuilder instance for Query-type requests.

GQL.mutation

  • Description: Starts building a Mutation-type GQL request.
  • Type: (modelName: string, name: string, fragments?: GQLFragment[]) => MutationGQL
  • Parameters:
    • modelName: Name of the target model.
    • name: Name of the target function.
  • Return Value: A GQLBuilder instance for Mutation-type requests.

GQL.fragment

  • Description: Starts building a GQL Fragment (reusable field set).
  • Type: (name: string, definition: string) => GQLResponseParameterBuilder
  • Parameters:
    • name: Name of the fragment.
    • definition: Target model/type for the fragment.
  • Return Value: A GQLBuilder instance for Fragment construction.

buildRequest (Request Parameter Construction)

MethodDescriptionSupported Types
stringParameterAdds string-type parameters (supports single values/arrays).`string
numberParameterAdds number-type parameters (supports single values/arrays).`number
booleanParameterAdds boolean-type parameters (supports single values/arrays).`boolean
enumerationParameterAdds enum-type parameters (uses enum name; supports single values/arrays).`string
mapParameterAdds Map/JSON-type parameters (auto-serializes; supports single values/arrays).`object
objectParameterAdds arbitrary object parameters (manual structure handling).Custom objects
buildObjectParameterStarts building a nested object parameter (chainable).Nested objects
buildArrayParameterStarts building an array parameter (chainable for array items).Arrays of objects/primitives
nullParameterAdds a null value parameter.null
nullArrayParameterAdds an empty array parameter.[]

Tips

For methods supporting single values/arrays:

  • null: Includes the parameter in GQL with a null value.
  • undefined: Omits the parameter from the GQL entirely.

buildResponse (Response Parameter Construction)

MethodDescription
parameterQuickly builds response parameters (supports nested objects via arrays).
buildParametersBuilds nested object response parameters (chainable).
fragmentParameterReferences a pre-defined Fragment in the response.

responseFragmentParameter

  • Description: Uses a Fragment as the entire response (avoids repeating field definitions). Must be used with buildFragment/mountFragment.

buildFragment

  • Description: Builds a Fragment for the current request (uses the same syntax as buildResponse). For static Fragments, use mountFragment instead.

mountFragment

  • Description: Attaches a pre-defined Fragment to the current request (for reusability).

request

  • Description: Initiates the GQL request to the backend.
  • Parameters: moduleName (name of the backend module to target).
  • Return Value: A Promise resolving to the response data.

toString

  • Description: Generates the raw GQL string (for debugging or custom request workflows).
  • Return Value: Raw GQL text.

2. Usage Example: queryPage

const MODULE_NAME = 'demo';
const MODEL_NAME = 'ganttDemoModel';

public static async queryPage(): Promise<QueryPageResult<GanttDemoModel>> {
  return GQL.query(MODEL_NAME, 'queryPage')
    .buildRequest((builder) => {
      // Build "page" parameter (pagination config)
      builder.buildObjectParameter('page', (pageBuilder) => {
        pageBuilder.numberParameter('currentPage', 1) // Page 1
                   .numberParameter('size', 15);      // 15 items per page
      });
      // Build "queryWrapper" parameter (filter conditions)
      builder.buildObjectParameter('queryWrapper', (wrapperBuilder) => {
        wrapperBuilder.stringParameter('rsql', '1==1'); // No filter (match all)
      });
    })
    .buildResponse((builder) => {
      // Build response parameters (supports nested objects via [field, [subfields]])
      builder.parameter(
        ['content', ['id', 'code', 'name', 'taskStartDate', 'taskEndDate']], // Nested "content" object
        'totalPages', // Total pages
        'totalElements' // Total items
      );
    })
    .request(MODULE_NAME);
}

For nested response objects (e.g., content containing task), use nested arrays to define subfields:

builder.parameter(
  [
    'content', 
    [
      'id', 'code', 'name',
      ['task', ['id', 'code', 'name']] // Nested "task" object under "content"
    ]
  ],
  'totalPages',
  'totalElements'
);

3. Usage Example: create

const MODULE_NAME = 'demo';
const MODEL_NAME = 'ganttDemoModel';

public static async create(): Promise<GanttDemoModel> {
  return GQL.mutation(MODEL_NAME, 'create') // Initialize Mutation-type GQL
    .buildRequest((builder) => {
      builder.buildObjectParameter('data', (dataBuilder) => {
        dataBuilder.stringParameter('code', 'test') // Set "code"
                   .stringParameter('name', 'test'); // Set "name"
      });
    })
    .buildResponse((builder) => {
      builder.parameter('id', 'code', 'name', 'taskStartDate', 'taskEndDate');
    })
    .request(MODULE_NAME);
}

4. Usage Example: update

const MODULE_NAME = 'demo';
const MODEL_NAME = 'ganttDemoModel';

public static async update(id: string): Promise<GanttDemoModel> {
  return GQL.mutation(MODEL_NAME, 'update')
    .buildRequest((builder) => {
      builder.buildObjectParameter('data', (dataBuilder) => {
        dataBuilder.stringParameter('id', id) // Mandatory: main model ID
                   .stringParameter('name', 'newName'); // Update "name"
      });
    })
    .buildResponse((builder) => {
      builder.parameter('id', 'code', 'name', 'taskStartDate', 'taskEndDate');
    })
    .request(MODULE_NAME);
}

5. Usage Example: delete

const MODULE_NAME = 'demo';
const MODEL_NAME = 'ganttDemoModel';

public static async delete(ids: string[]): Promise<GanttDemoModel[]> {
  return GQL.mutation(MODEL_NAME, 'delete')
    .buildRequest((builder) => {
      // Build "dataList" array (convert ID array to object array)
      builder.buildArrayParameter('dataList', ids, (itemBuilder, id) => {
        itemBuilder.stringParameter('id', id); // Each item only needs "id"
      });
    })
    .buildResponse((builder) => {
      builder.parameter('id', 'code', 'name', 'taskStartDate', 'taskEndDate');
    })
    .request(MODULE_NAME);
}

The buildArrayParameter method converts a simple ID array (string[]) into an array of objects (each with an id field). The second parameter is the source array, and the third parameter defines how to map each array item to a GQL object.

6. Usage of Enums (Data Dictionaries)

const MODULE_NAME = 'demo';
const MODEL_NAME = 'ganttDemoModel';

public static async update(id: string): Promise<GanttDemoModel> {
  return GQL.mutation(MODEL_NAME, 'update')
    .buildRequest((builder) => {
      builder.buildObjectParameter('data', (builder) => {
        builder.stringParameter('id', id).enumerationParameter('status', ActiveEnum.ACTIVE);
      });
    })
    .buildResponse((builder) => {
      builder.parameter('id', 'code', 'name', 'taskStartDate', 'taskEndDate');
    })
    .request(MODULE_NAME);
}

Note

When the enum value does not match the enum name required by the backend, it must be passed as a string.

7. Usage of Maps (Key-Value Pairs)

const MODULE_NAME = 'demo';
const MODEL_NAME = 'ganttDemoModel';

const jsonData = {
  "key1": "value1",
  "key2": "value2"
}

public static async update(id: string): Promise<GanttDemoModel> {
  return GQL.mutation(MODEL_NAME, 'update')
    .buildRequest((builder) => {
      builder.buildObjectParameter('data', (builder) => {
        builder
          .stringParameter('id', id)
          .mapParameter('jsonData', jsonData)
          .mapParameter('jsonArrayData', [jsonData, jsonData]);
      });
    })
    .buildResponse((builder) => {
      builder.parameter('id', 'code', 'name', 'taskStartDate', 'taskEndDate');
    })
    .request(MODULE_NAME);
}

For fields of Map data type, use the mapParameter method to pass values. It will automatically select one of the previously used serialization methods (GraphqlHelper#serializableObject or GraphqlHelper#serializableObjectArray) based on the input parameter type.

Note

The mapParameter method requires upgrading @oinone/kunlun-request to version 6.2.7 or higher. Users on lower versions can use stringParameter with the corresponding serialization method instead.

Example:

stringParameter('jsonData', GraphqlHelper.serializableObject(jsonData))

8. Usage of Primitive Type Arrays

All request parameter construction methods for primitive types support passing parameters in array format. When you need to pass an ids array as a request parameter, you can send the request as follows:

const MODULE_NAME = 'demo';
const MODEL_NAME = 'ganttDemoModel';

public static async update(id: string, ids: string[]): Promise<GanttDemoModel> {
  return GQL.mutation(MODEL_NAME, 'update')
    .buildRequest((builder) => {
      builder.buildObjectParameter('data', (builder) => {
        builder.stringParameter('id', id).stringParameter('ids', ids);
      });
    })
    .buildResponse((builder) => {
      builder.parameter('id', 'code', 'name', 'ids');
    })
    .request(MODULE_NAME);
}

9. Usage of Object Types

const MODULE_NAME = 'demo';
const MODEL_NAME = 'ganttDemoModel';

public static async update(id: string, taskId: string): Promise<GanttDemoModel> {
  return GQL.mutation(MODEL_NAME, 'update')
    .buildRequest((builder) => {
      builder.buildObjectParameter('data', (builder) => {
        builder.stringParameter('id', id)
          .buildObjectParameter('task', (builder) => {
            builder.stringParameter('id', taskId);
          });
      });
    })
    .buildResponse((builder) => {
      builder.parameter('id', 'code', 'name', ['task', ['id', 'code', 'name']]);
    })
    .request(MODULE_NAME);
}

For fields of object type, use the buildObjectParameter method to start constructing each parameter within the object.

10. Usage of Object Array Types

const MODULE_NAME = 'demo';
const MODEL_NAME = 'ganttDemoModel';

public static async update(data: Partial<GanttDemoModel> & Required<IdModel>): Promise<GanttDemoModel> {
  return GQL.mutation(MODEL_NAME, 'update')
    .buildRequest((builder) => {
      builder.buildObjectParameter('data', (builder) => {
        builder.stringParameter('id', data.id)
          .buildArrayParameter('children', data.children, (builder, child) => {
            builder
              .stringParameter('id', child.id)
              .stringParameter('code', child.code)
              .stringParameter('name', child.name);
          });
      });
    })
    .buildResponse((builder) => {
      builder.parameter('id', 'code', 'name', 'taskStartDate', 'taskEndDate', [
        'children',
        ['id', 'parentId', 'code', 'name']
      ]);
    })
    .request(MODULE_NAME);
}

For fields of object array type, use the buildArrayParameter method. The second parameter of this method is the value of each item in the array. Subsequent usage is identical to constructing a single object.

(II) GenericFunctionService

In practice, we found that initiating requests with GQL is more cumbersome compared to Ajax/Axios. Is there a way to initiate requests like we do with Ajax/Axios?

Let’s use GenericFunctionService to send GQL requests in an Ajax/Axios-like manner:

public static queryListByWrapper(): Promise<GanttDemoModel[] | undefined> {
  return GenericFunctionService.INSTANCE.simpleExecuteByName(MODEL_MODEL, 'queryListByWrapper', {
    rsql: '1==1'
  });
}

A standard Ajax/Axios request includes a request path (URL), request method (HttpMethod), and request body (Body). Similar to initiating an Ajax/Axios request, simpleExecuteByName accepts two or more parameters:

  • The first two parameters are the namespace and function name, which correspond to the request path (URL).
  • Any number of subsequent parameters correspond to the request body (Body).
  • The request method (HttpMethod) is fixed to POST and does not need to be explicitly specified.

1. GenericFunctionService API

execute

  • Function Description: Executes any function.
  • Type: <T>(functionDefinition: RuntimeFunctionDefinition, options: GenericFunctionOptions, ...args: unknown[]) => Promise<T | undefined>
  • Parameters:
    • functionDefinition: Specifies the function definition, obtained via FunctionCache.
    • options: Optional configurations for function execution.
    • ...args: Variable-length input parameters, passed in the order defined by the function.
  • Return Value: The function request result.

executeByFun

  • Function Description: Executes a function by specifying the namespace and function code.
  • Type: <T>(namespace: string, fun: string, options: GenericFunctionOptions, ...args: unknown[]) => Promise<T | undefined>
  • Parameters:
    • namespace: Specifies the namespace of the function to execute.
    • fun: Specifies the code of the function to execute.
    • options: Optional configurations for function execution.
    • ...args: Variable-length input parameters, passed in the order defined by the function.
  • Return Value: The function request result.

executeByName

  • Function Description: Executes a function by specifying the namespace and function name.
  • Type: <T>(namespace: string, name: string, options: GenericFunctionOptions, ...args: unknown[]) => Promise<T | undefined>
  • Parameters:
    • namespace: Specifies the namespace of the function to execute.
    • fun: Specifies the code of the function to execute.
    • options: Optional configurations for function execution.
    • ...args: Variable-length input parameters, passed in the order defined by the function.
  • Return Value: The function request result.

simpleExecute

  • Function Description: Executes any function using default execution options.
  • Type: <T>(functionDefinition: RuntimeFunctionDefinition, ...args: unknown[]) => Promise<T | undefined>
  • Parameters:
    • functionDefinition: Specifies the function definition, obtained via FunctionCache.
    • ...args: Variable-length input parameters, passed in the order defined by the function.
  • Return Value: The function request result.

simpleExecuteByFun

  • Function Description: Executes a function by specifying the namespace and function code, using default execution options.
  • Type: <T>(namespace: string, fun: string, ...args: unknown[]) => Promise<T | undefined>
  • Parameters:
    • namespace: Specifies the namespace of the function to execute.
    • fun: Specifies the code of the function to execute.
    • ...args: Variable-length input parameters, passed in the order defined by the function.
  • Return Value: The function request result.

simpleExecuteByName

  • Function Description: Executes a function by specifying the namespace and function name, using default execution options.
  • Type: <T>(namespace: string, name: string, ...args: unknown[]) => Promise<T | undefined>
  • Parameters:
    • namespace: Specifies the namespace of the function to execute.
    • fun: Specifies the code of the function to execute.
    • ...args: Variable-length input parameters, passed in the order defined by the function.
  • Return Value: The function request result.

Below, we will explain how to use GenericFunctionService to initiate GQL requests for all the GQL syntax examples covered in Chapter 1.

2. Usage Example with queryPage

const MODEL_MODEL = 'demo.gantt.GanttDemoModel';

public static async queryPage(): Promise<QueryPageResult<GanttDemoModel[]> | undefined> {
  return GenericFunctionService.INSTANCE.simpleExecuteByName(
    MODEL_MODEL,
    'queryPage',
    {
      currentPage: 1,
      size: 15
    },
    {
      rsql: '1==1'
    }
  );
}

From our previous GQL syntax learning, we know that the queryPage function has two input parameters, in order: QueryPagination and QueryWrapper. In the example above, we use the simpleExecuteByName method to initiate the call and pass the corresponding parameters in order.

The methods of the Generic Function Service use generic return types. In TypeScript type inference, as long as we define the type declaration for the function's return value, automatic inference is possible. We recommend that the call methods of individual services also declare the types of their input and output parameters whenever possible.

Note

To avoid request exceptions caused by changes in parameter names, the Generic Function Service matches function input parameter names by the order of variable-length parameters. This is the optimal solution we can achieve given the constraints.

Although each input parameter is marked with the corresponding model code in the function's metadata definition, JavaScript itself has no concept of types—this is therefore the best approach available to us.

3. Usage Example with create

const MODEL_MODEL = 'demo.gantt.GanttDemoModel';

public static async create(): Promise<GanttDemoModel | undefined> {
  return GenericFunctionService.INSTANCE.simpleExecuteByName(MODEL_MODEL, 'create', {
    code: 'test',
    name: 'test'
  });
}

In this example, we represent the structure of the create function's input parameters by flattening them. The following examples follow the same pattern.

Typically, you need to use function input parameters to construct the object to be created.

4. Usage Example with update

const MODEL_MODEL = 'demo.gantt.GanttDemoModel';

public static async update(id: string): Promise<GanttDemoModel | undefined> {
  return GenericFunctionService.INSTANCE.simpleExecuteByName(MODEL_MODEL, 'update', {
    id,
    name: 'newName'
  });
}

5. Usage Example with delete

const MODEL_MODEL = 'demo.gantt.GanttDemoModel';

public static async delete(ids: string[]): Promise<GanttDemoModel[] | undefined> {
  return GenericFunctionService.INSTANCE.simpleExecuteByName(
    MODEL_MODEL,
    'delete',
    ids.map((id) => ({ id }))
  );
}

From our previous GQL syntax learning, we know that the delete function is called by passing an array of dataList objects. Therefore, when using the Generic Function Service, we also need to follow the corresponding data type requirements.

When passing a string array of ids, we need to convert it into an object array and pass it as the first parameter of the delete function.

6. Usage of Enums (Data Dictionaries)

const MODEL_MODEL = 'demo.gantt.GanttDemoModel';

public static async update(id: string): Promise<GanttDemoModel | undefined> {
  return GenericFunctionService.INSTANCE.simpleExecuteByName(MODEL_MODEL, 'update', {
    id,
    status: ActiveEnum.ACTIVE
  });
}

When using the Generic Function Service, you no longer need to worry about data type conversion—enums can be passed directly according to their declared types.

Note

When the enum value does not match the enum name required by the backend, it must be passed as a string.

7. Usage of Maps (Key-Value Pairs)

const MODEL_MODEL = 'demo.gantt.GanttDemoModel';

const jsonData = {
  "key1": "value1",
  "key2": "value2"
}

public static async update(id: string): Promise<GanttDemoModel | undefined> {
  return GenericFunctionService.INSTANCE.simpleExecuteByName(MODEL_MODEL, 'update', {
    id,
    jsonData,
    jsonArrayData: [jsonData, jsonData]
  });
}

When using the Generic Function Service, you no longer need to worry about data type conversion—Map data types can also be passed easily without needing to care about whether serialization methods are required.

8. Multi-Level Nesting

const MODEL_MODEL = 'demo.gantt.GanttDemoModel';

public static async create(): Promise<GanttDemoModel | undefined> {
  return GenericFunctionService.INSTANCE.executeByName(
    MODEL_MODEL,
    'create',
    { deep: 2 },
    {
      code: 'test',
      name: 'test',
      children: [
        { code: 'c1', name: 'c1' },
        {
          code: 'c2',
          name: 'c2',
          children: [
            {
              code: 'c2-1',
              name: 'c2-1'
            },
            {
              code: 'c2-2',
              name: 'c2-2'
            }
          ]
        }
      ]
    }
  );
}

When the nesting depth of the passed object exceeds 1 level, the request can still be sent completely, but the response body cannot be fully constructed based on the request object. If we continue to use the previous simpleExecuteByName method to initiate this request, we will find that the children parameter of the c2 object is missing in the return result.

Therefore, to fully retrieve the response body object with deeper nesting levels, use the executeByName method and pass the deep property set to 2—this will return a more complete response body object.

Note

The deep parameter is used to automatically retrieve the hierarchy of response fields. Its default value is 1, meaning only one level of association is queried starting from the current model (sufficient for most cases). When you need to retrieve response body objects with deeper levels, be cautious of performance overhead caused by the deep parameter—this is especially noticeable in models with many associated fields.

Edit this page
Last Updated:1/14/26, 8:45 AM
Prev
Router Service
Next
HttpClient Service
默认页脚
Copyright © 2026 Mr.Hope