flowbase-cli 0.1.8__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.8
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,7 +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
117
- flowbase docs catalogs page-components # page region component kinds (field / relatedList)
119
+ flowbase docs catalogs page-components # page region component kinds (field / relatedList / approval)
118
120
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
119
121
  flowbase docs catalogs icons # icon guidance + examples
120
122
  ```
@@ -139,6 +141,8 @@ Do not compose update bodies from scratch when an existing resource exists — `
139
141
  flowbase objects create --label Customer --key customer --description "Customer records"
140
142
  flowbase fields create obj_xxx --json '{"label":"Name","key":"name","description":"Customer name","type":"text.singleLine","permission":"required"}'
141
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
142
146
  ```
143
147
 
144
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`.
@@ -168,22 +172,29 @@ flowbase changes direct-release --message "Fix customer field" --yes
168
172
 
169
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.
170
174
 
171
- ### 6.4 Workflows and pages
175
+ ### 6.4 Workflows, approvals, and pages
172
176
 
173
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`.
174
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
+
175
181
  ```bash
176
182
  flowbase docs catalogs workflow-nodes
177
183
  flowbase workflows create obj_xxx --json '{"label":"Set owner","enabled":true,"trigger":{"event":"create","match":"all","conditions":[]},"nodes":[],"edges":[]}'
178
184
  flowbase workflows update obj_xxx wfl_xxx --json-file workflow.json
179
185
  flowbase workflows executions obj_xxx wfl_xxx --result failure
180
186
 
181
- flowbase docs catalogs page-components # field + relatedList keys
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
182
193
  flowbase docs example pages.create --output page.json # create-page layout template
183
194
  flowbase pages create --json-file page.json
184
195
  flowbase pages set-default pag_xxx --base-revision 4 --yes
185
196
 
186
- # Detail pages may embed relatedList components (reverse lookup lists).
197
+ # Detail pages may embed relatedList and at most one approval component.
187
198
  flowbase pages get pag_xxx > page.json # prefer get → edit → update
188
199
  # or: flowbase docs example pages.update --output page.json
189
200
  flowbase pages update pag_xxx --json-file page.json
@@ -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,7 +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
108
- flowbase docs catalogs page-components # page region component kinds (field / relatedList)
110
+ flowbase docs catalogs page-components # page region component kinds (field / relatedList / approval)
109
111
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
110
112
  flowbase docs catalogs icons # icon guidance + examples
111
113
  ```
@@ -130,6 +132,8 @@ Do not compose update bodies from scratch when an existing resource exists — `
130
132
  flowbase objects create --label Customer --key customer --description "Customer records"
131
133
  flowbase fields create obj_xxx --json '{"label":"Name","key":"name","description":"Customer name","type":"text.singleLine","permission":"required"}'
132
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
133
137
  ```
134
138
 
135
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`.
@@ -159,22 +163,29 @@ flowbase changes direct-release --message "Fix customer field" --yes
159
163
 
160
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.
161
165
 
162
- ### 6.4 Workflows and pages
166
+ ### 6.4 Workflows, approvals, and pages
163
167
 
164
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`.
165
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
+
166
172
  ```bash
167
173
  flowbase docs catalogs workflow-nodes
168
174
  flowbase workflows create obj_xxx --json '{"label":"Set owner","enabled":true,"trigger":{"event":"create","match":"all","conditions":[]},"nodes":[],"edges":[]}'
169
175
  flowbase workflows update obj_xxx wfl_xxx --json-file workflow.json
170
176
  flowbase workflows executions obj_xxx wfl_xxx --result failure
171
177
 
172
- flowbase docs catalogs page-components # field + relatedList keys
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
173
184
  flowbase docs example pages.create --output page.json # create-page layout template
174
185
  flowbase pages create --json-file page.json
175
186
  flowbase pages set-default pag_xxx --base-revision 4 --yes
176
187
 
177
- # Detail pages may embed relatedList components (reverse lookup lists).
188
+ # Detail pages may embed relatedList and at most one approval component.
178
189
  flowbase pages get pag_xxx > page.json # prefer get → edit → update
179
190
  # or: flowbase docs example pages.update --output page.json
180
191
  flowbase pages update pag_xxx --json-file page.json
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flowbase-cli"
7
- version = "0.1.8"
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.8"
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"),
@@ -242,7 +307,8 @@ ENDPOINTS = (
242
307
  '"columnFieldIds":["fld_title","fld_status","fld_created_at"],'
243
308
  '"sort":{"fieldId":"fld_created_at","direction":"desc"},"limit":5,"showViewAll":true,'
244
309
  '"createAction":{"enabled":true,"label":"New ticket"},'
245
- '"carryIns":[{"fieldId":"fld_customer","source":"currentRecord"}]}]]}],"rules":[]}}'
310
+ '"carryIns":[{"fieldId":"fld_customer","source":"currentRecord"}]},'
311
+ '{"kind":"approval","label":"Approval","hideLabel":false}]]}],"rules":[]}}'
246
312
  )),
247
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,
248
314
  request_body='{"baseRevision":4}'),
@@ -391,7 +457,7 @@ ENDPOINTS = (
391
457
  ENDPOINT_BY_KEY = {endpoint.key: endpoint for endpoint in ENDPOINTS}
392
458
 
393
459
  # Closed-domain catalogs for offline authoring of complex request bodies.
394
- # Keep in sync with server registries (field types, workflow node kinds,
460
+ # Keep in sync with server registries (field types, workflow/approval node kinds,
395
461
  # permission points, dashboard filter presets, page component kinds).
396
462
  # Icons intentionally reference the server file because the full Tabler set is large.
397
463
  FIELD_TYPES = (
@@ -413,6 +479,13 @@ FIELD_TYPES = (
413
479
  {"type": "url", "constraints": ["maxLength", "placeholder"], "writable": True},
414
480
  {"type": "attachment", "constraints": ["maxFileCount", "maxFileSize", "maxTotalSize", "allowedExtensions"], "writable": True},
415
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},
416
489
  )
417
490
 
418
491
  WORKFLOW_NODE_TYPES = (
@@ -426,6 +499,22 @@ WORKFLOW_NODE_TYPES = (
426
499
  "note": "terminal node"},
427
500
  )
428
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
+
429
518
  FUNCTION_PERMISSIONS = (
430
519
  "workspace.access", "app.manage", "object.manage", "dashboard.manage",
431
520
  "version.manage", "org.manage", "system.manage",
@@ -439,13 +528,14 @@ DASHBOARD_DATE_RANGE_PRESETS = (
439
528
  "this_week", "this_month", "this_quarter", "this_year",
440
529
  )
441
530
 
442
- # Page region column components (1.8 relatedList + field components).
443
- # Field components omit `kind` (legacy snapshot shape); related lists set kind.
531
+ # Page region column components (field + relatedList + approval).
532
+ # Field components omit `kind` (legacy snapshot shape); relatedList/approval set kind.
444
533
  PAGE_COMPONENTS = {
445
534
  "note": (
446
- "Region columns hold field components and/or relatedList components. "
447
- "relatedList is allowed only on detail pages. Prefer pages.get → edit → "
448
- "pages.update; use pages.options to resolve candidate objects/fields."
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."
449
539
  ),
450
540
  "kinds": (
451
541
  {
@@ -489,12 +579,24 @@ PAGE_COMPONENTS = {
489
579
  "implicit relationFieldId eq currentRecord condition.",
490
580
  },
491
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
+ },
492
594
  ),
493
595
  }
494
596
 
495
597
  RESOURCE_ID_PREFIXES = {
496
598
  "app_": "app", "grp_": "app group", "obj_": "object", "fld_": "field",
497
- "viw_": "view", "wfl_": "workflow", "pag_": "custom page",
599
+ "viw_": "view", "wfl_": "workflow", "apr_": "approval flow", "pag_": "custom page",
498
600
  "reg_": "page region", "cmp_": "page component",
499
601
  "dsh_": "dashboard", "dcm_": "dashboard component", "rol_": "role",
500
602
  "rec_": "record", "att_": "attachment", "emp_": "employee", "dep_": "department",
@@ -503,6 +605,7 @@ RESOURCE_ID_PREFIXES = {
503
605
  CATALOGS = {
504
606
  "field-types": FIELD_TYPES,
505
607
  "workflow-nodes": WORKFLOW_NODE_TYPES,
608
+ "approval-nodes": APPROVAL_NODE_TYPES,
506
609
  "permissions": {"function": FUNCTION_PERMISSIONS, "dataScopes": DATA_SCOPES},
507
610
  "dashboard-filters": {
508
611
  "types": DASHBOARD_FILTER_TYPES,
@@ -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, page-components, 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.8
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,7 +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
117
- flowbase docs catalogs page-components # page region component kinds (field / relatedList)
119
+ flowbase docs catalogs page-components # page region component kinds (field / relatedList / approval)
118
120
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
119
121
  flowbase docs catalogs icons # icon guidance + examples
120
122
  ```
@@ -139,6 +141,8 @@ Do not compose update bodies from scratch when an existing resource exists — `
139
141
  flowbase objects create --label Customer --key customer --description "Customer records"
140
142
  flowbase fields create obj_xxx --json '{"label":"Name","key":"name","description":"Customer name","type":"text.singleLine","permission":"required"}'
141
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
142
146
  ```
143
147
 
144
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`.
@@ -168,22 +172,29 @@ flowbase changes direct-release --message "Fix customer field" --yes
168
172
 
169
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.
170
174
 
171
- ### 6.4 Workflows and pages
175
+ ### 6.4 Workflows, approvals, and pages
172
176
 
173
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`.
174
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
+
175
181
  ```bash
176
182
  flowbase docs catalogs workflow-nodes
177
183
  flowbase workflows create obj_xxx --json '{"label":"Set owner","enabled":true,"trigger":{"event":"create","match":"all","conditions":[]},"nodes":[],"edges":[]}'
178
184
  flowbase workflows update obj_xxx wfl_xxx --json-file workflow.json
179
185
  flowbase workflows executions obj_xxx wfl_xxx --result failure
180
186
 
181
- flowbase docs catalogs page-components # field + relatedList keys
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
182
193
  flowbase docs example pages.create --output page.json # create-page layout template
183
194
  flowbase pages create --json-file page.json
184
195
  flowbase pages set-default pag_xxx --base-revision 4 --yes
185
196
 
186
- # Detail pages may embed relatedList components (reverse lookup lists).
197
+ # Detail pages may embed relatedList and at most one approval component.
187
198
  flowbase pages get pag_xxx > page.json # prefer get → edit → update
188
199
  # or: flowbase docs example pages.update --output page.json
189
200
  flowbase pages update pag_xxx --json-file page.json
@@ -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
 
@@ -117,10 +119,14 @@ class FlowBaseCliTest(unittest.TestCase):
117
119
  payload = json.loads(stdout.getvalue())
118
120
  body = json.loads(payload["requestBody"])
119
121
  self.assertEqual(body["page"]["type"], "detail")
120
- component = body["page"]["regions"][0]["columns"][0][0]
121
- self.assertEqual(component["kind"], "relatedList")
122
- self.assertIn("relationFieldId", component)
123
- self.assertIn("carryIns", component)
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
+ )
124
130
 
125
131
  def test_docs_catalogs_lists_and_dumps_domains(self):
126
132
  stdout = io.StringIO()
@@ -130,6 +136,7 @@ class FlowBaseCliTest(unittest.TestCase):
130
136
  names = json.loads(stdout.getvalue())
131
137
  self.assertIn("field-types", names)
132
138
  self.assertIn("workflow-nodes", names)
139
+ self.assertIn("approval-nodes", names)
133
140
  self.assertIn("permissions", names)
134
141
  self.assertIn("page-components", names)
135
142
 
@@ -139,8 +146,15 @@ class FlowBaseCliTest(unittest.TestCase):
139
146
  self.assertEqual(code, 0)
140
147
  field_types = json.loads(stdout.getvalue())
141
148
  type_names = {item["type"] for item in field_types}
149
+ self.assertEqual(len(field_types), 25)
142
150
  self.assertIn("select.single", type_names)
143
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"])
144
158
 
145
159
  stdout = io.StringIO()
146
160
  with patch("sys.stdout", stdout):
@@ -149,6 +163,17 @@ class FlowBaseCliTest(unittest.TestCase):
149
163
  nodes = json.loads(stdout.getvalue())
150
164
  self.assertIn("trigger", {item["type"] for item in nodes})
151
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
+
152
177
  stdout = io.StringIO()
153
178
  with patch("sys.stdout", stdout):
154
179
  code = main(["docs", "catalogs", "page-components"])
@@ -157,6 +182,12 @@ class FlowBaseCliTest(unittest.TestCase):
157
182
  kinds = {item.get("kind") for item in page_components["kinds"]}
158
183
  self.assertIn(None, kinds)
159
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"})
160
191
 
161
192
  stdout = io.StringIO()
162
193
  with patch("sys.stdout", stdout):
@@ -234,6 +265,92 @@ class FlowBaseCliTest(unittest.TestCase):
234
265
  )
235
266
  self.assertEqual(request.call_args.kwargs["body"], body)
236
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
+
237
354
  def test_view_set_enabled_sends_revision_and_requires_yes(self):
238
355
  stderr = io.StringIO()
239
356
  with patch("sys.stderr", stderr), patch("sys.stdin.isatty", return_value=False):
@@ -368,6 +485,83 @@ class FlowBaseCliTest(unittest.TestCase):
368
485
  },
369
486
  )
370
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
+
371
565
  def test_group_create_and_app_set_group(self):
372
566
  with patch(
373
567
  "flowbase_cli.main.OpenApiClient.request",
File without changes