# Platform API
The platform provides two kinds of APIs:
- REST API: call the platform over HTTP. Used by the frontend, mobile apps, third-party systems and scripts.
- Plugin utility library (
tech.muyan:api): Java/Groovy utility classes that plugin source code uses to call platform capabilities, such as querying data, creating objects and running code asynchronously.
# Contents
# REST API
# Address Prefix
When deployed as described in Docker Deployment, the frontend and backend are served by the same nginx. nginx strips the /api prefix from requests starting with /api/ and forwards them to the backend. So when calling from outside, prefix every path below with /api. For example, the full address of the login endpoint is http://<server address>/api/auth/login.
TIP
Request addresses seen inside dynamic logic (for example the Webhook requestUrl) are the addresses the backend sees, without the /api prefix.
# Login
curl -X POST http://<server address>/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"[email protected]","password":"password"}'
2
3
A successful login returns HTTP 200 with the following response body (tokens truncated):
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"username": "[email protected]",
"roles": ["ROLE_DEVELOPER", "ROLE_ADMIN", "ROLE_USER"],
"id": 1,
"name": "čļ
įē§įŽĄįå",
"avatar": null,
"extInfo": null
}
2
3
4
5
6
7
8
9
10
11
12
access_token: the access token. It is valid for 3600 seconds by default (expires_in), which can be changed with the backend settingsecurity.jwt.expiration.refresh_token: the refresh token, used to obtain a new access token. See Refreshing the Token.
A wrong password returns HTTP 400; a non-existent user returns HTTP 404:
{"success":false,"errCode":1010,"errMsg":"ErrorCode: 1010, ErrorMsg: UserPasswordMismatch, ..."}
{"success":false,"errCode":1005,"errMsg":"ErrorCode: 1005, ErrorMsg: NotFound, ..."}
2
# Passing the Token in Requests
Depending on the characteristics of the request, the backend reads the token from one of the following sources:
| Priority | Method | Example |
|---|---|---|
| 1 | Authorization request header (recommended) | Authorization: Bearer <access_token> |
| 2 | access_token in a form-encoded request body | Content-Type: application/x-www-form-urlencoded, request body access_token=<token>&... |
| 3 | access_token URL parameter | GET /api/data/DynamicLogic?access_token=<token> |
| 4 | access_token cookie | Cookie: access_token=<token> |
curl 'http://<server address>/api/data/DynamicLogic?max=10&offset=0' \
-H 'Authorization: Bearer <access_token>'
2
Note
- In the current version, method 2 (form-encoded request body) does not work in practice: the request returns HTTP 400
IllegalArgument ... has an invalid signature. Use one of the other three methods. - When the
Authorizationheader starts withBearer, only the header is read; for non-GET form-encoded requests only the request body is read, and the URL parameter and cookie are not checked. - A token in a URL parameter appears in the server access logs; when calling a dynamic service it is also written verbatim into the "Exec params" of the service execution record. Use the request header instead of the URL parameter whenever possible.
Responses when authentication fails:
- No token: endpoints that require login (such as
/auth/userInfoand/permissions/...) return HTTP 401 (errCode1008 Unauthorized); endpoints that check permissions by domain class (such as/data/...) return HTTP 403 (errCode1004 NoPermission). - Token cannot be parsed: HTTP 401 (
errCode1012 TokenInvalid); wrong signature: HTTP 400 (errCode1001 IllegalArgument). - Token expired: HTTP 401 (
errCode1006 TokenExpired). In this case, first refresh the token.
# Refreshing the Token
After the access_token expires, you can exchange the refresh_token for a new access_token without entering the password again. The request parameters must be form-encoded (a JSON request body is not supported and returns 400):
curl -X POST http://<server address>/api/auth/access_token \
-d 'grant_type=refresh_token&refresh_token=<refresh_token>'
2
On success, the response has the same structure as the login response, and its refresh_token is still the one passed in the request.
Security note
In the current version, the refresh_token has no expiration (the backend setting security.jwt.refreshExpiration is empty by default), and it can be used directly as an access_token to call endpoints. In addition, calling GET /auth/userInfo with an unexpired access_token returns a newly issued, non-expiring refresh_token in the response (/auth/access_token, by contrast, only returns the passed-in token as the refresh_token). Keep the refresh_token as safe as a password: do not write it to logs, URLs or any storage other than the frontend.
# Temporary Access via a Token in the URL
Opening an address such as http://<server address>/?access_token=<access_token> makes the frontend use that token to access the system directly, without the login page. This suits scenarios where users jump in from another system and view a page without logging in.
In this mode:
- The frontend does not write the token to the browser's localStorage, so no login state remains after the page is closed.
- When the page opens, the frontend calls
GET /auth/userInfo. This endpoint uses the token in the URL to issue a new pair ofaccess_tokenandrefresh_token, which the frontend uses to renew the session while the page is open.
Risk
The token in the URL is not single-use. Anyone who gets the link can call /auth/userInfo before the token expires to obtain a new refresh_token, and the refresh_token currently does not expire (see the previous section). Therefore:
- Only generate such links for dedicated accounts with minimal permissions, never for administrator accounts;
- Only send links through trusted channels; do not post them on public pages, tickets or chat groups;
- In the current version, neither login nor token access checks whether the account is disabled or locked, and changing the password does not invalidate issued tokens. After a link leaks, delete the account or change its username; to invalidate all issued tokens, you have to change the backend setting
security.jwt.secretand restart.
# Main Routes
The table below lists the main built-in endpoints (all paths omit the /api prefix). <domain> is the short name of a domain class, such as DynamicLogic.
| Method | Path | Description |
|---|---|---|
| POST | /auth/login | Log in, see above |
| POST | /auth/access_token | Exchange a refresh_token for a new token |
| GET | /auth/userInfo | Return the current user's information and issue a new pair of tokens |
| POST | /auth/logout | Log out. In the current version the call returns 403 and does not work |
| GET | /data/<domain>?max=&offset= | Paged query of an object list, returns {total, data, formHookDataList}. max defaults to 10, maximum 500 |
| GET | /data/<domain>/<id> | Query a single object, returns {data, formHookData}, with the object in data |
| POST | /data/<domain> | Create an object; the request body is JSON |
| PUT | /data/<domain>/<id> | Update an object; the request body is JSON and only needs id and the fields to change |
| PUT | /data/<domain>/batch | Batch update; the request body is a JSON array |
| DELETE | /data/<domain>/<id> | Delete an object |
| POST | /search/<domain>?offset=&max= | Query an object list by conditions |
| POST | /search/keyword/<domain>?q=&max= | Query by keyword; the keyword goes in the URL parameter q |
| POST | /action/byName/<action name> | Execute an object action by name |
| POST | /action/byId/<action id> | Execute an object action by id |
| GET | /permissions/<domain>/create | Whether the current user can create objects of this type, returns {"create": true} |
| POST | /permissions/<domain>/ | Request body {"ids":[...]}, returns the view/update/delete permissions for each object |
| Any | /service/<service name> | Call a dynamic service, see Dynamic Service |
| GET / POST | /webhook/<url> | Call a Webhook, see Webhook |
| GET / POST | /attachment/... | Upload and download attachments |
| GET | /dashboard/list, /dashboard/meta/<id>, /dashboard/widget/data/<id> | Dashboards and their widget data |
| GET | /config/system, /config/<key> | Read system configuration |
Examples of creating and updating an object (the ids in the examples come from a specific environment: logicType 12 is DYNAMIC_SERVICE_CORE_LOGIC, dynamicLogicEngine 1 is GROOVY_CODE, and 93 is the newly created dynamic logic. Look up the ids in your own environment first with GET /api/data/DynamicLogicType and GET /api/data/DynamicLogicEngine):
# Create
curl -X POST http://<server address>/api/data/DynamicLogic \
-H 'Authorization: Bearer <access_token>' -H 'Content-Type: application/json' \
-d '{"name":"æĨč¯ĸ莞å¤čŋčĄįļæ","description":"æčŽžå¤įŧåˇčŋå莞å¤čŋčĄįļæ","logicType":{"id":12},"dynamicLogicEngine":{"id":1},"code":"return [:]"}'
# Update: pass only id and the fields to change
curl -X PUT http://<server address>/api/data/DynamicLogic/93 \
-H 'Authorization: Bearer <access_token>' -H 'Content-Type: application/json' \
-d '{"id":93,"description":"æčŽžå¤įŧåˇčŋå莞å¤čŋčĄįļæåį´¯čŽĄčŋčĄæļéŋ"}'
2
3
4
5
6
7
8
9
The create, update and delete endpoints respond with {"status": "...", "message": "...", "data": {...}, "date": <timestamp in milliseconds>}. Note that when creating or updating a single object fails data validation (for example, a required field is empty), the HTTP status code is still 200; only status is error and message contains the validation error. Callers must check the status field rather than the HTTP status code alone. The batch update endpoint PUT /data/<domain>/batch differs: if any record fails validation, it throws an exception directly and returns an error response.
Object-type fields are expressed as {"id": <id>}. Create, read, update and delete are controlled by the role requirements configured on the domain class, see Object Permission Control.
TIP
The route table already contains PATCH /data/<domain>/<id>, but the backend has no implementation for it and the call returns 403. Use PUT instead.
Besides the table above, plugins can register their own endpoints through a Custom Controller.
# Plugin Utility Library
Plugin source code calls platform capabilities through the tech.muyan:api library. The utility classes in the library are stubs: their method bodies only throw IllegalStateException ("This method will be provided by platform implementation dynamically"), and the real implementation is provided by the platform at runtime. Therefore it must be included as compileOnly and must not be packaged into the plugin; otherwise calls throw exceptions directly. The plugin template's codes/build.gradle is already configured:
dependencies {
compileOnly 'tech.muyan:api:0.0.5'
testCompileOnly 'tech.muyan:api:0.0.5'
testRuntimeOnly 'tech.muyan:api:0.0.5'
}
2
3
4
5
Version notes
- The plugin template uses
0.0.5by default. The methods listed in this section are based on0.0.5and have been confirmed to exist at runtime on the current platform (1.0.0-beta18). - Custom Controller (
MuyanDynamicController, the@Get/@Post/@Put/@Deleteannotations and so on) and dynamic RPC (MuyanRpcService,@RpcClient,DynamicRpcClientService) are only provided in the 1.0.0 series of the api library, for example1.0.0-11-35-SNAPSHOTin thelibs-snapshotrepository; they are not in 0.0.5. TheAsyncHelperin the 1.0.0 series api library matches the platform runtime (it addstask(String, Runnable),supplyAsyncand more), but itsMessageHelperstill only declarespushNotificationand cannot be used either (see MessageHelper). - A few methods in the library do not exist in the current platform runtime. They are marked one by one below; do not use them.
Contents:
- StorageUtils
- QueryHelper
- SimpleQuery
- DomainHelper
- BeanContainer
- AsyncHelper
- MessageSeverity
- MessageHelper
- Plugin Extension Interfaces
# StorageUtils
Utility class for handling file storage.
# createStorageFileDomain
public static StorageFieldValue createStorageFileDomain(String fileName, String mimeType, InputStream inputStream)
Saves a file to platform storage and returns a StorageFieldValue object representing the file, which can be assigned to an object's file field.
- Parameters:
fileName: file namemimeType: MIME type of the fileinputStream: file content
- Returns: a
StorageFieldValueobject
# QueryHelper
Runs code within a database session or transaction, or executes SQL directly.
public static <T> T withSession(Closure<T> closure)
public static <T> T withNewSession(Closure<T> closure)
public static <T> T withTransaction(Closure<T> closure)
public static <T> T withSql(Function<Sql, T> function)
2
3
4
withSession: runs the closure in the current database session.withNewSession: runs the closure in a new database session.withTransaction: runs the closure in a transaction.withSql: runs the function with agroovy.sql.Sqlobject, suitable for native SQL.
import groovy.sql.Sql
import tech.muyan.utils.QueryHelper
List rows = QueryHelper.withSql { Sql sql ->
sql.rows("select id, name from dynamic_logic where logic_type_id = ?", [12])
}
2
3
4
5
6
# SimpleQuery
A query class for querying objects by conditions.
SimpleQuery.of("WorkTask")
.ge("scheduledStartTime", start)
.listAll();
2
3
This query retrieves all WorkTask objects whose scheduled start time is greater than or equal to the given start time.
Multiple conditions can be chained together:
SimpleQuery.of("WorkTask")
.eq("assignee", user)
.ge("scheduledStartTime", start)
.lt("scheduledEndTime", end)
.eq("status", "ACTIVE")
.listAll();
2
3
4
5
6
In the examples above:
"WorkTask"is the short name of the domain class."scheduledStartTime"is the name of a field in the domain class."assignee"is the user assigned to the WorkTask; the condition must use the user object itself, notuser.id.
TIP
For object-type (DOMAIN_OBJECT) fields, use the object itself rather than object.id as the match condition.
# Condition Methods
The following methods all return the SimpleQuery itself and can be chained:
| Method | Description |
|---|---|
eq(String fieldName, Object value) | Equal to |
ne(String fieldName, Object value) | Not equal to |
gt(String fieldName, Object value) | Greater than |
ge(String fieldName, Object value) | Greater than or equal to |
lt(String fieldName, Object value) | Less than |
le(String fieldName, Object value) | Less than or equal to |
iLike(String fieldName, String value) | Case-insensitive pattern match |
notILike(String fieldName, String value) | Case-insensitive pattern non-match |
in(String fieldName, Collection<?> value) | In the collection |
notIn(String fieldName, Collection<?> value) | Not in the collection |
isNull(String fieldName) | Is null |
notNull(String fieldName) | Is not null |
addConditions(List<QueryCondition> queryConditions) | Add multiple conditions at once |
# Executing the Query
| Method | Return type | Description |
|---|---|---|
get() | T | Return a single result |
list(int offset, int limit) | PaginationQueryResult<T> | Paged query |
list(int offset, int limit, List<String> orderBy) | PaginationQueryResult<T> | Paged and sorted |
list(int offset, int limit, List<String> orderBy, boolean asc) | PaginationQueryResult<T> | Paged with sort direction |
listAll() | List<T> | Return all results |
PaginationQueryResult<T> extends List<T> and can be used directly as a list.
# Static Methods
| Method | Description |
|---|---|
SimpleQuery<?> of(String domainName) | Create a query by domain class short name |
<T> SimpleQuery<T> of(Class<T> clazz) | Create a query by class |
<T> SimpleQuery<T> of(Class<T> clazz, boolean and) | Create a query by class, specifying whether conditions are combined with AND or OR |
Object getById(String domainName, Long id) | Query by id |
<T> T getById(Class<T> clazz, Long id) | Query by id |
List<Object> getByIds(String domainName, List<Long> ids) | Query by a list of ids |
<T> List<T> getByIds(Class<T> clazz, List<Long> ids) | Query by a list of ids |
List<Object> getAll(String domainName) | Query all objects |
<T> List<T> getAll(Class<T> clazz) | Query all objects |
long count(String domainName) | Count objects |
<T> long count(Class<T> clazz) | Count objects |
# DomainHelper
Utility class for creating, updating, deleting and rendering objects.
public static Object buildDomain(String domainName)
public static Object buildDomain(String domainName, Object properties)
public static void createDomain(Object requestData)
public static void updateDomain(Object requestData)
public static void deleteDomain(Object requestData)
public static Map<String, Object> render(Object domainObj, DomainObjectRenderType renderType)
public static <T> List<T> batchCreate(Class<T> domainClazz, List<T> domainObjects)
public static <T> List<T> batchCreate(String domainName, List<T> domainObjects)
2
3
4
5
6
7
8
buildDomain: constructs an object (without saving it) by domain class short name, optionally with properties.createDomain/updateDomain/deleteDomain: create, update and delete objects from request data.render: converts an object to a Map.DomainObjectRenderTypehas two values:ONLY_LABEL_FIELD(only the label field) andALL_COLUMNS(all fields).batchCreate: creates objects in batch and returns the list of created objects.
# BeanContainer
Gets beans from the platform and plugins.
public static <T> T getBean(Class<T> beanClass)
public static <T> Collection<T> getBeansOfType(Class<T> beanClass)
2
getBean: gets one bean by type.getBeansOfType: gets all beans of a type.
BeanContainer is provided at runtime by the built-in platform plugin PlatformAdapter, so PlatformAdapter must remain in the plugin's dependsOnPlugins. See Plugin Development.
# AsyncHelper
Runs tasks asynchronously while keeping context such as the current tenant in the new thread.
public static Runnable scheduleAtFixRate(long period, Runnable runnable)
public static Runnable scheduleAtFixRate(long period, Runnable runnable, boolean newThread)
public static void task(Runnable runnable)
2
3
scheduleAtFixRate: runs the task repeatedly at a fixed interval (milliseconds). The return value is aRunnable; running it cancels the task. WhennewThreadistrue, a dedicated timer thread is used.task: runs a task asynchronously once.
Both methods carry the current tenant and the plugin's class loader into the new thread, but not the currently logged-in user. The platform runtime has marked both methods @Deprecated (for task(Runnable), switch to task(String threadName, Runnable runnable) in the 1.0.0 series api library); avoid them in new code where possible.
Methods not available on the current platform
The following methods are declared in api 0.0.5, but the current platform runtime has no implementation for them. Calling them fails with NoSuchMethodError (Java or @CompileStatic code) or MissingMethodException (dynamic Groovy code):
task(boolean newThread, Runnable runnable)task(ExecutorService executorService, Runnable runnable)newForkJoinPool(int parallelism)
In addition, the platform runtime's task(Runnable) returns Thread, which does not match the void declared in 0.0.5. Calling it from dynamic Groovy code works; calling it from Java or @CompileStatic code fails with NoSuchMethodError.
# MessageSeverity
Enum representing the severity of a message.
| Value | Description |
|---|---|
INFO | General information |
WARNING | Warning |
ERROR | Error |
IMPORTANT | Important |
INTERNAL | Originally intended for internal system information exchange. In the current version the backend does not filter it, and the frontend pops up a notification as for general information (INFO) |
ACTION_REQUIRED | A message that requires user action. In the current version the frontend displays it the same way as IMPORTANT: a notification in the lower-right corner that does not close automatically and does not block the page |
# MessageHelper
Currently unavailable
The pushNotification method declared in the api library (0.0.5 and the 1.0.0 series) does not exist in the current platform runtime, and calls to it fail. What the platform runtime actually provides is pushMessage, and the recipient field of its Notification is toUser (a user object), which does not match toUserName (a username) in api 0.0.5. Until the platform fixes this, do not call MessageHelper in plugins.
# Plugin Extension Interfaces
Besides the utility classes above, the tech.muyan.api package also contains interfaces for plugins to implement, for example:
DynamicDomainEntity: binds a POJO class in a plugin to a platform domain class.MuyanPlatformComponent: a plugin component; the platform callsonLoad()/offLoad()when loading and unloading plugins.websocket.MuyanWebSocketComponent: lets a plugin handle WebSocket messages.
For usage, see Plugin Development.