flowbase-cli 0.1.7__tar.gz → 0.1.9__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowbase-cli
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Command line client for the FlowBase OpenAPI
5
5
  Author: DeepFlowAI
6
6
  License: Apache-2.0
@@ -9,7 +9,7 @@ Description-Content-Type: text/markdown
9
9
 
10
10
  # FlowBase CLI
11
11
 
12
- `flowbase` is the official command line client for the FlowBase OpenAPI. It manages apps, objects, fields, views, custom pages, workflows, dashboards, business records, roles, and configuration versions across the three FlowBase environments.
12
+ `flowbase` is the official command line client for the FlowBase OpenAPI. It manages apps, objects, fields, views, custom pages, workflows, approval flows, dashboards, business records, roles, and configuration versions across the three FlowBase environments.
13
13
 
14
14
  ## 1. Install and connect
15
15
 
@@ -47,7 +47,7 @@ Most commands run on every Host; `roles` is production-only and `changes commit/
47
47
 
48
48
  ### 3.1 Metadata vs records
49
49
 
50
- - **Metadata** (`metadata` scope): apps, objects, fields, views, pages, workflows, dashboards, roles, versions.
50
+ - **Metadata** (`metadata` scope): apps, objects, fields, views, pages, workflows, approvals, dashboards, roles, versions.
51
51
  - **Business records** (`records` scope): rows inside an object.
52
52
 
53
53
  Metadata changes are **drafts** against the current environment's configuration repository. They do not affect the running front-end until they are committed and released.
@@ -77,9 +77,11 @@ flowbase
77
77
  │ add-dashboards | remove-dashboard | set-dashboard-group | reorder-members
78
78
  ├── groups create | reorder | update | delete App groups (under an app)
79
79
  ├── objects list | get | create | update | delete | restore
80
+ │ cli-edit-policy get | update
80
81
  ├── fields create | update | delete | restore Fields under an object
81
82
  ├── views list | create | update | reorder | set-enabled | delete
82
83
  ├── workflows list | get | create | update | reorder | set-enabled | delete | executions
84
+ ├── approvals list | get | create | update | reorder | set-enabled | delete
83
85
  ├── pages list | options | get | create | update | set-default | delete | restore
84
86
  ├── dashboards list | get | create | update | copy | delete
85
87
  ├── changes commit | release | direct-release
@@ -114,6 +116,7 @@ flowbase docs catalogs field-types # field type names + allowed constrain
114
116
  flowbase docs catalogs workflow-nodes # node types with in/out edge rules
115
117
  flowbase docs catalogs permissions # function permission points + data scopes
116
118
  flowbase docs catalogs dashboard-filters # filter types + date-range presets
119
+ flowbase docs catalogs page-components # page region component kinds (field / relatedList / approval)
117
120
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
118
121
  flowbase docs catalogs icons # icon guidance + examples
119
122
  ```
@@ -138,9 +141,11 @@ Do not compose update bodies from scratch when an existing resource exists — `
138
141
  flowbase objects create --label Customer --key customer --description "Customer records"
139
142
  flowbase fields create obj_xxx --json '{"label":"Name","key":"name","description":"Customer name","type":"text.singleLine","permission":"required"}'
140
143
  flowbase records create obj_xxx --json '{"values":{"name":"Acme Corp"}}'
144
+ flowbase objects cli-edit-policy get obj_xxx
145
+ flowbase objects cli-edit-policy update obj_xxx --json '{"baseRevision":4,"policies":{"fld_xxx":"user_approval"}}' --yes
141
146
  ```
142
147
 
143
- Record `values` keys are **field keys** (`name`), not internal IDs (`fld_...`). IDs are `obj_`/`fld_`/`dsh_`/`wfl_`/`pag_`-prefixed; see `docs catalogs id-prefixes`.
148
+ Record `values` keys are **field keys** (`name`), not internal IDs (`fld_...`). IDs are `obj_`/`fld_`/`dsh_`/`wfl_`/`pag_`/`reg_`/`cmp_`-prefixed; see `docs catalogs id-prefixes`.
144
149
 
145
150
  ### 6.2 Edit and publish configuration on a test environment
146
151
 
@@ -167,19 +172,32 @@ flowbase changes direct-release --message "Fix customer field" --yes
167
172
 
168
173
  `direct-release` always runs a server-side preflight, prints the current version, diff summary, checks and risk, and binds a one-time confirmation token. `--yes` accepts that reviewed result; it never skips validation.
169
174
 
170
- ### 6.4 Workflows and pages
175
+ ### 6.4 Workflows, approvals, and pages
171
176
 
172
177
  A workflow graph must satisfy structural rules: exactly one `trigger` node, at least one `end` node, a DAG, and valid per-type edges. See `docs catalogs workflow-nodes` before composing `nodes`/`edges`.
173
178
 
179
+ Approval flow graphs are separate metadata: exactly one `trigger`, at least one `approval` node, both `approvedEnd` and `rejectedEnd`, and dual outcome edges from each approval node. Employee task decisions are not available via API key / CLI. See `docs catalogs approval-nodes`.
180
+
174
181
  ```bash
175
182
  flowbase docs catalogs workflow-nodes
176
183
  flowbase workflows create obj_xxx --json '{"label":"Set owner","enabled":true,"trigger":{"event":"create","match":"all","conditions":[]},"nodes":[],"edges":[]}'
177
184
  flowbase workflows update obj_xxx wfl_xxx --json-file workflow.json
178
185
  flowbase workflows executions obj_xxx wfl_xxx --result failure
179
186
 
180
- flowbase docs example pages.create --output page.json # authoritative layout template
187
+ flowbase docs catalogs approval-nodes
188
+ flowbase docs example approvals.create --output approval.json
189
+ flowbase approvals create obj_xxx --json-file approval.json
190
+ flowbase approvals set-enabled obj_xxx apr_xxx --base-revision 4 --enabled --yes
191
+
192
+ flowbase docs catalogs page-components # field + relatedList + approval keys
193
+ flowbase docs example pages.create --output page.json # create-page layout template
181
194
  flowbase pages create --json-file page.json
182
195
  flowbase pages set-default pag_xxx --base-revision 4 --yes
196
+
197
+ # Detail pages may embed relatedList and at most one approval component.
198
+ flowbase pages get pag_xxx > page.json # prefer get → edit → update
199
+ # or: flowbase docs example pages.update --output page.json
200
+ flowbase pages update pag_xxx --json-file page.json
183
201
  ```
184
202
 
185
203
  ### 6.5 Records and attachments
@@ -1,6 +1,6 @@
1
1
  # FlowBase CLI
2
2
 
3
- `flowbase` is the official command line client for the FlowBase OpenAPI. It manages apps, objects, fields, views, custom pages, workflows, dashboards, business records, roles, and configuration versions across the three FlowBase environments.
3
+ `flowbase` is the official command line client for the FlowBase OpenAPI. It manages apps, objects, fields, views, custom pages, workflows, approval flows, dashboards, business records, roles, and configuration versions across the three FlowBase environments.
4
4
 
5
5
  ## 1. Install and connect
6
6
 
@@ -38,7 +38,7 @@ Most commands run on every Host; `roles` is production-only and `changes commit/
38
38
 
39
39
  ### 3.1 Metadata vs records
40
40
 
41
- - **Metadata** (`metadata` scope): apps, objects, fields, views, pages, workflows, dashboards, roles, versions.
41
+ - **Metadata** (`metadata` scope): apps, objects, fields, views, pages, workflows, approvals, dashboards, roles, versions.
42
42
  - **Business records** (`records` scope): rows inside an object.
43
43
 
44
44
  Metadata changes are **drafts** against the current environment's configuration repository. They do not affect the running front-end until they are committed and released.
@@ -68,9 +68,11 @@ flowbase
68
68
  │ add-dashboards | remove-dashboard | set-dashboard-group | reorder-members
69
69
  ├── groups create | reorder | update | delete App groups (under an app)
70
70
  ├── objects list | get | create | update | delete | restore
71
+ │ cli-edit-policy get | update
71
72
  ├── fields create | update | delete | restore Fields under an object
72
73
  ├── views list | create | update | reorder | set-enabled | delete
73
74
  ├── workflows list | get | create | update | reorder | set-enabled | delete | executions
75
+ ├── approvals list | get | create | update | reorder | set-enabled | delete
74
76
  ├── pages list | options | get | create | update | set-default | delete | restore
75
77
  ├── dashboards list | get | create | update | copy | delete
76
78
  ├── changes commit | release | direct-release
@@ -105,6 +107,7 @@ flowbase docs catalogs field-types # field type names + allowed constrain
105
107
  flowbase docs catalogs workflow-nodes # node types with in/out edge rules
106
108
  flowbase docs catalogs permissions # function permission points + data scopes
107
109
  flowbase docs catalogs dashboard-filters # filter types + date-range presets
110
+ flowbase docs catalogs page-components # page region component kinds (field / relatedList / approval)
108
111
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
109
112
  flowbase docs catalogs icons # icon guidance + examples
110
113
  ```
@@ -129,9 +132,11 @@ Do not compose update bodies from scratch when an existing resource exists — `
129
132
  flowbase objects create --label Customer --key customer --description "Customer records"
130
133
  flowbase fields create obj_xxx --json '{"label":"Name","key":"name","description":"Customer name","type":"text.singleLine","permission":"required"}'
131
134
  flowbase records create obj_xxx --json '{"values":{"name":"Acme Corp"}}'
135
+ flowbase objects cli-edit-policy get obj_xxx
136
+ flowbase objects cli-edit-policy update obj_xxx --json '{"baseRevision":4,"policies":{"fld_xxx":"user_approval"}}' --yes
132
137
  ```
133
138
 
134
- Record `values` keys are **field keys** (`name`), not internal IDs (`fld_...`). IDs are `obj_`/`fld_`/`dsh_`/`wfl_`/`pag_`-prefixed; see `docs catalogs id-prefixes`.
139
+ Record `values` keys are **field keys** (`name`), not internal IDs (`fld_...`). IDs are `obj_`/`fld_`/`dsh_`/`wfl_`/`pag_`/`reg_`/`cmp_`-prefixed; see `docs catalogs id-prefixes`.
135
140
 
136
141
  ### 6.2 Edit and publish configuration on a test environment
137
142
 
@@ -158,19 +163,32 @@ flowbase changes direct-release --message "Fix customer field" --yes
158
163
 
159
164
  `direct-release` always runs a server-side preflight, prints the current version, diff summary, checks and risk, and binds a one-time confirmation token. `--yes` accepts that reviewed result; it never skips validation.
160
165
 
161
- ### 6.4 Workflows and pages
166
+ ### 6.4 Workflows, approvals, and pages
162
167
 
163
168
  A workflow graph must satisfy structural rules: exactly one `trigger` node, at least one `end` node, a DAG, and valid per-type edges. See `docs catalogs workflow-nodes` before composing `nodes`/`edges`.
164
169
 
170
+ Approval flow graphs are separate metadata: exactly one `trigger`, at least one `approval` node, both `approvedEnd` and `rejectedEnd`, and dual outcome edges from each approval node. Employee task decisions are not available via API key / CLI. See `docs catalogs approval-nodes`.
171
+
165
172
  ```bash
166
173
  flowbase docs catalogs workflow-nodes
167
174
  flowbase workflows create obj_xxx --json '{"label":"Set owner","enabled":true,"trigger":{"event":"create","match":"all","conditions":[]},"nodes":[],"edges":[]}'
168
175
  flowbase workflows update obj_xxx wfl_xxx --json-file workflow.json
169
176
  flowbase workflows executions obj_xxx wfl_xxx --result failure
170
177
 
171
- flowbase docs example pages.create --output page.json # authoritative layout template
178
+ flowbase docs catalogs approval-nodes
179
+ flowbase docs example approvals.create --output approval.json
180
+ flowbase approvals create obj_xxx --json-file approval.json
181
+ flowbase approvals set-enabled obj_xxx apr_xxx --base-revision 4 --enabled --yes
182
+
183
+ flowbase docs catalogs page-components # field + relatedList + approval keys
184
+ flowbase docs example pages.create --output page.json # create-page layout template
172
185
  flowbase pages create --json-file page.json
173
186
  flowbase pages set-default pag_xxx --base-revision 4 --yes
187
+
188
+ # Detail pages may embed relatedList and at most one approval component.
189
+ flowbase pages get pag_xxx > page.json # prefer get → edit → update
190
+ # or: flowbase docs example pages.update --output page.json
191
+ flowbase pages update pag_xxx --json-file page.json
174
192
  ```
175
193
 
176
194
  ### 6.5 Records and attachments
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flowbase-cli"
7
- version = "0.1.7"
7
+ version = "0.1.9"
8
8
  description = "Command line client for the FlowBase OpenAPI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """FlowBase OpenAPI CLI."""
2
2
 
3
- __version__ = "0.1.7"
3
+ __version__ = "0.1.9"
@@ -173,6 +173,29 @@ ENDPOINTS = (
173
173
  request_body='{"label":"Customer","description":"Customer records","icon":"IconDatabase"}'),
174
174
  Endpoint("objects.delete", "flowbase_objects_delete", "DELETE", "/objects/{object_id}", "metadata:write", "Logically delete an object.", "flowbase objects delete obj_xxx --yes", True),
175
175
  Endpoint("objects.restore", "flowbase_objects_restore", "POST", "/objects/{object_id}/restore", "metadata:write", "Restore an object.", "flowbase objects restore obj_xxx --yes", True),
176
+ Endpoint(
177
+ "objects.cli-edit-policy.get",
178
+ "flowbase_objects_cli_edit_policy_get",
179
+ "GET",
180
+ "/objects/{object_id}/cli-edit-policy",
181
+ "metadata:read",
182
+ "Get object CLI field edit policies.",
183
+ "flowbase objects cli-edit-policy get obj_xxx",
184
+ ),
185
+ Endpoint(
186
+ "objects.cli-edit-policy.update",
187
+ "flowbase_objects_cli_edit_policy_update",
188
+ "PUT",
189
+ "/objects/{object_id}/cli-edit-policy",
190
+ "metadata:write",
191
+ "Update object CLI field edit policies as a complete draft snapshot.",
192
+ "flowbase objects cli-edit-policy update obj_xxx --json-file cli-policy.json --yes",
193
+ requires_confirmation=True,
194
+ request_body=(
195
+ '{"baseRevision":4,"policies":{'
196
+ '"fld_confirm":"user_approval","fld_direct":"direct"}}'
197
+ ),
198
+ ),
176
199
  Endpoint("fields.create", "flowbase_fields_create", "POST", "/objects/{object_id}/fields", "metadata:write", "Create a field draft.", "flowbase fields create obj_xxx --json-file field.json",
177
200
  request_body=(
178
201
  '{"label":"Stage","key":"stage","description":"Pipeline stage","type":"select.single","typeVersion":"v1",'
@@ -222,6 +245,48 @@ ENDPOINTS = (
222
245
  Endpoint("workflows.delete", "flowbase_workflows_delete", "POST", "/objects/{object_id}/workflows/{workflow_id}/delete", "metadata:write", "Logically delete a workflow.", "flowbase workflows delete obj_xxx wfl_xxx --base-revision 4 --yes", True,
223
246
  request_body='{"baseRevision":4}'),
224
247
  Endpoint("workflows.executions", "flowbase_workflows_executions", "GET", "/objects/{object_id}/workflows/{workflow_id}/executions", "metadata:read", "List workflow execution records.", "flowbase workflows executions obj_xxx wfl_xxx --result failure"),
248
+ Endpoint("approvals.list", "flowbase_approvals_list", "GET", "/objects/{object_id}/approvals", "metadata:read", "List object approval flows.", "flowbase approvals list obj_xxx"),
249
+ Endpoint("approvals.get", "flowbase_approvals_get", "GET", "/objects/{object_id}/approvals/{approval_id}", "metadata:read", "Get an approval flow graph.", "flowbase approvals get obj_xxx apr_xxx"),
250
+ Endpoint("approvals.create", "flowbase_approvals_create", "POST", "/objects/{object_id}/approvals", "metadata:write", "Create an approval flow draft.", "flowbase approvals create obj_xxx --json-file approval.json",
251
+ request_body=(
252
+ '{"baseRevision":3,"label":"Contract approval","enabled":true,'
253
+ '"trigger":{"manual":true,"create":false,"update":false,"match":"all","conditions":[]},'
254
+ '"displayFieldIds":["fld_xxx"],"lockRecord":true,"allowWithdraw":true,'
255
+ '"nodes":['
256
+ '{"id":"apn_trigger","type":"trigger"},'
257
+ '{"id":"apn_review","type":"approval","label":"Manager review",'
258
+ '"approverRules":[{"type":"employees","employeeIds":["emp_xxx"]}],'
259
+ '"mode":"all","selfPolicy":"skip","unavailablePolicy":"autoReject"},'
260
+ '{"id":"apn_approved","type":"approvedEnd"},'
261
+ '{"id":"apn_rejected","type":"rejectedEnd"}],'
262
+ '"edges":['
263
+ '{"id":"ape_1","source":"apn_trigger","target":"apn_review"},'
264
+ '{"id":"ape_2","source":"apn_review","target":"apn_approved","outcome":"approved"},'
265
+ '{"id":"ape_3","source":"apn_review","target":"apn_rejected","outcome":"rejected"}]}'
266
+ )),
267
+ Endpoint("approvals.update", "flowbase_approvals_update", "PUT", "/objects/{object_id}/approvals/{approval_id}", "metadata:write", "Update an approval flow graph.", "flowbase approvals update obj_xxx apr_xxx --json-file approval.json",
268
+ request_body=(
269
+ '{"baseRevision":4,"label":"Contract approval","enabled":true,'
270
+ '"trigger":{"manual":true,"create":false,"update":false,"match":"all","conditions":[]},'
271
+ '"displayFieldIds":["fld_xxx"],"lockRecord":true,"allowWithdraw":true,'
272
+ '"nodes":['
273
+ '{"id":"apn_trigger","type":"trigger"},'
274
+ '{"id":"apn_review","type":"approval","label":"Manager review",'
275
+ '"approverRules":[{"type":"employees","employeeIds":["emp_xxx"]}],'
276
+ '"mode":"all","selfPolicy":"skip","unavailablePolicy":"autoReject"},'
277
+ '{"id":"apn_approved","type":"approvedEnd"},'
278
+ '{"id":"apn_rejected","type":"rejectedEnd"}],'
279
+ '"edges":['
280
+ '{"id":"ape_1","source":"apn_trigger","target":"apn_review"},'
281
+ '{"id":"ape_2","source":"apn_review","target":"apn_approved","outcome":"approved"},'
282
+ '{"id":"ape_3","source":"apn_review","target":"apn_rejected","outcome":"rejected"}]}'
283
+ )),
284
+ Endpoint("approvals.reorder", "flowbase_approvals_reorder", "PUT", "/objects/{object_id}/approvals/order", "metadata:write", "Reorder object approval flows.", "flowbase approvals reorder obj_xxx --json '{\"baseRevision\":4,\"approvalIds\":[\"apr_a\",\"apr_b\"]}'",
285
+ request_body='{"baseRevision":4,"approvalIds":["apr_a","apr_b"]}'),
286
+ Endpoint("approvals.set-enabled", "flowbase_approvals_set_enabled", "POST", "/objects/{object_id}/approvals/{approval_id}/enabled", "metadata:write", "Enable or disable an approval flow.", "flowbase approvals set-enabled obj_xxx apr_xxx --base-revision 4 --enabled --yes", True,
287
+ request_body='{"baseRevision":4,"enabled":true}'),
288
+ Endpoint("approvals.delete", "flowbase_approvals_delete", "POST", "/objects/{object_id}/approvals/{approval_id}/delete", "metadata:write", "Logically delete an approval flow.", "flowbase approvals delete obj_xxx apr_xxx --base-revision 4 --yes", True,
289
+ request_body='{"baseRevision":4}'),
225
290
  Endpoint("pages.list", "flowbase_pages_list", "GET", "/pages", "metadata:read", "List custom Page metadata.", "flowbase pages list --object-id obj_xxx"),
226
291
  Endpoint("pages.options", "flowbase_pages_options", "GET", "/pages/options", "metadata:read", "Get custom Page design options.", "flowbase pages options"),
227
292
  Endpoint("pages.get", "flowbase_pages_get", "GET", "/pages/{page_id}", "metadata:read", "Get a custom Page.", "flowbase pages get pag_xxx"),
@@ -233,9 +298,17 @@ ENDPOINTS = (
233
298
  )),
234
299
  Endpoint("pages.update", "flowbase_pages_update", "PUT", "/pages/{page_id}", "metadata:write", "Update a custom Page draft.", "flowbase pages update pag_xxx --json-file page.json",
235
300
  request_body=(
236
- '{"baseRevision":4,"page":{"id":"pag_xxx","key":"customer_create","label":"Create customer",'
237
- '"description":"","objectId":"obj_xxx","type":"create",'
238
- '"regions":[{"id":"reg_xxx","label":"Main","hideLabel":true,"columnRatios":[1],"columns":[[]]}],"rules":[]}}'
301
+ '{"baseRevision":4,"page":{"id":"pag_xxx","key":"customer_detail","label":"Customer detail",'
302
+ '"description":"","objectId":"obj_customer","type":"detail",'
303
+ '"regions":[{"id":"reg_xxx","label":"Related","hideLabel":false,"columnRatios":[1],'
304
+ '"columns":[[{"kind":"relatedList","label":"Tickets","hideLabel":false,'
305
+ '"objectId":"obj_ticket","relationFieldId":"fld_customer",'
306
+ '"filters":[{"fieldId":"fld_status","operator":"neq","value":"done"}],'
307
+ '"columnFieldIds":["fld_title","fld_status","fld_created_at"],'
308
+ '"sort":{"fieldId":"fld_created_at","direction":"desc"},"limit":5,"showViewAll":true,'
309
+ '"createAction":{"enabled":true,"label":"New ticket"},'
310
+ '"carryIns":[{"fieldId":"fld_customer","source":"currentRecord"}]},'
311
+ '{"kind":"approval","label":"Approval","hideLabel":false}]]}],"rules":[]}}'
239
312
  )),
240
313
  Endpoint("pages.set-default", "flowbase_pages_set_default", "POST", "/pages/{page_id}/set-default", "metadata:write", "Set a default custom Page.", "flowbase pages set-default pag_xxx --base-revision 4 --yes", True,
241
314
  request_body='{"baseRevision":4}'),
@@ -384,9 +457,9 @@ ENDPOINTS = (
384
457
  ENDPOINT_BY_KEY = {endpoint.key: endpoint for endpoint in ENDPOINTS}
385
458
 
386
459
  # Closed-domain catalogs for offline authoring of complex request bodies.
387
- # Keep in sync with server registries (field types, workflow node kinds,
388
- # permission points, dashboard filter presets). Icons intentionally reference
389
- # the server file because the full Tabler set is large.
460
+ # Keep in sync with server registries (field types, workflow/approval node kinds,
461
+ # permission points, dashboard filter presets, page component kinds).
462
+ # Icons intentionally reference the server file because the full Tabler set is large.
390
463
  FIELD_TYPES = (
391
464
  {"type": "text.singleLine", "constraints": ["maxLength"], "writable": True},
392
465
  {"type": "text.multiLine", "constraints": ["maxLength"], "writable": True},
@@ -406,6 +479,13 @@ FIELD_TYPES = (
406
479
  {"type": "url", "constraints": ["maxLength", "placeholder"], "writable": True},
407
480
  {"type": "attachment", "constraints": ["maxFileCount", "maxFileSize", "maxTotalSize", "allowedExtensions"], "writable": True},
408
481
  {"type": "text.rich", "constraints": ["maxLength", "placeholder"], "writable": True},
482
+ {"type": "autonumber", "constraints": ["prefix", "start", "minDigits"], "writable": False},
483
+ {"type": "number.currency", "constraints": ["currency", "minimum", "maximum", "scale", "currencyDisplay", "useThousands"], "writable": True},
484
+ {"type": "number.percent", "constraints": ["minimum", "maximum", "scale"], "writable": True},
485
+ {"type": "duration", "constraints": ["precision", "minimum", "maximum", "displayFormat"], "writable": True},
486
+ {"type": "rating", "constraints": ["maxRating", "color"], "writable": True},
487
+ {"type": "lookup.record.multiple", "constraints": ["targetObjectId", "displayFieldId", "maxSelections"], "writable": True},
488
+ {"type": "lookup.department", "constraints": [], "writable": True},
409
489
  )
410
490
 
411
491
  WORKFLOW_NODE_TYPES = (
@@ -419,6 +499,22 @@ WORKFLOW_NODE_TYPES = (
419
499
  "note": "terminal node"},
420
500
  )
421
501
 
502
+ APPROVAL_NODE_TYPES = (
503
+ {"type": "trigger", "cardinality": "exactly 1", "in": 0, "out": 1,
504
+ "note": "entry node; initiation modes live on the flow trigger, not the node"},
505
+ {"type": "approval", "cardinality": ">= 1", "in": 1, "out": 2,
506
+ "note": "requires approved + rejected outcome edges; approverRules / mode / "
507
+ "selfPolicy / unavailablePolicy"},
508
+ {"type": "branch", "cardinality": ">= 0", "in": 1, "out": "1 per branch (+ defaultBranchId)",
509
+ "note": "same condition protocol as workflows; each branchId needs an edge"},
510
+ {"type": "updateRecord", "cardinality": ">= 0", "in": 1, "out": 1,
511
+ "note": "assignments update the locked record as system actor"},
512
+ {"type": "approvedEnd", "cardinality": ">= 1", "in": "any", "out": 0,
513
+ "note": "terminal approved outcome"},
514
+ {"type": "rejectedEnd", "cardinality": ">= 1", "in": "any", "out": 0,
515
+ "note": "terminal rejected outcome"},
516
+ )
517
+
422
518
  FUNCTION_PERMISSIONS = (
423
519
  "workspace.access", "app.manage", "object.manage", "dashboard.manage",
424
520
  "version.manage", "org.manage", "system.manage",
@@ -432,9 +528,76 @@ DASHBOARD_DATE_RANGE_PRESETS = (
432
528
  "this_week", "this_month", "this_quarter", "this_year",
433
529
  )
434
530
 
531
+ # Page region column components (field + relatedList + approval).
532
+ # Field components omit `kind` (legacy snapshot shape); relatedList/approval set kind.
533
+ PAGE_COMPONENTS = {
534
+ "note": (
535
+ "Region columns hold field, relatedList, and/or approval components. "
536
+ "relatedList and approval are allowed only on detail pages; each detail "
537
+ "page may include at most one approval component. Prefer pages.get → "
538
+ "edit → pages.update; use pages.options to resolve candidate objects/fields."
539
+ ),
540
+ "kinds": (
541
+ {
542
+ "kind": None,
543
+ "name": "field",
544
+ "note": "Omit kind. Places fields of sourceObjectId on a grid.",
545
+ "requiredKeys": (
546
+ "label", "hideLabel", "sourceObjectId", "grid",
547
+ "alignment", "labelPosition", "labelWidth", "fields",
548
+ ),
549
+ },
550
+ {
551
+ "kind": "relatedList",
552
+ "name": "related record list",
553
+ "pageTypes": ("detail",),
554
+ "requiredKeys": (
555
+ "kind", "label", "hideLabel", "objectId", "relationFieldId",
556
+ "columnFieldIds", "sort", "limit", "showViewAll", "createAction",
557
+ ),
558
+ "optionalKeys": ("id", "filters", "carryIns"),
559
+ "limit": (5, 10, 20),
560
+ "sortDirections": ("asc", "desc"),
561
+ "carryInSources": (
562
+ {
563
+ "source": "currentRecord",
564
+ "note": "Value is the current record id; target field must be "
565
+ "lookup.record pointing at the page object. When create "
566
+ "is enabled, relationFieldId must appear exactly once "
567
+ "with this source.",
568
+ },
569
+ {
570
+ "source": "currentRecordField",
571
+ "requires": "sourceFieldId",
572
+ "note": "Copy an active field from the current record; source "
573
+ "and target field types must match.",
574
+ },
575
+ ),
576
+ "filters": {
577
+ "shape": {"fieldId": "fld_...", "operator": "...", "value": "..."},
578
+ "note": "Same filter protocol as object views; AND-merged with the "
579
+ "implicit relationFieldId eq currentRecord condition.",
580
+ },
581
+ },
582
+ {
583
+ "kind": "approval",
584
+ "name": "record approval",
585
+ "pageTypes": ("detail",),
586
+ "maxPerPage": 1,
587
+ "requiredKeys": ("kind", "label", "hideLabel"),
588
+ "optionalKeys": ("id",),
589
+ "note": (
590
+ "Reference-free presentation slot for submit / status / history. "
591
+ "Runtime data comes from employee APIs, not OpenAPI/CLI."
592
+ ),
593
+ },
594
+ ),
595
+ }
596
+
435
597
  RESOURCE_ID_PREFIXES = {
436
598
  "app_": "app", "grp_": "app group", "obj_": "object", "fld_": "field",
437
- "viw_": "view", "wfl_": "workflow", "pag_": "custom page",
599
+ "viw_": "view", "wfl_": "workflow", "apr_": "approval flow", "pag_": "custom page",
600
+ "reg_": "page region", "cmp_": "page component",
438
601
  "dsh_": "dashboard", "dcm_": "dashboard component", "rol_": "role",
439
602
  "rec_": "record", "att_": "attachment", "emp_": "employee", "dep_": "department",
440
603
  }
@@ -442,11 +605,13 @@ RESOURCE_ID_PREFIXES = {
442
605
  CATALOGS = {
443
606
  "field-types": FIELD_TYPES,
444
607
  "workflow-nodes": WORKFLOW_NODE_TYPES,
608
+ "approval-nodes": APPROVAL_NODE_TYPES,
445
609
  "permissions": {"function": FUNCTION_PERMISSIONS, "dataScopes": DATA_SCOPES},
446
610
  "dashboard-filters": {
447
611
  "types": DASHBOARD_FILTER_TYPES,
448
612
  "dateRangePresets": DASHBOARD_DATE_RANGE_PRESETS,
449
613
  },
614
+ "page-components": PAGE_COMPONENTS,
450
615
  "id-prefixes": RESOURCE_ID_PREFIXES,
451
616
  "icons": {
452
617
  "note": "6146 Tabler icon names are supported; any Icon* name from the "
@@ -523,6 +523,22 @@ def handle_objects(args: argparse.Namespace) -> int:
523
523
  return run_request(args, "objects.list", query={"status": args.status})
524
524
  if action == "get":
525
525
  return run_request(args, "objects.get", path_params={"object_id": args.object_id}, query={"status": args.status})
526
+ if action == "cli-edit-policy":
527
+ path = {"object_id": args.object_id}
528
+ if args.objects_cli_edit_policy_action == "get":
529
+ return run_request(
530
+ args, "objects.cli-edit-policy.get", path_params=path
531
+ )
532
+ body = load_json_payload(args.json, args.json_file)
533
+ require_fields(body, "baseRevision", "policies")
534
+ if not isinstance(body["policies"], dict):
535
+ raise CliError("Request field 'policies' must be an object.")
536
+ return run_request(
537
+ args,
538
+ "objects.cli-edit-policy.update",
539
+ path_params=path,
540
+ body=body,
541
+ )
526
542
  if action == "create":
527
543
  body = _payload_from_args(
528
544
  args,
@@ -638,6 +654,34 @@ def handle_workflows(args: argparse.Namespace) -> int:
638
654
  )
639
655
 
640
656
 
657
+ def handle_approvals(args: argparse.Namespace) -> int:
658
+ action = args.approvals_action
659
+ path = {"object_id": args.object_id}
660
+ if hasattr(args, "approval_id"):
661
+ path["approval_id"] = args.approval_id
662
+ if action in ("list", "get"):
663
+ return run_request(args, f"approvals.{action}", path_params=path)
664
+ if action in ("create", "update"):
665
+ body = load_json_payload(args.json, args.json_file)
666
+ require_fields(body, "label", "trigger", "displayFieldIds", "nodes", "edges")
667
+ return run_request(args, f"approvals.{action}", path_params=path, body=body)
668
+ if action == "reorder":
669
+ body = load_json_payload(args.json, args.json_file)
670
+ require_fields(body, "baseRevision", "approvalIds")
671
+ if not isinstance(body["approvalIds"], list):
672
+ raise CliError("Request field 'approvalIds' must be an array.")
673
+ return run_request(args, "approvals.reorder", path_params=path, body=body)
674
+ if action == "set-enabled":
675
+ return run_request(
676
+ args, "approvals.set-enabled", path_params=path,
677
+ body={"baseRevision": args.base_revision, "enabled": args.enabled},
678
+ )
679
+ return run_request(
680
+ args, "approvals.delete", path_params=path,
681
+ body={"baseRevision": args.base_revision},
682
+ )
683
+
684
+
641
685
  def handle_pages(args: argparse.Namespace) -> int:
642
686
  action = args.pages_action
643
687
  if action == "list":
@@ -906,7 +950,7 @@ def build_parser() -> argparse.ArgumentParser:
906
950
  docs_actions.add_parser("operations")
907
951
  docs_catalogs = docs_actions.add_parser(
908
952
  "catalogs",
909
- help="List closed-domain catalogs, or dump one (field-types, workflow-nodes, permissions, dashboard-filters, id-prefixes, icons).",
953
+ help="List closed-domain catalogs, or dump one (field-types, workflow-nodes, approval-nodes, permissions, dashboard-filters, page-components, id-prefixes, icons).",
910
954
  )
911
955
  docs_catalogs.add_argument("name", nargs="?", default=None)
912
956
  docs_show = docs_actions.add_parser("show")
@@ -1054,6 +1098,21 @@ def build_parser() -> argparse.ArgumentParser:
1054
1098
  item = object_actions.add_parser(action)
1055
1099
  item.add_argument("object_id")
1056
1100
  add_yes(item)
1101
+ object_cli_policy = object_actions.add_parser(
1102
+ "cli-edit-policy", help="Read or update user CLI field edit policies."
1103
+ )
1104
+ object_cli_policy_actions = object_cli_policy.add_subparsers(
1105
+ dest="objects_cli_edit_policy_action", required=True
1106
+ )
1107
+ object_cli_policy_get = object_cli_policy_actions.add_parser("get")
1108
+ object_cli_policy_get.add_argument("object_id")
1109
+ object_cli_policy_update = object_cli_policy_actions.add_parser("update")
1110
+ object_cli_policy_update.add_argument("object_id")
1111
+ add_json_args(
1112
+ object_cli_policy_update,
1113
+ endpoint_key="objects.cli-edit-policy.update",
1114
+ )
1115
+ add_yes(object_cli_policy_update)
1057
1116
  objects.set_defaults(handler=handle_objects)
1058
1117
 
1059
1118
  fields = groups.add_parser("fields", help="Manage field metadata.")
@@ -1137,6 +1196,36 @@ def build_parser() -> argparse.ArgumentParser:
1137
1196
  workflow_executions.add_argument("--result", choices=("success", "failure"))
1138
1197
  workflows.set_defaults(handler=handle_workflows)
1139
1198
 
1199
+ approvals = groups.add_parser("approvals", help="Manage object approval flow metadata.")
1200
+ approval_actions = approvals.add_subparsers(dest="approvals_action", required=True)
1201
+ approval_list = approval_actions.add_parser("list")
1202
+ approval_list.add_argument("object_id")
1203
+ approval_get = approval_actions.add_parser("get")
1204
+ approval_get.add_argument("object_id")
1205
+ approval_get.add_argument("approval_id")
1206
+ for action in ("create", "reorder"):
1207
+ item = approval_actions.add_parser(action)
1208
+ item.add_argument("object_id")
1209
+ add_json_args(item, endpoint_key=f"approvals.{action}")
1210
+ approval_update = approval_actions.add_parser("update")
1211
+ approval_update.add_argument("object_id")
1212
+ approval_update.add_argument("approval_id")
1213
+ add_json_args(approval_update, endpoint_key="approvals.update")
1214
+ approval_enabled = approval_actions.add_parser("set-enabled")
1215
+ approval_enabled.add_argument("object_id")
1216
+ approval_enabled.add_argument("approval_id")
1217
+ approval_enabled.add_argument("--base-revision", required=True, type=int)
1218
+ approval_enabled.add_argument(
1219
+ "--enabled", action=argparse.BooleanOptionalAction, required=True,
1220
+ )
1221
+ add_yes(approval_enabled)
1222
+ approval_delete = approval_actions.add_parser("delete")
1223
+ approval_delete.add_argument("object_id")
1224
+ approval_delete.add_argument("approval_id")
1225
+ approval_delete.add_argument("--base-revision", required=True, type=int)
1226
+ add_yes(approval_delete)
1227
+ approvals.set_defaults(handler=handle_approvals)
1228
+
1140
1229
  pages = groups.add_parser("pages", help="Manage custom Page metadata.")
1141
1230
  page_actions = pages.add_subparsers(dest="pages_action", required=True)
1142
1231
  page_list = page_actions.add_parser("list")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowbase-cli
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Command line client for the FlowBase OpenAPI
5
5
  Author: DeepFlowAI
6
6
  License: Apache-2.0
@@ -9,7 +9,7 @@ Description-Content-Type: text/markdown
9
9
 
10
10
  # FlowBase CLI
11
11
 
12
- `flowbase` is the official command line client for the FlowBase OpenAPI. It manages apps, objects, fields, views, custom pages, workflows, dashboards, business records, roles, and configuration versions across the three FlowBase environments.
12
+ `flowbase` is the official command line client for the FlowBase OpenAPI. It manages apps, objects, fields, views, custom pages, workflows, approval flows, dashboards, business records, roles, and configuration versions across the three FlowBase environments.
13
13
 
14
14
  ## 1. Install and connect
15
15
 
@@ -47,7 +47,7 @@ Most commands run on every Host; `roles` is production-only and `changes commit/
47
47
 
48
48
  ### 3.1 Metadata vs records
49
49
 
50
- - **Metadata** (`metadata` scope): apps, objects, fields, views, pages, workflows, dashboards, roles, versions.
50
+ - **Metadata** (`metadata` scope): apps, objects, fields, views, pages, workflows, approvals, dashboards, roles, versions.
51
51
  - **Business records** (`records` scope): rows inside an object.
52
52
 
53
53
  Metadata changes are **drafts** against the current environment's configuration repository. They do not affect the running front-end until they are committed and released.
@@ -77,9 +77,11 @@ flowbase
77
77
  │ add-dashboards | remove-dashboard | set-dashboard-group | reorder-members
78
78
  ├── groups create | reorder | update | delete App groups (under an app)
79
79
  ├── objects list | get | create | update | delete | restore
80
+ │ cli-edit-policy get | update
80
81
  ├── fields create | update | delete | restore Fields under an object
81
82
  ├── views list | create | update | reorder | set-enabled | delete
82
83
  ├── workflows list | get | create | update | reorder | set-enabled | delete | executions
84
+ ├── approvals list | get | create | update | reorder | set-enabled | delete
83
85
  ├── pages list | options | get | create | update | set-default | delete | restore
84
86
  ├── dashboards list | get | create | update | copy | delete
85
87
  ├── changes commit | release | direct-release
@@ -114,6 +116,7 @@ flowbase docs catalogs field-types # field type names + allowed constrain
114
116
  flowbase docs catalogs workflow-nodes # node types with in/out edge rules
115
117
  flowbase docs catalogs permissions # function permission points + data scopes
116
118
  flowbase docs catalogs dashboard-filters # filter types + date-range presets
119
+ flowbase docs catalogs page-components # page region component kinds (field / relatedList / approval)
117
120
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
118
121
  flowbase docs catalogs icons # icon guidance + examples
119
122
  ```
@@ -138,9 +141,11 @@ Do not compose update bodies from scratch when an existing resource exists — `
138
141
  flowbase objects create --label Customer --key customer --description "Customer records"
139
142
  flowbase fields create obj_xxx --json '{"label":"Name","key":"name","description":"Customer name","type":"text.singleLine","permission":"required"}'
140
143
  flowbase records create obj_xxx --json '{"values":{"name":"Acme Corp"}}'
144
+ flowbase objects cli-edit-policy get obj_xxx
145
+ flowbase objects cli-edit-policy update obj_xxx --json '{"baseRevision":4,"policies":{"fld_xxx":"user_approval"}}' --yes
141
146
  ```
142
147
 
143
- Record `values` keys are **field keys** (`name`), not internal IDs (`fld_...`). IDs are `obj_`/`fld_`/`dsh_`/`wfl_`/`pag_`-prefixed; see `docs catalogs id-prefixes`.
148
+ Record `values` keys are **field keys** (`name`), not internal IDs (`fld_...`). IDs are `obj_`/`fld_`/`dsh_`/`wfl_`/`pag_`/`reg_`/`cmp_`-prefixed; see `docs catalogs id-prefixes`.
144
149
 
145
150
  ### 6.2 Edit and publish configuration on a test environment
146
151
 
@@ -167,19 +172,32 @@ flowbase changes direct-release --message "Fix customer field" --yes
167
172
 
168
173
  `direct-release` always runs a server-side preflight, prints the current version, diff summary, checks and risk, and binds a one-time confirmation token. `--yes` accepts that reviewed result; it never skips validation.
169
174
 
170
- ### 6.4 Workflows and pages
175
+ ### 6.4 Workflows, approvals, and pages
171
176
 
172
177
  A workflow graph must satisfy structural rules: exactly one `trigger` node, at least one `end` node, a DAG, and valid per-type edges. See `docs catalogs workflow-nodes` before composing `nodes`/`edges`.
173
178
 
179
+ Approval flow graphs are separate metadata: exactly one `trigger`, at least one `approval` node, both `approvedEnd` and `rejectedEnd`, and dual outcome edges from each approval node. Employee task decisions are not available via API key / CLI. See `docs catalogs approval-nodes`.
180
+
174
181
  ```bash
175
182
  flowbase docs catalogs workflow-nodes
176
183
  flowbase workflows create obj_xxx --json '{"label":"Set owner","enabled":true,"trigger":{"event":"create","match":"all","conditions":[]},"nodes":[],"edges":[]}'
177
184
  flowbase workflows update obj_xxx wfl_xxx --json-file workflow.json
178
185
  flowbase workflows executions obj_xxx wfl_xxx --result failure
179
186
 
180
- flowbase docs example pages.create --output page.json # authoritative layout template
187
+ flowbase docs catalogs approval-nodes
188
+ flowbase docs example approvals.create --output approval.json
189
+ flowbase approvals create obj_xxx --json-file approval.json
190
+ flowbase approvals set-enabled obj_xxx apr_xxx --base-revision 4 --enabled --yes
191
+
192
+ flowbase docs catalogs page-components # field + relatedList + approval keys
193
+ flowbase docs example pages.create --output page.json # create-page layout template
181
194
  flowbase pages create --json-file page.json
182
195
  flowbase pages set-default pag_xxx --base-revision 4 --yes
196
+
197
+ # Detail pages may embed relatedList and at most one approval component.
198
+ flowbase pages get pag_xxx > page.json # prefer get → edit → update
199
+ # or: flowbase docs example pages.update --output page.json
200
+ flowbase pages update pag_xxx --json-file page.json
183
201
  ```
184
202
 
185
203
  ### 6.5 Records and attachments
@@ -90,6 +90,8 @@ class FlowBaseCliTest(unittest.TestCase):
90
90
  self.assertTrue(any(item["key"] == "pages.create" for item in payload))
91
91
  self.assertTrue(any(item["key"] == "views.create" for item in payload))
92
92
  self.assertTrue(any(item["key"] == "roles.create" for item in payload))
93
+ self.assertTrue(any(item["key"] == "objects.cli-edit-policy.get" for item in payload))
94
+ self.assertTrue(any(item["key"] == "objects.cli-edit-policy.update" for item in payload))
93
95
  role_create = next(item for item in payload if item["key"] == "roles.create")
94
96
  self.assertEqual(role_create["environments"], ["production"])
95
97
 
@@ -110,6 +112,22 @@ class FlowBaseCliTest(unittest.TestCase):
110
112
  payload = json.loads(stdout.getvalue())
111
113
  self.assertIn("regions", payload["requestBody"])
112
114
 
115
+ stdout = io.StringIO()
116
+ with patch("sys.stdout", stdout):
117
+ code = main(["docs", "show", "pages.update"])
118
+ self.assertEqual(code, 0)
119
+ payload = json.loads(stdout.getvalue())
120
+ body = json.loads(payload["requestBody"])
121
+ self.assertEqual(body["page"]["type"], "detail")
122
+ components = body["page"]["regions"][0]["columns"][0]
123
+ self.assertEqual(components[0]["kind"], "relatedList")
124
+ self.assertIn("relationFieldId", components[0])
125
+ self.assertIn("carryIns", components[0])
126
+ self.assertEqual(
127
+ components[1],
128
+ {"kind": "approval", "label": "Approval", "hideLabel": False},
129
+ )
130
+
113
131
  def test_docs_catalogs_lists_and_dumps_domains(self):
114
132
  stdout = io.StringIO()
115
133
  with patch("sys.stdout", stdout):
@@ -118,7 +136,9 @@ class FlowBaseCliTest(unittest.TestCase):
118
136
  names = json.loads(stdout.getvalue())
119
137
  self.assertIn("field-types", names)
120
138
  self.assertIn("workflow-nodes", names)
139
+ self.assertIn("approval-nodes", names)
121
140
  self.assertIn("permissions", names)
141
+ self.assertIn("page-components", names)
122
142
 
123
143
  stdout = io.StringIO()
124
144
  with patch("sys.stdout", stdout):
@@ -126,8 +146,15 @@ class FlowBaseCliTest(unittest.TestCase):
126
146
  self.assertEqual(code, 0)
127
147
  field_types = json.loads(stdout.getvalue())
128
148
  type_names = {item["type"] for item in field_types}
149
+ self.assertEqual(len(field_types), 25)
129
150
  self.assertIn("select.single", type_names)
130
151
  self.assertIn("lookup.employee", type_names)
152
+ self.assertTrue({
153
+ "autonumber", "number.currency", "number.percent", "duration",
154
+ "rating", "lookup.record.multiple", "lookup.department",
155
+ }.issubset(type_names))
156
+ autonumber = next(item for item in field_types if item["type"] == "autonumber")
157
+ self.assertFalse(autonumber["writable"])
131
158
 
132
159
  stdout = io.StringIO()
133
160
  with patch("sys.stdout", stdout):
@@ -136,6 +163,40 @@ class FlowBaseCliTest(unittest.TestCase):
136
163
  nodes = json.loads(stdout.getvalue())
137
164
  self.assertIn("trigger", {item["type"] for item in nodes})
138
165
 
166
+ stdout = io.StringIO()
167
+ with patch("sys.stdout", stdout):
168
+ code = main(["docs", "catalogs", "approval-nodes"])
169
+ self.assertEqual(code, 0)
170
+ approval_nodes = json.loads(stdout.getvalue())
171
+ self.assertTrue(
172
+ {"trigger", "approval", "approvedEnd", "rejectedEnd"}.issubset(
173
+ {item["type"] for item in approval_nodes}
174
+ )
175
+ )
176
+
177
+ stdout = io.StringIO()
178
+ with patch("sys.stdout", stdout):
179
+ code = main(["docs", "catalogs", "page-components"])
180
+ self.assertEqual(code, 0)
181
+ page_components = json.loads(stdout.getvalue())
182
+ kinds = {item.get("kind") for item in page_components["kinds"]}
183
+ self.assertIn(None, kinds)
184
+ self.assertIn("relatedList", kinds)
185
+ self.assertIn("approval", kinds)
186
+ approval = next(
187
+ item for item in page_components["kinds"] if item.get("kind") == "approval"
188
+ )
189
+ self.assertEqual(approval["maxPerPage"], 1)
190
+ self.assertEqual(set(approval["requiredKeys"]), {"kind", "label", "hideLabel"})
191
+
192
+ stdout = io.StringIO()
193
+ with patch("sys.stdout", stdout):
194
+ code = main(["docs", "catalogs", "id-prefixes"])
195
+ self.assertEqual(code, 0)
196
+ prefixes = json.loads(stdout.getvalue())
197
+ self.assertEqual(prefixes["reg_"], "page region")
198
+ self.assertEqual(prefixes["cmp_"], "page component")
199
+
139
200
  def test_docs_list_marks_body_and_confirmation(self):
140
201
  stdout = io.StringIO()
141
202
  with patch("sys.stdout", stdout):
@@ -204,6 +265,92 @@ class FlowBaseCliTest(unittest.TestCase):
204
265
  )
205
266
  self.assertEqual(request.call_args.kwargs["body"], body)
206
267
 
268
+ def test_approval_create_sends_graph_snapshot(self):
269
+ body = {
270
+ "baseRevision": 3,
271
+ "label": "Contract approval",
272
+ "trigger": {
273
+ "manual": True,
274
+ "create": False,
275
+ "update": False,
276
+ "match": "all",
277
+ "conditions": [],
278
+ },
279
+ "displayFieldIds": ["fld_1"],
280
+ "lockRecord": True,
281
+ "allowWithdraw": True,
282
+ "nodes": [{"id": "apn_trigger", "type": "trigger"}],
283
+ "edges": [],
284
+ }
285
+ stdout = io.StringIO()
286
+ with (
287
+ patch(
288
+ "flowbase_cli.main.OpenApiClient.request",
289
+ return_value=b'{"approvalId":"apr_1","draftRevision":4}',
290
+ ) as request,
291
+ patch("sys.stdout", stdout),
292
+ ):
293
+ code = main([
294
+ "--api-key",
295
+ "fbk_test",
296
+ "approvals",
297
+ "create",
298
+ "obj_1",
299
+ "--json",
300
+ json.dumps(body),
301
+ ])
302
+ self.assertEqual(code, 0)
303
+ self.assertEqual(request.call_args.args[0], "approvals.create")
304
+ self.assertEqual(
305
+ request.call_args.kwargs["path_params"],
306
+ {"object_id": "obj_1"},
307
+ )
308
+ self.assertEqual(request.call_args.kwargs["body"], body)
309
+
310
+ def test_approval_set_enabled_sends_revision_and_requires_yes(self):
311
+ stderr = io.StringIO()
312
+ with patch("sys.stderr", stderr), patch("sys.stdin.isatty", return_value=False):
313
+ code = main([
314
+ "--api-key",
315
+ "fbk_test",
316
+ "approvals",
317
+ "set-enabled",
318
+ "obj_1",
319
+ "apr_1",
320
+ "--base-revision",
321
+ "4",
322
+ "--no-enabled",
323
+ ])
324
+ self.assertEqual(code, 1)
325
+ self.assertIn("without --yes", stderr.getvalue())
326
+
327
+ with patch(
328
+ "flowbase_cli.main.OpenApiClient.request",
329
+ return_value=b'{"ok":true,"draftRevision":5}',
330
+ ) as request:
331
+ code = main([
332
+ "--api-key",
333
+ "fbk_test",
334
+ "approvals",
335
+ "set-enabled",
336
+ "obj_1",
337
+ "apr_1",
338
+ "--base-revision",
339
+ "4",
340
+ "--no-enabled",
341
+ "--yes",
342
+ ])
343
+ self.assertEqual(code, 0)
344
+ self.assertEqual(request.call_args.args[0], "approvals.set-enabled")
345
+ self.assertEqual(
346
+ request.call_args.kwargs["path_params"],
347
+ {"object_id": "obj_1", "approval_id": "apr_1"},
348
+ )
349
+ self.assertEqual(
350
+ request.call_args.kwargs["body"],
351
+ {"baseRevision": 4, "enabled": False},
352
+ )
353
+
207
354
  def test_view_set_enabled_sends_revision_and_requires_yes(self):
208
355
  stderr = io.StringIO()
209
356
  with patch("sys.stderr", stderr), patch("sys.stdin.isatty", return_value=False):
@@ -338,6 +485,83 @@ class FlowBaseCliTest(unittest.TestCase):
338
485
  },
339
486
  )
340
487
 
488
+ def test_object_cli_edit_policy_get_and_update(self):
489
+ with patch(
490
+ "flowbase_cli.main.OpenApiClient.request",
491
+ return_value=b'{"objectId":"obj_1","draftRevision":4,"policies":{}}',
492
+ ) as request:
493
+ code = main([
494
+ "--api-key",
495
+ "fbk_test",
496
+ "objects",
497
+ "cli-edit-policy",
498
+ "get",
499
+ "obj_1",
500
+ ])
501
+ self.assertEqual(code, 0)
502
+ self.assertEqual(request.call_args.args[0], "objects.cli-edit-policy.get")
503
+ self.assertEqual(
504
+ request.call_args.kwargs["path_params"], {"object_id": "obj_1"}
505
+ )
506
+
507
+ body = {
508
+ "baseRevision": 4,
509
+ "policies": {"fld_1": "user_approval", "fld_2": "direct"},
510
+ }
511
+ with patch(
512
+ "flowbase_cli.main.OpenApiClient.request",
513
+ return_value=b'{"ok":true,"draftRevision":5}',
514
+ ) as request:
515
+ code = main([
516
+ "--api-key",
517
+ "fbk_test",
518
+ "objects",
519
+ "cli-edit-policy",
520
+ "update",
521
+ "obj_1",
522
+ "--json",
523
+ json.dumps(body),
524
+ "--yes",
525
+ ])
526
+ self.assertEqual(code, 0)
527
+ self.assertEqual(request.call_args.args[0], "objects.cli-edit-policy.update")
528
+ self.assertEqual(request.call_args.kwargs["body"], body)
529
+
530
+ def test_object_cli_edit_policy_update_requires_complete_body_and_yes(self):
531
+ stderr = io.StringIO()
532
+ with patch("sys.stderr", stderr):
533
+ code = main([
534
+ "objects",
535
+ "cli-edit-policy",
536
+ "update",
537
+ "obj_1",
538
+ "--json",
539
+ '{"baseRevision":4}',
540
+ "--yes",
541
+ ])
542
+ self.assertEqual(code, 1)
543
+ self.assertIn("policies", stderr.getvalue())
544
+
545
+ stderr = io.StringIO()
546
+ with (
547
+ patch("sys.stderr", stderr),
548
+ patch("sys.stdin.isatty", return_value=False),
549
+ patch("flowbase_cli.main.OpenApiClient.request") as request,
550
+ ):
551
+ code = main([
552
+ "--api-key",
553
+ "fbk_test",
554
+ "objects",
555
+ "cli-edit-policy",
556
+ "update",
557
+ "obj_1",
558
+ "--json",
559
+ '{"baseRevision":4,"policies":{}}',
560
+ ])
561
+ self.assertEqual(code, 1)
562
+ self.assertIn("without --yes", stderr.getvalue())
563
+ request.assert_not_called()
564
+
341
565
  def test_group_create_and_app_set_group(self):
342
566
  with patch(
343
567
  "flowbase_cli.main.OpenApiClient.request",
File without changes