@qingflow-tech/qingflow-cli 1.1.50-beta.5 → 1.1.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/npm/lib/runtime.mjs +13 -29
- package/package.json +1 -1
- package/pyproject.toml +2 -8
- package/skills/qingflow-cli/SKILL.md +322 -93
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +9 -3
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +137 -3
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +12 -7
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -3
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -9
- package/skills/qingflow-cli/reference/builder/50-views.md +191 -192
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +0 -2
- package/skills/qingflow-cli/reference/builder/README.md +1 -1
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +6 -6
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +95 -76
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +4 -6
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +19 -20
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +14 -18
- package/skills/qingflow-cli/reference/examples/README.md +1 -1
- package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +159 -0
- package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +36 -30
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +1 -2
- package/skills/qingflow-cli/reference/record/analysis/README.md +113 -21
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +80 -52
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
- package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +42 -0
- package/skills/qingflow-common/SKILL.md +4 -7
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/app_form_compat.py +0 -3
- package/src/qingflow_mcp/app_form_validator.py +17 -69
- package/src/qingflow_mcp/builder_facade/app_form.py +100 -497
- package/src/qingflow_mcp/builder_facade/models.py +17 -227
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +0 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +30 -64
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -46
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +27 -61
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +12 -7
- package/src/qingflow_mcp/builder_facade/service.py +539 -1671
- package/src/qingflow_mcp/builder_facade/workflow_spec.py +1 -1
- package/src/qingflow_mcp/cli/commands/__init__.py +1 -2
- package/src/qingflow_mcp/cli/commands/builder.py +55 -76
- package/src/qingflow_mcp/cli/commands/record.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +1 -97
- package/src/qingflow_mcp/config.py +1 -0
- package/src/qingflow_mcp/public_surface.py +0 -8
- package/src/qingflow_mcp/response_trim.py +2 -11
- package/src/qingflow_mcp/server.py +0 -2
- package/src/qingflow_mcp/server_app_builder.py +10 -34
- package/src/qingflow_mcp/server_app_user.py +0 -4
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +1 -2
- package/src/qingflow_mcp/solution/executor.py +8 -96
- package/src/qingflow_mcp/solution/spec_models.py +0 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +398 -218
- package/src/qingflow_mcp/tools/base.py +42 -10
- package/src/qingflow_mcp/tools/record_tools.py +46 -11
- package/src/qingflow_mcp/tools/solution_tools.py +0 -2
- package/src/qingflow_mcp/tools/view_tools.py +6 -0
- package/skills/qingflow-cli/reference/data/README.md +0 -32
- package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +0 -14
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +0 -50
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
- package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +0 -42
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +0 -1245
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +0 -53
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +0 -60
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +0 -73
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +0 -48
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +0 -47
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +0 -319
- package/src/qingflow_mcp/builder_facade/views_extract.py +0 -592
- package/src/qingflow_mcp/builder_facade/views_schema.py +0 -400
- package/src/qingflow_mcp/cli/commands/data.py +0 -134
- package/src/qingflow_mcp/tools/csv_export.py +0 -168
- package/src/qingflow_mcp/tools/data_tools.py +0 -284
- package/src/qingflow_mcp/tools/view_writer.py +0 -44
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
from copy import deepcopy
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from jsonschema import Draft202012Validator, FormatChecker
|
|
11
|
-
from referencing import Registry, Resource
|
|
12
|
-
|
|
13
|
-
from .app_form_schema import compact_schema_errors, schema_error_message
|
|
14
|
-
|
|
15
|
-
JSONObject = dict[str, Any]
|
|
16
|
-
VIEWS_SCHEMA_ROOT = Path(__file__).with_name("schemas") / "views"
|
|
17
|
-
VIEWS_SUPPORTED_SCHEMA_KINDS = (
|
|
18
|
-
"summary",
|
|
19
|
-
"complete",
|
|
20
|
-
"display",
|
|
21
|
-
"data-permission",
|
|
22
|
-
"operation-permission",
|
|
23
|
-
"query-condition",
|
|
24
|
-
"button-config",
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class ViewsContractError(ValueError):
|
|
29
|
-
"""Raised when a views declaration violates the versioned schema contract."""
|
|
30
|
-
|
|
31
|
-
def __init__(self, error_code: str, message: str, *, path: str | None = None, details: JSONObject | None = None) -> None:
|
|
32
|
-
super().__init__(message)
|
|
33
|
-
self.error_code = error_code
|
|
34
|
-
self.message = message
|
|
35
|
-
self.path = path
|
|
36
|
-
self.details = details or {}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def compact_none_values(value: Any) -> Any:
|
|
40
|
-
"""Recursively drop None values so pydantic dumps match the JSON Schema contract."""
|
|
41
|
-
if isinstance(value, dict):
|
|
42
|
-
return {key: compact_none_values(item) for key, item in value.items() if item is not None}
|
|
43
|
-
if isinstance(value, list):
|
|
44
|
-
return [compact_none_values(item) for item in value]
|
|
45
|
-
return value
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@dataclass(frozen=True, slots=True)
|
|
49
|
-
class ViewsSchemaContext:
|
|
50
|
-
"""Resolved schema context for one views schema version."""
|
|
51
|
-
|
|
52
|
-
schema_version: str
|
|
53
|
-
api_version: str
|
|
54
|
-
schemas: dict[str, JSONObject]
|
|
55
|
-
schema_files: dict[str, str]
|
|
56
|
-
ref_registry: Registry
|
|
57
|
-
supported_schema_kinds: tuple[str, ...]
|
|
58
|
-
contract_source: str
|
|
59
|
-
contract_version: str
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def summary_schema(self) -> JSONObject:
|
|
63
|
-
return self.schemas["summary"]
|
|
64
|
-
|
|
65
|
-
@property
|
|
66
|
-
def complete_schema(self) -> JSONObject:
|
|
67
|
-
return self.schemas["complete"]
|
|
68
|
-
|
|
69
|
-
def schema_for_kind(self, schema_kind: str) -> JSONObject:
|
|
70
|
-
requested = str(schema_kind or "").strip()
|
|
71
|
-
schema = self.schemas.get(requested)
|
|
72
|
-
if schema is not None:
|
|
73
|
-
return schema
|
|
74
|
-
raise ViewsContractError(
|
|
75
|
-
"UNSUPPORTED_SCHEMA_KIND",
|
|
76
|
-
f"unsupported views schema kind: {requested}",
|
|
77
|
-
details={"supportedKinds": list(self.supported_schema_kinds)},
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
def _validate(self, payload: JSONObject, *, schema_kind: str) -> None:
|
|
81
|
-
schema = self.schema_for_kind(schema_kind)
|
|
82
|
-
validator = Draft202012Validator(schema, format_checker=FormatChecker(), registry=self.ref_registry)
|
|
83
|
-
errors = sorted(validator.iter_errors(payload), key=lambda item: (list(item.absolute_path), item.message))
|
|
84
|
-
if not errors:
|
|
85
|
-
return
|
|
86
|
-
compact_errors = compact_schema_errors(errors, schema)
|
|
87
|
-
error = compact_errors[0] if compact_errors else errors[0]
|
|
88
|
-
pointer = "".join(f"/{str(part).replace('~', '~0').replace('/', '~1')}" for part in error.absolute_path) or "/"
|
|
89
|
-
raise ViewsContractError(
|
|
90
|
-
"INVALID_SPEC",
|
|
91
|
-
schema_error_message(error),
|
|
92
|
-
path=pointer,
|
|
93
|
-
details={
|
|
94
|
-
"validator": error.validator,
|
|
95
|
-
"schemaPath": "/".join(str(part) for part in error.absolute_schema_path),
|
|
96
|
-
"schemaKind": schema_kind,
|
|
97
|
-
},
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
def validate_upsert(self, payload: JSONObject, *, schema_kind: str = "complete") -> None:
|
|
101
|
-
"""Validate a single (alias-normalized) view declaration."""
|
|
102
|
-
self._validate(payload, schema_kind=schema_kind)
|
|
103
|
-
|
|
104
|
-
def validate_declaration(self, payload: JSONObject, *, schema_kind: str = "complete") -> None:
|
|
105
|
-
"""Validate one view declaration used by ``app_views_apply.upsert_view``."""
|
|
106
|
-
if not isinstance(payload, dict):
|
|
107
|
-
raise ViewsContractError("INVALID_SPEC", "views declaration must be a JSON object", path="/")
|
|
108
|
-
batch_keys = ("views", "upsert_views", "upsertViews", "apps")
|
|
109
|
-
if any(key in payload for key in batch_keys):
|
|
110
|
-
raise ViewsContractError(
|
|
111
|
-
"VIEW_BATCH_CREATE_UNSUPPORTED",
|
|
112
|
-
"views schema validates one view declaration; batch envelopes are unsupported",
|
|
113
|
-
path="/",
|
|
114
|
-
details={"unsupportedKeys": [key for key in batch_keys if key in payload], "schemaKind": schema_kind},
|
|
115
|
-
)
|
|
116
|
-
self._validate(payload, schema_kind=schema_kind)
|
|
117
|
-
|
|
118
|
-
class ViewsSchemaRegistry:
|
|
119
|
-
"""Registry for versioned views declaration schemas (mirrors AppFormSchemaRegistry)."""
|
|
120
|
-
|
|
121
|
-
def __init__(self, root: Path = VIEWS_SCHEMA_ROOT, *, enabled_versions: tuple[str, ...] | None = None) -> None:
|
|
122
|
-
self.root = root
|
|
123
|
-
self.manifest = self._read_json(root / "manifest.json")
|
|
124
|
-
versions = self.manifest.get("versions")
|
|
125
|
-
if not isinstance(versions, dict) or not versions:
|
|
126
|
-
raise RuntimeError("views schema manifest must define versions")
|
|
127
|
-
env_versions = self._configured_versions()
|
|
128
|
-
defaults = tuple(
|
|
129
|
-
str(version)
|
|
130
|
-
for version, metadata in versions.items()
|
|
131
|
-
if isinstance(metadata, dict) and bool(metadata.get("enabledByDefault"))
|
|
132
|
-
)
|
|
133
|
-
configured = tuple(enabled_versions) if enabled_versions is not None else (env_versions or defaults)
|
|
134
|
-
self.stored_versions = tuple(str(version) for version in versions)
|
|
135
|
-
self.latest_version = str(self.manifest.get("latestVersion") or "")
|
|
136
|
-
self._stored_api_to_version: dict[str, str] = {}
|
|
137
|
-
for version, metadata in versions.items():
|
|
138
|
-
if not isinstance(metadata, dict):
|
|
139
|
-
raise RuntimeError(f"views schema metadata must be an object: {version}")
|
|
140
|
-
api_version = str(metadata.get("apiVersion") or "")
|
|
141
|
-
if not api_version:
|
|
142
|
-
raise RuntimeError(f"views schema version has no apiVersion: {version}")
|
|
143
|
-
if api_version in self._stored_api_to_version:
|
|
144
|
-
raise RuntimeError(f"duplicate views apiVersion: {api_version}")
|
|
145
|
-
self._stored_api_to_version[api_version] = str(version)
|
|
146
|
-
self.enabled_versions = configured
|
|
147
|
-
if self.latest_version not in versions:
|
|
148
|
-
raise RuntimeError("views latestVersion is not stored")
|
|
149
|
-
if self.latest_version not in self.enabled_versions:
|
|
150
|
-
raise RuntimeError("views latestVersion must remain enabled")
|
|
151
|
-
self._contexts: dict[str, ViewsSchemaContext] = {}
|
|
152
|
-
self._api_to_version: dict[str, str] = {}
|
|
153
|
-
for version in self.enabled_versions:
|
|
154
|
-
metadata = versions.get(version)
|
|
155
|
-
if not isinstance(metadata, dict):
|
|
156
|
-
raise RuntimeError(f"enabled views schema version is not stored: {version}")
|
|
157
|
-
api_version = str(metadata.get("apiVersion") or "")
|
|
158
|
-
schema_files = metadata.get("schemaFiles")
|
|
159
|
-
if not isinstance(schema_files, dict):
|
|
160
|
-
schema_files = {kind: f"{version}/{kind}.json" for kind in ("summary", "complete")}
|
|
161
|
-
supported_schema_kinds = tuple(str(item) for item in metadata.get("schemaKinds") or schema_files)
|
|
162
|
-
if not {"summary", "complete"}.issubset(supported_schema_kinds):
|
|
163
|
-
raise RuntimeError(f"views schema version must support summary and complete kinds: {version}")
|
|
164
|
-
schemas: dict[str, JSONObject] = {}
|
|
165
|
-
paths: dict[str, Path] = {}
|
|
166
|
-
for name in supported_schema_kinds:
|
|
167
|
-
relative_path = schema_files.get(name)
|
|
168
|
-
if not isinstance(relative_path, str) or not relative_path:
|
|
169
|
-
raise RuntimeError(f"views schema kind has no schema file: {version}/{name}")
|
|
170
|
-
path = root / relative_path
|
|
171
|
-
schema = self._read_json(path)
|
|
172
|
-
schemas[name] = schema
|
|
173
|
-
paths[name] = path
|
|
174
|
-
ref_registry = Registry()
|
|
175
|
-
for name, schema in schemas.items():
|
|
176
|
-
schema_id = str(schema.get("$id") or "")
|
|
177
|
-
if version not in schema_id:
|
|
178
|
-
raise RuntimeError(f"views {name} schema $id mismatch for {version}: {schema_id}")
|
|
179
|
-
Draft202012Validator.check_schema(schema)
|
|
180
|
-
resource = Resource.from_contents(schema)
|
|
181
|
-
if schema_id:
|
|
182
|
-
ref_registry = ref_registry.with_resource(schema_id, resource)
|
|
183
|
-
ref_registry = ref_registry.with_resource(paths[name].resolve().as_uri(), resource)
|
|
184
|
-
for name, schema in schemas.items():
|
|
185
|
-
self._validate_local_refs(schema, paths[name], schemas, paths)
|
|
186
|
-
context = ViewsSchemaContext(
|
|
187
|
-
version,
|
|
188
|
-
api_version,
|
|
189
|
-
schemas,
|
|
190
|
-
{name: str(schema_files[name]) for name in supported_schema_kinds},
|
|
191
|
-
ref_registry,
|
|
192
|
-
supported_schema_kinds,
|
|
193
|
-
str(metadata.get("contractSource") or "app_views_apply"),
|
|
194
|
-
str(metadata.get("contractVersion") or version),
|
|
195
|
-
)
|
|
196
|
-
self._contexts[version] = context
|
|
197
|
-
self._api_to_version[api_version] = version
|
|
198
|
-
|
|
199
|
-
def schema(
|
|
200
|
-
self,
|
|
201
|
-
*,
|
|
202
|
-
schema_version: str | None = None,
|
|
203
|
-
schema_kind: str | None = None,
|
|
204
|
-
) -> JSONObject:
|
|
205
|
-
context = self.resolve_schema_version(schema_version)
|
|
206
|
-
requested_kind = str(schema_kind or "summary").strip()
|
|
207
|
-
if requested_kind not in context.supported_schema_kinds:
|
|
208
|
-
raise ViewsContractError(
|
|
209
|
-
"UNSUPPORTED_SCHEMA_KIND",
|
|
210
|
-
f"unsupported views schema kind: {requested_kind}",
|
|
211
|
-
details={"supportedKinds": list(context.supported_schema_kinds)},
|
|
212
|
-
)
|
|
213
|
-
schema, dependencies = self._bundle_schema(
|
|
214
|
-
context.schema_for_kind(requested_kind),
|
|
215
|
-
schema_path=(self.root / context.schema_files[requested_kind]),
|
|
216
|
-
schemas=context.schemas,
|
|
217
|
-
schema_files=context.schema_files,
|
|
218
|
-
)
|
|
219
|
-
return {
|
|
220
|
-
"schemaVersion": context.schema_version,
|
|
221
|
-
"apiVersion": context.api_version,
|
|
222
|
-
"latestVersion": self.latest_version,
|
|
223
|
-
"storedVersions": list(self.stored_versions),
|
|
224
|
-
"enabledVersions": list(self.enabled_versions),
|
|
225
|
-
"supportedSchemaKinds": list(context.supported_schema_kinds),
|
|
226
|
-
"schemaKind": requested_kind,
|
|
227
|
-
"schemaFile": context.schema_files[requested_kind],
|
|
228
|
-
"schemaBundled": bool(dependencies),
|
|
229
|
-
"schemaDependencies": dependencies,
|
|
230
|
-
"schema": schema,
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
@classmethod
|
|
234
|
-
def _bundle_schema(
|
|
235
|
-
cls,
|
|
236
|
-
schema: JSONObject,
|
|
237
|
-
*,
|
|
238
|
-
schema_path: Path,
|
|
239
|
-
schemas: dict[str, JSONObject],
|
|
240
|
-
schema_files: dict[str, str],
|
|
241
|
-
) -> tuple[JSONObject, list[str]]:
|
|
242
|
-
"""Inline reachable cross-file definitions for standalone consumers."""
|
|
243
|
-
bundled = deepcopy(schema)
|
|
244
|
-
definitions = bundled.get("$defs")
|
|
245
|
-
if not isinstance(definitions, dict):
|
|
246
|
-
definitions = {}
|
|
247
|
-
dependencies: set[str] = set()
|
|
248
|
-
# schema_path is <root>/<version>/details/<file> or <root>/<version>/<file>.
|
|
249
|
-
# Find the manifest root from the configured path instead of relying on
|
|
250
|
-
# a fixed details depth.
|
|
251
|
-
cls_root = schema_path.parent
|
|
252
|
-
for candidate in schema_path.parents:
|
|
253
|
-
if (candidate / "manifest.json").is_file():
|
|
254
|
-
cls_root = candidate
|
|
255
|
-
break
|
|
256
|
-
paths = {name: cls_root / relative for name, relative in schema_files.items()}
|
|
257
|
-
|
|
258
|
-
def resolve_pointer(document: JSONObject, fragment: str, *, ref: str) -> Any:
|
|
259
|
-
target: Any = document
|
|
260
|
-
for raw_part in fragment[1:].split("/") if fragment.startswith("/") else ():
|
|
261
|
-
part = raw_part.replace("~1", "/").replace("~0", "~")
|
|
262
|
-
if not isinstance(target, dict) or part not in target:
|
|
263
|
-
raise RuntimeError(f"views schema contains a dangling $ref in {schema_path}: {ref}")
|
|
264
|
-
target = target[part]
|
|
265
|
-
return target
|
|
266
|
-
|
|
267
|
-
pending: list[tuple[str, JSONObject, Path]] = []
|
|
268
|
-
|
|
269
|
-
def add_definition(name: str, source: JSONObject, source_path: Path) -> None:
|
|
270
|
-
if name in definitions:
|
|
271
|
-
return
|
|
272
|
-
source_definitions = source.get("$defs")
|
|
273
|
-
if not isinstance(source_definitions, dict) or name not in source_definitions:
|
|
274
|
-
raise RuntimeError(f"views schema references an unknown definition: {name}")
|
|
275
|
-
definitions[name] = deepcopy(source_definitions[name])
|
|
276
|
-
pending.append((name, source, source_path))
|
|
277
|
-
|
|
278
|
-
def rewrite(value: Any, source: JSONObject, source_path: Path) -> None:
|
|
279
|
-
if isinstance(value, dict):
|
|
280
|
-
ref = value.get("$ref")
|
|
281
|
-
if isinstance(ref, str):
|
|
282
|
-
ref_file, _, fragment = ref.partition("#")
|
|
283
|
-
target_source = source
|
|
284
|
-
target_path = source_path
|
|
285
|
-
if ref_file:
|
|
286
|
-
target_path = (source_path.parent / ref_file).resolve()
|
|
287
|
-
target_name = next(
|
|
288
|
-
(name for name, path in paths.items() if path.resolve() == target_path),
|
|
289
|
-
None,
|
|
290
|
-
)
|
|
291
|
-
if target_name is None:
|
|
292
|
-
raise RuntimeError(f"views schema contains an external $ref in {source_path}: {ref}")
|
|
293
|
-
target_source = schemas[target_name]
|
|
294
|
-
dependencies.add(schema_files[target_name])
|
|
295
|
-
value["$ref"] = f"#{fragment}"
|
|
296
|
-
local_ref = str(value["$ref"])
|
|
297
|
-
if local_ref.startswith("#/$defs/"):
|
|
298
|
-
name = local_ref.removeprefix("#/$defs/").split("/", 1)[0]
|
|
299
|
-
add_definition(name, target_source, target_path)
|
|
300
|
-
else:
|
|
301
|
-
resolve_pointer(target_source, fragment, ref=ref)
|
|
302
|
-
for child in value.values():
|
|
303
|
-
rewrite(child, source, source_path)
|
|
304
|
-
elif isinstance(value, list):
|
|
305
|
-
for child in value:
|
|
306
|
-
rewrite(child, source, source_path)
|
|
307
|
-
|
|
308
|
-
for key, value in bundled.items():
|
|
309
|
-
if key != "$defs":
|
|
310
|
-
rewrite(value, schema, schema_path)
|
|
311
|
-
while pending:
|
|
312
|
-
name, source, source_path = pending.pop()
|
|
313
|
-
rewrite(definitions[name], source, source_path)
|
|
314
|
-
if definitions:
|
|
315
|
-
bundled["$defs"] = definitions
|
|
316
|
-
return bundled, sorted(dependencies)
|
|
317
|
-
|
|
318
|
-
def resolve_schema_version(self, schema_version: str | None) -> ViewsSchemaContext:
|
|
319
|
-
version = str(schema_version or self.latest_version).strip()
|
|
320
|
-
if version not in self.stored_versions:
|
|
321
|
-
raise self._version_error("UNSUPPORTED_SCHEMA_VERSION", version)
|
|
322
|
-
if version not in self.enabled_versions:
|
|
323
|
-
raise self._version_error("SCHEMA_VERSION_DISABLED", version)
|
|
324
|
-
return self._contexts[version]
|
|
325
|
-
|
|
326
|
-
def resolve_api_version(self, api_version: str) -> ViewsSchemaContext:
|
|
327
|
-
version = self._api_to_version.get(api_version)
|
|
328
|
-
if version is not None:
|
|
329
|
-
return self._contexts[version]
|
|
330
|
-
stored = self._stored_api_to_version.get(api_version)
|
|
331
|
-
if stored is not None:
|
|
332
|
-
raise self._version_error("SCHEMA_VERSION_DISABLED", stored)
|
|
333
|
-
raise self._version_error("UNSUPPORTED_SCHEMA_VERSION", api_version)
|
|
334
|
-
|
|
335
|
-
def _version_error(self, error_code: str, requested: str) -> ViewsContractError:
|
|
336
|
-
return ViewsContractError(
|
|
337
|
-
error_code,
|
|
338
|
-
f"unsupported views schema selection: {requested}",
|
|
339
|
-
details={
|
|
340
|
-
"requestedVersion": requested,
|
|
341
|
-
"latestVersion": self.latest_version,
|
|
342
|
-
"storedVersions": list(self.stored_versions),
|
|
343
|
-
"enabledVersions": list(self.enabled_versions),
|
|
344
|
-
},
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
@staticmethod
|
|
348
|
-
def _read_json(path: Path) -> JSONObject:
|
|
349
|
-
value = json.loads(path.read_text(encoding="utf-8"))
|
|
350
|
-
if not isinstance(value, dict):
|
|
351
|
-
raise RuntimeError(f"views schema artifact must be an object: {path}")
|
|
352
|
-
return value
|
|
353
|
-
|
|
354
|
-
@staticmethod
|
|
355
|
-
def _validate_local_refs(
|
|
356
|
-
document: JSONObject,
|
|
357
|
-
path: Path,
|
|
358
|
-
documents: dict[str, JSONObject],
|
|
359
|
-
paths: dict[str, Path],
|
|
360
|
-
) -> None:
|
|
361
|
-
stack: list[Any] = [document]
|
|
362
|
-
while stack:
|
|
363
|
-
value = stack.pop()
|
|
364
|
-
if isinstance(value, dict):
|
|
365
|
-
ref = value.get("$ref")
|
|
366
|
-
if ref is not None:
|
|
367
|
-
if not isinstance(ref, str):
|
|
368
|
-
raise RuntimeError(f"views schema contains an invalid $ref in {path}: {ref}")
|
|
369
|
-
ref_file, _, fragment = ref.partition("#")
|
|
370
|
-
target_document = document
|
|
371
|
-
if ref_file:
|
|
372
|
-
target_path = (path.parent / ref_file).resolve()
|
|
373
|
-
if target_path not in {item.resolve() for item in paths.values()}:
|
|
374
|
-
raise RuntimeError(f"views schema contains an external $ref in {path}: {ref}")
|
|
375
|
-
target_name = next(name for name, item in paths.items() if item.resolve() == target_path)
|
|
376
|
-
target_document = documents[target_name]
|
|
377
|
-
target: Any = target_document
|
|
378
|
-
for raw_part in fragment[1:].split("/") if fragment.startswith("/") else ():
|
|
379
|
-
part = raw_part.replace("~1", "/").replace("~0", "~")
|
|
380
|
-
if not isinstance(target, dict) or part not in target:
|
|
381
|
-
raise RuntimeError(f"views schema contains a dangling $ref in {path}: {ref}")
|
|
382
|
-
target = target[part]
|
|
383
|
-
stack.extend(value.values())
|
|
384
|
-
elif isinstance(value, list):
|
|
385
|
-
stack.extend(value)
|
|
386
|
-
|
|
387
|
-
@staticmethod
|
|
388
|
-
def _configured_versions() -> tuple[str, ...]:
|
|
389
|
-
raw = os.environ.get("QINGFLOW_VIEWS_SCHEMA_VERSIONS", "")
|
|
390
|
-
return tuple(part.strip() for part in raw.split(",") if part.strip())
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
_REGISTRY: ViewsSchemaRegistry | None = None
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
def views_schema_registry() -> ViewsSchemaRegistry:
|
|
397
|
-
global _REGISTRY
|
|
398
|
-
if _REGISTRY is None:
|
|
399
|
-
_REGISTRY = ViewsSchemaRegistry()
|
|
400
|
-
return _REGISTRY
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import textwrap
|
|
5
|
-
|
|
6
|
-
from ..context import CliContext
|
|
7
|
-
from .common import load_list_arg, require_list_arg, require_object_arg, raise_config_error
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
11
|
-
parser = subparsers.add_parser(
|
|
12
|
-
"data",
|
|
13
|
-
help="默认应用数据查询与直接增删改",
|
|
14
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
15
|
-
epilog=textwrap.dedent("""
|
|
16
|
-
JSON 文件约定:
|
|
17
|
-
columns.json [101, 102]
|
|
18
|
-
where.json [{"field_id": 101, "op": "eq", "value": "A"}]
|
|
19
|
-
sort.json [{"field_id": 101, "direction": "asc"}]
|
|
20
|
-
items.json [{"fields": {"101": "A"}}]
|
|
21
|
-
fields.json {"101": "B"}
|
|
22
|
-
query/get/insert/update 默认返回 field_capabilities;可用 --no-schema 关闭。
|
|
23
|
-
""").strip(),
|
|
24
|
-
)
|
|
25
|
-
commands = parser.add_subparsers(dest="data_command", required=True, metavar="{query,get,insert,update,delete}")
|
|
26
|
-
|
|
27
|
-
query = commands.add_parser(
|
|
28
|
-
"query", help="分页查询应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
29
|
-
epilog='最小示例:qingflow data query --app-key APP_1 --where-file where.json\n'
|
|
30
|
-
'where.json: [{"field_id": 101, "op": "eq", "value": "A"}]',
|
|
31
|
-
)
|
|
32
|
-
query.add_argument("--app-key", required=True)
|
|
33
|
-
query.add_argument("--columns-file", help='JSON 数组,例如 [101, 102]')
|
|
34
|
-
query.add_argument(
|
|
35
|
-
"--where-file",
|
|
36
|
-
help='JSON 数组;项为 {"field_id":101,"op":"eq","value":"A"},in/not_in 的 value 必须为数组',
|
|
37
|
-
)
|
|
38
|
-
query.add_argument("--sort-file", help='JSON 数组,例如 [{"field_id":101,"direction":"asc"}]')
|
|
39
|
-
query.add_argument("--search", default="")
|
|
40
|
-
query.add_argument("--page", type=int, default=1)
|
|
41
|
-
query.add_argument("--page-size", type=int, default=100)
|
|
42
|
-
query.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
43
|
-
query.add_argument("--all", action="store_true", dest="all_pages")
|
|
44
|
-
query.add_argument("--output", choices=("json", "csv"), default="json")
|
|
45
|
-
query.set_defaults(handler=_handle_query, format_hint="data_query")
|
|
46
|
-
|
|
47
|
-
get = commands.add_parser("get", help="读取单条应用数据")
|
|
48
|
-
get.add_argument("--app-key", required=True)
|
|
49
|
-
get.add_argument("--record-id", required=True)
|
|
50
|
-
get.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
51
|
-
get.set_defaults(handler=_handle_get, format_hint="data_get")
|
|
52
|
-
|
|
53
|
-
insert = commands.add_parser(
|
|
54
|
-
"insert", help="新增应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
55
|
-
epilog='最小示例:items.json = [{"fields": {"101": "A"}}]',
|
|
56
|
-
)
|
|
57
|
-
insert.add_argument("--app-key", required=True)
|
|
58
|
-
insert.add_argument("--items-file", required=True, help='JSON 数组,项为 {"fields":{"字段ID":值}}')
|
|
59
|
-
insert.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
60
|
-
insert.set_defaults(handler=_handle_insert, format_hint="data_insert")
|
|
61
|
-
|
|
62
|
-
update = commands.add_parser(
|
|
63
|
-
"update", help="更新应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
64
|
-
epilog='最小示例:fields.json = {"101": "B"}',
|
|
65
|
-
)
|
|
66
|
-
update.add_argument("--app-key", required=True)
|
|
67
|
-
update.add_argument("--record-id", required=True)
|
|
68
|
-
update.add_argument("--fields-file", required=True, help='JSON 对象,键为字符串形式 field_id')
|
|
69
|
-
update.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
70
|
-
update.set_defaults(handler=_handle_update, format_hint="data_update")
|
|
71
|
-
|
|
72
|
-
delete = commands.add_parser("delete", help="删除应用数据")
|
|
73
|
-
delete.add_argument("--app-key", required=True)
|
|
74
|
-
delete.add_argument(
|
|
75
|
-
"--record-ids", action="append", default=[], metavar="ID[,ID...]",
|
|
76
|
-
help="待删除记录 ID;支持逗号分隔,也可重复传入",
|
|
77
|
-
)
|
|
78
|
-
delete.add_argument("--record-ids-file", help="附加的记录 ID JSON 数组文件")
|
|
79
|
-
delete.set_defaults(handler=_handle_delete, format_hint="data_delete")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _handle_query(args: argparse.Namespace, context: CliContext) -> dict:
|
|
83
|
-
if args.all_pages != (args.output == "csv"):
|
|
84
|
-
raise_config_error(
|
|
85
|
-
"--all 与 --output csv 必须同时使用。",
|
|
86
|
-
fix_hint="使用 `qingflow data query ... --all --output csv`,普通分页查询不要传这两个参数。",
|
|
87
|
-
)
|
|
88
|
-
return context.data.data_query(
|
|
89
|
-
profile=args.profile,
|
|
90
|
-
app_key=args.app_key,
|
|
91
|
-
columns=load_list_arg(args.columns_file, option_name="--columns-file"),
|
|
92
|
-
where=load_list_arg(args.where_file, option_name="--where-file"),
|
|
93
|
-
sort=load_list_arg(args.sort_file, option_name="--sort-file"),
|
|
94
|
-
search=args.search,
|
|
95
|
-
page=args.page,
|
|
96
|
-
page_size=args.page_size,
|
|
97
|
-
include_schema=args.include_schema,
|
|
98
|
-
all_pages=args.all_pages,
|
|
99
|
-
output=args.output,
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
104
|
-
return context.data.data_get(
|
|
105
|
-
profile=args.profile, app_key=args.app_key, record_id=args.record_id, include_schema=args.include_schema
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
|
|
110
|
-
return context.data.data_insert(
|
|
111
|
-
profile=args.profile, app_key=args.app_key,
|
|
112
|
-
items=require_list_arg(args.items_file, option_name="--items-file"), include_schema=args.include_schema,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _handle_update(args: argparse.Namespace, context: CliContext) -> dict:
|
|
117
|
-
return context.data.data_update(
|
|
118
|
-
profile=args.profile, app_key=args.app_key, record_id=args.record_id,
|
|
119
|
-
fields=require_object_arg(args.fields_file, option_name="--fields-file"), include_schema=args.include_schema,
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _handle_delete(args: argparse.Namespace, context: CliContext) -> dict:
|
|
124
|
-
record_ids = [item.strip() for group in args.record_ids for item in group.split(",") if item.strip()]
|
|
125
|
-
record_ids.extend(load_list_arg(args.record_ids_file, option_name="--record-ids-file"))
|
|
126
|
-
if not record_ids:
|
|
127
|
-
raise_config_error(
|
|
128
|
-
"至少传入一个记录 ID。",
|
|
129
|
-
fix_hint="使用 `--record-ids ID[,ID...]`;也可附加 `--record-ids-file ids.json`。",
|
|
130
|
-
)
|
|
131
|
-
return context.data.data_delete(
|
|
132
|
-
profile=args.profile, app_key=args.app_key,
|
|
133
|
-
record_ids=record_ids,
|
|
134
|
-
)
|