@synopackageland/templates 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/package.json +1 -1
- package/templates/_shared/AGENTS.md +6 -5
- package/templates/_shared/CLAUDE.md +2 -0
- package/templates/compose/dsm-package.yaml +1 -1
- package/templates/native-go/README.md +28 -11
- package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
- package/templates/native-go/backend/cmd/app/main.go +9 -2
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/go.sum +2 -2
- package/templates/native-go/backend/internal/api/operations.json +5 -0
- package/templates/native-go/backend/internal/api/permissions.go +7 -0
- package/templates/native-go/backend/internal/api/permissions.json +3 -1
- package/templates/native-go/backend/internal/app/routes.go +2 -46
- package/templates/native-go/backend/internal/app/routes_test.go +4 -25
- package/templates/native-go/dsm-package.yaml +2 -28
- package/templates/native-go/examples/operations.json +112 -0
- package/templates/native-go/operations.json +27 -0
- package/templates/native-go/ui/app.js +0 -25
- package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -4
- package/templates/native-go-root-ops/frontend/test/executor-gate.test.ts +7 -6
- package/templates/native-go-vue/frontend/package-lock.json +4 -4
- package/templates/native-go-vue/frontend/package.json +1 -1
- package/templates/native-go-vue/frontend/src/App.vue +5 -34
- package/templates/native-go-vue/frontend/test/apps.test.ts +19 -25
- package/templates/native-go-vue/frontend/test/native-controls.test.ts +4 -1
- package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +5 -3
- package/templates/native-node/README.md +27 -7
- package/templates/native-node/backend/app/permissions.json +3 -1
- package/templates/native-node/backend/package-lock.json +1 -1
- package/templates/native-node/backend/src/routes.ts +5 -43
- package/templates/native-node/backend/test/routes.test.ts +3 -23
- package/templates/native-node/dsm-package.yaml +3 -29
- package/templates/native-node/examples/operations.json +112 -0
- package/templates/native-node/operations.json +27 -0
- package/templates/native-node-delegate/operations.patch.json +20 -0
- package/templates/native-node-jobs/operations.patch.json +52 -0
- package/templates/native-node-root-ops/operations.patch.json +20 -0
- package/templates/native-python/README.md +27 -7
- package/templates/native-python/backend/app/permissions.json +3 -1
- package/templates/native-python/backend/app/routes.py +2 -41
- package/templates/native-python/backend/tests/test_routes.py +3 -19
- package/templates/native-python/dsm-package.yaml +3 -29
- package/templates/native-python/examples/operations.json +112 -0
- package/templates/native-python/operations.json +27 -0
- package/templates/native-python-delegate/operations.patch.json +20 -0
- package/templates/native-python-jobs/operations.patch.json +52 -0
- package/templates/native-python-root-ops/operations.patch.json +20 -0
- package/templates/native-rust/README.md +28 -8
- package/templates/native-rust/backend/Cargo.lock +2 -2
- package/templates/native-rust/backend/Cargo.toml +1 -1
- package/templates/native-rust/backend/permissions.json +3 -1
- package/templates/native-rust/backend/src/routes.rs +11 -149
- package/templates/native-rust/dsm-package.yaml +3 -29
- package/templates/native-rust/examples/operations.json +112 -0
- package/templates/native-rust/operations.json +27 -0
- package/templates/native-rust-delegate/backend/Cargo.lock +2 -2
- package/templates/native-rust-delegate/backend/Cargo.toml +1 -1
- package/templates/native-rust-delegate/backend/src/delegate.rs +7 -1
- package/templates/native-rust-delegate/operations.patch.json +20 -0
- package/templates/native-rust-jobs/operations.patch.json +52 -0
- package/templates/native-rust-root-ops/operations.patch.json +20 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "synopkgland.api",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"operations": {
|
|
5
|
+
"example.read": {
|
|
6
|
+
"summary": "Read a resource",
|
|
7
|
+
"ai": "read",
|
|
8
|
+
"cli": [
|
|
9
|
+
"example",
|
|
10
|
+
"read"
|
|
11
|
+
],
|
|
12
|
+
"effect": "read",
|
|
13
|
+
"permission": "administrators",
|
|
14
|
+
"paramsSchema": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
},
|
|
18
|
+
"resultSchema": {
|
|
19
|
+
"type": "object"
|
|
20
|
+
},
|
|
21
|
+
"flow": {
|
|
22
|
+
"type": "invoke",
|
|
23
|
+
"method": "example.read"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"example.preview": {
|
|
27
|
+
"summary": "Preview a resource change",
|
|
28
|
+
"ai": "preview",
|
|
29
|
+
"cli": [
|
|
30
|
+
"example",
|
|
31
|
+
"preview"
|
|
32
|
+
],
|
|
33
|
+
"effect": "read",
|
|
34
|
+
"permission": "administrators",
|
|
35
|
+
"paramsSchema": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": false
|
|
38
|
+
},
|
|
39
|
+
"resultSchema": {
|
|
40
|
+
"type": "object"
|
|
41
|
+
},
|
|
42
|
+
"flow": {
|
|
43
|
+
"type": "invoke",
|
|
44
|
+
"method": "example.preview"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"example.draft": {
|
|
48
|
+
"summary": "Draft a resource change for human confirmation",
|
|
49
|
+
"ai": "draft",
|
|
50
|
+
"cli": [
|
|
51
|
+
"example",
|
|
52
|
+
"draft"
|
|
53
|
+
],
|
|
54
|
+
"effect": "write",
|
|
55
|
+
"permission": "administrators",
|
|
56
|
+
"paramsSchema": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false
|
|
59
|
+
},
|
|
60
|
+
"resultSchema": {
|
|
61
|
+
"type": "object"
|
|
62
|
+
},
|
|
63
|
+
"flow": {
|
|
64
|
+
"type": "invoke",
|
|
65
|
+
"method": "example.draft"
|
|
66
|
+
},
|
|
67
|
+
"preview": "example.preview"
|
|
68
|
+
},
|
|
69
|
+
"example.human": {
|
|
70
|
+
"summary": "Delete a resource after human review",
|
|
71
|
+
"ai": "human-only",
|
|
72
|
+
"cli": [
|
|
73
|
+
"example",
|
|
74
|
+
"human"
|
|
75
|
+
],
|
|
76
|
+
"effect": "destructive",
|
|
77
|
+
"permission": "administrators",
|
|
78
|
+
"paramsSchema": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"additionalProperties": false
|
|
81
|
+
},
|
|
82
|
+
"resultSchema": {
|
|
83
|
+
"type": "object"
|
|
84
|
+
},
|
|
85
|
+
"flow": {
|
|
86
|
+
"type": "invoke",
|
|
87
|
+
"method": "example.human"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"example.hidden": {
|
|
91
|
+
"summary": "Read internal state",
|
|
92
|
+
"ai": "hidden",
|
|
93
|
+
"cli": [
|
|
94
|
+
"example",
|
|
95
|
+
"hidden"
|
|
96
|
+
],
|
|
97
|
+
"effect": "read",
|
|
98
|
+
"permission": "administrators",
|
|
99
|
+
"paramsSchema": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"additionalProperties": false
|
|
102
|
+
},
|
|
103
|
+
"resultSchema": {
|
|
104
|
+
"type": "object"
|
|
105
|
+
},
|
|
106
|
+
"flow": {
|
|
107
|
+
"type": "invoke",
|
|
108
|
+
"method": "example.hidden"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "synopkgland.api",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"operations": {
|
|
5
|
+
"app.status": {
|
|
6
|
+
"summary": "Read App status",
|
|
7
|
+
"ai": "read",
|
|
8
|
+
"cli": [
|
|
9
|
+
"app",
|
|
10
|
+
"status"
|
|
11
|
+
],
|
|
12
|
+
"effect": "read",
|
|
13
|
+
"permission": "administrators",
|
|
14
|
+
"paramsSchema": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
},
|
|
18
|
+
"resultSchema": {
|
|
19
|
+
"type": "object"
|
|
20
|
+
},
|
|
21
|
+
"flow": {
|
|
22
|
+
"type": "invoke",
|
|
23
|
+
"method": "app.status"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"operations": {
|
|
3
|
+
"report.save": {
|
|
4
|
+
"summary": "Save an example report",
|
|
5
|
+
"ai": "hidden",
|
|
6
|
+
"effect": "write",
|
|
7
|
+
"permission": "authenticated",
|
|
8
|
+
"cli": [
|
|
9
|
+
"report",
|
|
10
|
+
"save"
|
|
11
|
+
],
|
|
12
|
+
"paramsSchema": {},
|
|
13
|
+
"resultSchema": {},
|
|
14
|
+
"flow": {
|
|
15
|
+
"type": "invoke",
|
|
16
|
+
"method": "report.save"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"operations": {
|
|
3
|
+
"hash.submit": {
|
|
4
|
+
"summary": "Start a file hash job",
|
|
5
|
+
"ai": "hidden",
|
|
6
|
+
"effect": "write",
|
|
7
|
+
"permission": "administrators",
|
|
8
|
+
"cli": [
|
|
9
|
+
"hash",
|
|
10
|
+
"submit"
|
|
11
|
+
],
|
|
12
|
+
"paramsSchema": {},
|
|
13
|
+
"resultSchema": {},
|
|
14
|
+
"flow": {
|
|
15
|
+
"type": "invoke",
|
|
16
|
+
"method": "hash.submit"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"hash.get": {
|
|
20
|
+
"summary": "Read a file hash job",
|
|
21
|
+
"ai": "hidden",
|
|
22
|
+
"effect": "read",
|
|
23
|
+
"permission": "administrators",
|
|
24
|
+
"cli": [
|
|
25
|
+
"hash",
|
|
26
|
+
"get"
|
|
27
|
+
],
|
|
28
|
+
"paramsSchema": {},
|
|
29
|
+
"resultSchema": {},
|
|
30
|
+
"flow": {
|
|
31
|
+
"type": "invoke",
|
|
32
|
+
"method": "hash.get"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"hash.cancel": {
|
|
36
|
+
"summary": "Cancel a file hash job",
|
|
37
|
+
"ai": "hidden",
|
|
38
|
+
"effect": "write",
|
|
39
|
+
"permission": "administrators",
|
|
40
|
+
"cli": [
|
|
41
|
+
"hash",
|
|
42
|
+
"cancel"
|
|
43
|
+
],
|
|
44
|
+
"paramsSchema": {},
|
|
45
|
+
"resultSchema": {},
|
|
46
|
+
"flow": {
|
|
47
|
+
"type": "invoke",
|
|
48
|
+
"method": "hash.cancel"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"operations": {
|
|
3
|
+
"whoami.run": {
|
|
4
|
+
"summary": "Run the approved identity operation",
|
|
5
|
+
"ai": "hidden",
|
|
6
|
+
"effect": "write",
|
|
7
|
+
"permission": "administrators",
|
|
8
|
+
"cli": [
|
|
9
|
+
"whoami",
|
|
10
|
+
"run"
|
|
11
|
+
],
|
|
12
|
+
"paramsSchema": {},
|
|
13
|
+
"resultSchema": {},
|
|
14
|
+
"flow": {
|
|
15
|
+
"type": "invoke",
|
|
16
|
+
"method": "whoami.run"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -6,7 +6,7 @@ runs as its own DSM package account, needs no containers, and has no root by def
|
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
8
|
DSM main menu → DSM window (ui/app.js)
|
|
9
|
-
│ ctx.call("
|
|
9
|
+
│ ctx.call("app.status") same-origin POST + DSM session
|
|
10
10
|
▼
|
|
11
11
|
/webman/3rdparty/__PACKAGE__/cgi/api.cgi (the SDK runtime, runs as package user)
|
|
12
12
|
1. transport guards: POST, JSON, same origin, ≤ 1 MiB, bounded JSON complexity
|
|
@@ -17,7 +17,7 @@ DSM main menu → DSM window (ui/app.js)
|
|
|
17
17
|
|
|
18
18
|
## Requests and long work
|
|
19
19
|
|
|
20
|
-
SDK/App 0.
|
|
20
|
+
SDK/App 0.47.0 defaults to 300-second requests and a 302-second browser wait.
|
|
21
21
|
Explicit `backend.timeout_seconds` and earlier parent deadlines still apply;
|
|
22
22
|
rebuild the App to use the new SDK. For long NAS work, enable the existing jobs
|
|
23
23
|
capability: return admission and the original job ID promptly, execute in the
|
|
@@ -27,7 +27,7 @@ last queued/running state intact. After a lost admission reply, reuse the same
|
|
|
27
27
|
idempotency key and input instead of replaying a mutation with a new key.
|
|
28
28
|
Jobs default to 30 minutes; approved executor limits remain separate. ctl keeps
|
|
29
29
|
its 62-second transport default; use an explicit `--timeout` or an existing
|
|
30
|
-
nonblocking job. See the SDK's [request deadline contract](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.
|
|
30
|
+
nonblocking job. See the SDK's [request deadline contract](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.47.0/docs/request-deadlines.md).
|
|
31
31
|
|
|
32
32
|
## Quick start
|
|
33
33
|
|
|
@@ -35,6 +35,8 @@ Needs Node.js with `synopkgland` installed, and Python 3.8 or later.
|
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
(cd backend && python3 -m unittest discover -s tests) # method tests
|
|
38
|
+
synopkgland ctl generate
|
|
39
|
+
synopkgland check
|
|
38
40
|
synopkgland build # → dist/__PACKAGE__-0.1.0-0001-{x86_64,aarch64}.spk
|
|
39
41
|
synopkgland inspect dist/__PACKAGE__-0.1.0-0001-x86_64.spk
|
|
40
42
|
```
|
|
@@ -42,8 +44,7 @@ synopkgland inspect dist/__PACKAGE__-0.1.0-0001-x86_64.spk
|
|
|
42
44
|
Install: Package Center → Settings → General → Trust Level "Any publisher",
|
|
43
45
|
then Manual Install with the SPK for your NAS's CPU. Open
|
|
44
46
|
**__DISPLAY_NAME__** from the DSM main menu: it shows who you are signed in
|
|
45
|
-
as
|
|
46
|
-
greeting in the package's data folder.
|
|
47
|
+
as and what the backend reports.
|
|
47
48
|
|
|
48
49
|
## Project layout
|
|
49
50
|
|
|
@@ -55,7 +56,7 @@ greeting in the package's data folder.
|
|
|
55
56
|
| `i18n/glossary.json` | DSM's own UI terms, and which messages must use them | Add terms |
|
|
56
57
|
| `backend/app/routes.py` | Your methods: `build` registers them | Yes — your backend |
|
|
57
58
|
| `backend/app/jobs.py`, `root_ops.py`, `delegate.py` | Capability examples (stubs unless `init --with` added them) | Yes |
|
|
58
|
-
| `backend/app/permissions.json` |
|
|
59
|
+
| `backend/app/permissions.json` | Generated method policy | No |
|
|
59
60
|
| `backend/app/main.py` | Runs the App on the runtime's channel | Rarely |
|
|
60
61
|
| `backend/tests/` | unittest tests; `support.call` plays the runtime | Yes |
|
|
61
62
|
| `backend/vendor/synopkgland_app` | The Python SDK for tests, copied by `synopkgland init` from the SDK release; the build ships its own copy of the release `backend.sdk` names | No |
|
|
@@ -68,7 +69,8 @@ runtime CGI at `ui/cgi/api.cgi`.
|
|
|
68
69
|
## Add a method
|
|
69
70
|
|
|
70
71
|
Register it in `build`. Every method defaults to DSM administrator membership;
|
|
71
|
-
|
|
72
|
+
declare metadata and access rules in `operations.json`; `ctl generate` writes
|
|
73
|
+
`backend/app/permissions.json`:
|
|
72
74
|
|
|
73
75
|
```python
|
|
74
76
|
def build(cfg):
|
|
@@ -179,3 +181,21 @@ bounded private copy for the worker; no browser session or NAS picker is used.
|
|
|
179
181
|
Submit with `hash.submit`; use `hash.get` for progress and `hash.cancel` to
|
|
180
182
|
cancel. These handlers call the existing `bg.submit`, `bg.get` and `bg.cancel`
|
|
181
183
|
services; the runtime checks the input path and binds ownership to the caller.
|
|
184
|
+
|
|
185
|
+
## Operation registry
|
|
186
|
+
|
|
187
|
+
Declare each App method once in `operations.json`, including its English summary,
|
|
188
|
+
params/result schemas, effect, permission, flow and required `ai` class. The
|
|
189
|
+
SDK contributes its own hidden operation catalog for enabled capability routes.
|
|
190
|
+
Register the handler, then run `synopkgland ctl generate`, `synopkgland ctl
|
|
191
|
+
generate --check` and `synopkgland check`. Build refuses generated drift or a
|
|
192
|
+
route/spec mismatch. Never maintain commands or operation descriptions in the
|
|
193
|
+
manifest or skill by hand. Read the generated `generated/operations.md`.
|
|
194
|
+
|
|
195
|
+
`examples/operations.json` illustrates read, preview, draft, human-only and hidden.
|
|
196
|
+
It is documentation only: its operations have no handlers and are never shipped
|
|
197
|
+
as commands. Copy a declaration only when implementing a real App capability.
|
|
198
|
+
Draft writes require a non-mutating preview and explicit human confirmation.
|
|
199
|
+
Long operations use the existing job flow; tools own polling and pagination.
|
|
200
|
+
|
|
201
|
+
Use a matching SDK/CLI/templates release containing the registry support.
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
checked the DSM session, the entry gate and that rule before a handler runs.
|
|
1
|
+
"""Register real handlers here; operations.json owns metadata and permissions.
|
|
2
|
+
Run ctl generate and ctl generate --check after adding a handler.
|
|
4
3
|
"""
|
|
5
4
|
|
|
6
|
-
import json
|
|
7
5
|
import os
|
|
8
6
|
import platform
|
|
9
7
|
import sys
|
|
10
|
-
import tempfile
|
|
11
8
|
import time
|
|
12
9
|
|
|
13
10
|
from synopkgland_app import App, Error
|
|
@@ -15,7 +12,6 @@ from synopkgland_app import App, Error
|
|
|
15
12
|
import delegate
|
|
16
13
|
import jobs
|
|
17
14
|
import root_ops
|
|
18
|
-
from params import fields
|
|
19
15
|
|
|
20
16
|
|
|
21
17
|
class Config:
|
|
@@ -35,8 +31,6 @@ class Config:
|
|
|
35
31
|
def build(cfg):
|
|
36
32
|
app = App()
|
|
37
33
|
app.method("app.status")(lambda ctx, params: status(cfg, ctx))
|
|
38
|
-
app.method("greeting.get")(lambda ctx, params: get_greeting(cfg))
|
|
39
|
-
app.method("greeting.set")(lambda ctx, params: set_greeting(cfg, ctx, params))
|
|
40
34
|
# Each capability module is a stub until `synopkgland init --with` adds its example.
|
|
41
35
|
for capability in (jobs, root_ops, delegate):
|
|
42
36
|
capability.register(app, cfg)
|
|
@@ -72,36 +66,3 @@ def installed_version(cfg):
|
|
|
72
66
|
except OSError:
|
|
73
67
|
pass
|
|
74
68
|
return ""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def greeting_path(cfg):
|
|
78
|
-
return os.path.join(cfg.data_dir, "greeting.json")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def get_greeting(cfg):
|
|
82
|
-
try:
|
|
83
|
-
with open(greeting_path(cfg), encoding="utf-8") as f:
|
|
84
|
-
return json.load(f)
|
|
85
|
-
except FileNotFoundError:
|
|
86
|
-
return {"text": "Hello"}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def set_greeting(cfg, ctx, params):
|
|
90
|
-
text = fields(params, text=str)["text"]
|
|
91
|
-
if not 1 <= len(text) <= 200:
|
|
92
|
-
raise Error.bad_request("text must be 1 to 200 characters")
|
|
93
|
-
greeting = {"text": text, "updated_by": ctx.user}
|
|
94
|
-
write_file_atomic(greeting_path(cfg), json.dumps(greeting).encode())
|
|
95
|
-
return greeting
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def write_file_atomic(path, data):
|
|
99
|
-
"""Replace path in one rename, so readers never see half a file."""
|
|
100
|
-
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), prefix=".tmp-")
|
|
101
|
-
try:
|
|
102
|
-
with os.fdopen(fd, "wb") as f:
|
|
103
|
-
f.write(data)
|
|
104
|
-
os.replace(tmp, path)
|
|
105
|
-
except BaseException:
|
|
106
|
-
os.unlink(tmp)
|
|
107
|
-
raise
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import os
|
|
3
2
|
import tempfile
|
|
4
3
|
import unittest
|
|
@@ -49,24 +48,9 @@ class RoutesTest(unittest.TestCase):
|
|
|
49
48
|
self.assertTrue(reply.ok, reply.error)
|
|
50
49
|
self.assertNotIn("portal", reply.data)
|
|
51
50
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_greeting_set_stores_the_text_and_who_set_it(self):
|
|
56
|
-
reply = call(self.app, "greeting.set", {"text": "Hi 👋"}, user="alice")
|
|
57
|
-
self.assertEqual(reply.data, {"text": "Hi 👋", "updated_by": "alice"})
|
|
58
|
-
self.assertEqual(call(self.app, "greeting.get").data, {"text": "Hi 👋", "updated_by": "alice"})
|
|
59
|
-
self.assertEqual(os.listdir(self.data), ["greeting.json"])
|
|
60
|
-
|
|
61
|
-
def test_greeting_counts_characters_not_bytes(self):
|
|
62
|
-
self.assertTrue(call(self.app, "greeting.set", {"text": "é" * 200}).ok)
|
|
63
|
-
|
|
64
|
-
def test_greeting_set_refuses_invalid_params_and_keeps_the_old_text(self):
|
|
65
|
-
for params in [None, [], {}, {"text": ""}, {"text": "x" * 201}, {"text": 5}, {"text": "hi", "extra": 1}]:
|
|
66
|
-
with self.subTest(params=params):
|
|
67
|
-
reply = call(self.app, "greeting.set", params)
|
|
68
|
-
self.assertEqual(reply.error["code"], "bad_request")
|
|
69
|
-
self.assertEqual(os.listdir(self.data), [])
|
|
51
|
+
def test_sample_operations_are_absent(self):
|
|
52
|
+
for method in ("greeting.get", "greeting.set"):
|
|
53
|
+
self.assertFalse(call(self.app, method).ok)
|
|
70
54
|
|
|
71
55
|
|
|
72
56
|
if __name__ == "__main__":
|
|
@@ -47,7 +47,7 @@ backend:
|
|
|
47
47
|
runtime: python
|
|
48
48
|
dir: backend/app
|
|
49
49
|
entry: main.py
|
|
50
|
-
sdk: 0.
|
|
50
|
+
sdk: 0.47.0
|
|
51
51
|
|
|
52
52
|
ui:
|
|
53
53
|
# Non-UI-SDK frontends open a full page; Portal is a separate opt-in.
|
|
@@ -91,33 +91,7 @@ capabilities: {}
|
|
|
91
91
|
# backup:
|
|
92
92
|
# hyper_backup: false
|
|
93
93
|
|
|
94
|
-
#
|
|
94
|
+
# Operation metadata has one source. Generate/check before building.
|
|
95
95
|
ctl:
|
|
96
96
|
enabled: true
|
|
97
|
-
|
|
98
|
-
status.show:
|
|
99
|
-
cli: [status]
|
|
100
|
-
summary: Show this App's status and verified caller
|
|
101
|
-
effect: read
|
|
102
|
-
flow: {type: invoke, method: app.status}
|
|
103
|
-
paramsSchema: {type: object, additionalProperties: false}
|
|
104
|
-
examples: [{}]
|
|
105
|
-
greeting.get:
|
|
106
|
-
cli: [greeting, get]
|
|
107
|
-
summary: Read the greeting
|
|
108
|
-
effect: read
|
|
109
|
-
flow: {type: invoke, method: greeting.get}
|
|
110
|
-
paramsSchema: {type: object, additionalProperties: false}
|
|
111
|
-
examples: [{}]
|
|
112
|
-
greeting.set:
|
|
113
|
-
cli: [greeting, set]
|
|
114
|
-
summary: Set the greeting
|
|
115
|
-
effect: write
|
|
116
|
-
flow: {type: invoke, method: greeting.set}
|
|
117
|
-
paramsSchema:
|
|
118
|
-
type: object
|
|
119
|
-
required: [text]
|
|
120
|
-
additionalProperties: false
|
|
121
|
-
properties:
|
|
122
|
-
text: {type: string, minLength: 1, maxLength: 200, description: Greeting text, examples: [Hello]}
|
|
123
|
-
examples: [{text: Hello}]
|
|
97
|
+
registry: operations.json
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "synopkgland.api",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"operations": {
|
|
5
|
+
"example.read": {
|
|
6
|
+
"summary": "Read a resource",
|
|
7
|
+
"ai": "read",
|
|
8
|
+
"cli": [
|
|
9
|
+
"example",
|
|
10
|
+
"read"
|
|
11
|
+
],
|
|
12
|
+
"effect": "read",
|
|
13
|
+
"permission": "administrators",
|
|
14
|
+
"paramsSchema": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
},
|
|
18
|
+
"resultSchema": {
|
|
19
|
+
"type": "object"
|
|
20
|
+
},
|
|
21
|
+
"flow": {
|
|
22
|
+
"type": "invoke",
|
|
23
|
+
"method": "example.read"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"example.preview": {
|
|
27
|
+
"summary": "Preview a resource change",
|
|
28
|
+
"ai": "preview",
|
|
29
|
+
"cli": [
|
|
30
|
+
"example",
|
|
31
|
+
"preview"
|
|
32
|
+
],
|
|
33
|
+
"effect": "read",
|
|
34
|
+
"permission": "administrators",
|
|
35
|
+
"paramsSchema": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": false
|
|
38
|
+
},
|
|
39
|
+
"resultSchema": {
|
|
40
|
+
"type": "object"
|
|
41
|
+
},
|
|
42
|
+
"flow": {
|
|
43
|
+
"type": "invoke",
|
|
44
|
+
"method": "example.preview"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"example.draft": {
|
|
48
|
+
"summary": "Draft a resource change for human confirmation",
|
|
49
|
+
"ai": "draft",
|
|
50
|
+
"cli": [
|
|
51
|
+
"example",
|
|
52
|
+
"draft"
|
|
53
|
+
],
|
|
54
|
+
"effect": "write",
|
|
55
|
+
"permission": "administrators",
|
|
56
|
+
"paramsSchema": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false
|
|
59
|
+
},
|
|
60
|
+
"resultSchema": {
|
|
61
|
+
"type": "object"
|
|
62
|
+
},
|
|
63
|
+
"flow": {
|
|
64
|
+
"type": "invoke",
|
|
65
|
+
"method": "example.draft"
|
|
66
|
+
},
|
|
67
|
+
"preview": "example.preview"
|
|
68
|
+
},
|
|
69
|
+
"example.human": {
|
|
70
|
+
"summary": "Delete a resource after human review",
|
|
71
|
+
"ai": "human-only",
|
|
72
|
+
"cli": [
|
|
73
|
+
"example",
|
|
74
|
+
"human"
|
|
75
|
+
],
|
|
76
|
+
"effect": "destructive",
|
|
77
|
+
"permission": "administrators",
|
|
78
|
+
"paramsSchema": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"additionalProperties": false
|
|
81
|
+
},
|
|
82
|
+
"resultSchema": {
|
|
83
|
+
"type": "object"
|
|
84
|
+
},
|
|
85
|
+
"flow": {
|
|
86
|
+
"type": "invoke",
|
|
87
|
+
"method": "example.human"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"example.hidden": {
|
|
91
|
+
"summary": "Read internal state",
|
|
92
|
+
"ai": "hidden",
|
|
93
|
+
"cli": [
|
|
94
|
+
"example",
|
|
95
|
+
"hidden"
|
|
96
|
+
],
|
|
97
|
+
"effect": "read",
|
|
98
|
+
"permission": "administrators",
|
|
99
|
+
"paramsSchema": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"additionalProperties": false
|
|
102
|
+
},
|
|
103
|
+
"resultSchema": {
|
|
104
|
+
"type": "object"
|
|
105
|
+
},
|
|
106
|
+
"flow": {
|
|
107
|
+
"type": "invoke",
|
|
108
|
+
"method": "example.hidden"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "synopkgland.api",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"operations": {
|
|
5
|
+
"app.status": {
|
|
6
|
+
"summary": "Read App status",
|
|
7
|
+
"ai": "read",
|
|
8
|
+
"cli": [
|
|
9
|
+
"app",
|
|
10
|
+
"status"
|
|
11
|
+
],
|
|
12
|
+
"effect": "read",
|
|
13
|
+
"permission": "administrators",
|
|
14
|
+
"paramsSchema": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
},
|
|
18
|
+
"resultSchema": {
|
|
19
|
+
"type": "object"
|
|
20
|
+
},
|
|
21
|
+
"flow": {
|
|
22
|
+
"type": "invoke",
|
|
23
|
+
"method": "app.status"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"operations": {
|
|
3
|
+
"report.save": {
|
|
4
|
+
"summary": "Save an example report",
|
|
5
|
+
"ai": "hidden",
|
|
6
|
+
"effect": "write",
|
|
7
|
+
"permission": "authenticated",
|
|
8
|
+
"cli": [
|
|
9
|
+
"report",
|
|
10
|
+
"save"
|
|
11
|
+
],
|
|
12
|
+
"paramsSchema": {},
|
|
13
|
+
"resultSchema": {},
|
|
14
|
+
"flow": {
|
|
15
|
+
"type": "invoke",
|
|
16
|
+
"method": "report.save"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|