@svadmin/surface 0.11.8 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/compatibility.json +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/json-render.d.ts +68 -0
- package/dist/json-render.js +117 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -189,3 +189,18 @@ Limits are eight data sources, 24 widgets, 100 rows per page, eight filters, thr
|
|
|
189
189
|
Actual OpenUI Lang parsing, nested interactive forms, registered business actions, server persistence, SSR/Lite rendering, arbitrary URLs, client aggregation, Canvas and iframe execution are not implemented. Aggregated metrics should come from a policy-authorized backend summary resource and bind through `resource-one`. The current UI contract remains read-only even though its definition can be edited.
|
|
190
190
|
|
|
191
191
|
See `docs/architecture/openui-surface-phase2.md` for the original feature scope and `docs/architecture/surface-integration-provenance.md` for historical integration evidence. 中文指南见文档站的“声明式 Surface”。
|
|
192
|
+
|
|
193
|
+
## json-render 适配
|
|
194
|
+
|
|
195
|
+
Surface 可以接收 Vercel `json-render` 使用的标准扁平 `Spec`(`root` + `elements`),同时继续由 Surface 的 TypeBox Schema、组件目录和资源策略负责最终校验。协议入口提供 `jsonRenderSpecToSurfaceSpec` 和 `surfaceSpecToJsonRenderSpec`:前者只接受以 `surface` 为根、一级业务组件为子节点的受控子集,后者用于把已校验的 SurfaceSpec 输出为标准扁平 Spec。
|
|
196
|
+
|
|
197
|
+
适配层不执行 json-render 的任意 action、脚本、URL 或嵌套组件树;业务数据查询仍经过 Surface DataProvider 和既有授权流程。项目业务契约继续使用 TypeBox,未引入 Zod 运行时依赖。本入口只提供格式互操作,不包含官方 json-render Catalog、流式编译器或渲染器集成。`binding` 和 `placement` 是适配器保留属性;导出包含这些业务属性的组件时会抛错,避免静默覆盖。
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { jsonRenderSpecToSurfaceSpec } from '@svadmin/surface';
|
|
201
|
+
|
|
202
|
+
const result = jsonRenderSpecToSurfaceSpec(jsonRenderSpec, catalog, policy);
|
|
203
|
+
if (result.ok) {
|
|
204
|
+
// 交给现有 SurfaceRenderer;此处仍是候选页面,不代表业务动作授权。
|
|
205
|
+
}
|
|
206
|
+
```
|
package/compatibility.json
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -10,3 +10,5 @@ export { SURFACE_EDIT_SCHEMA_VERSION, SURFACE_EDIT_LIMITS, createSurfaceRevision
|
|
|
10
10
|
export type { SurfaceEditOperation, SurfaceEditProposal, SurfaceEditIssue, SurfaceRevision, SurfaceRevisionResult, } from './edits.js';
|
|
11
11
|
export { svarGridPropsSchema, svarGridDefinition } from './svar-schema.js';
|
|
12
12
|
export type { SvarGridProps } from './svar-schema.js';
|
|
13
|
+
export { jsonRenderSurfaceRootPropsSchema, jsonRenderUiElementSchema, jsonRenderSpecSchema, jsonRenderSpecToSurfaceSpec, surfaceSpecToJsonRenderSpec, } from './json-render.js';
|
|
14
|
+
export type { JsonRenderSurfaceElement, JsonRenderSurfaceSpec, JsonRenderSurfaceResult, } from './json-render.js';
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,4 @@ export { SURFACE_AGENT_LIMITS, SURFACE_AGENT_RESPONSE_SCHEMA_VERSION, SURFACE_CA
|
|
|
5
5
|
export { SURFACE_EDIT_SCHEMA_VERSION, SURFACE_EDIT_LIMITS, createSurfaceRevision, applySurfaceEditProposal, createSurfaceEditSchema, buildSurfaceEditMessages, } from './edits.js';
|
|
6
6
|
// 只导出 JSON schema,不将可选 SVAR 渲染器加载到协议入口。
|
|
7
7
|
export { svarGridPropsSchema, svarGridDefinition } from './svar-schema.js';
|
|
8
|
+
export { jsonRenderSurfaceRootPropsSchema, jsonRenderUiElementSchema, jsonRenderSpecSchema, jsonRenderSpecToSurfaceSpec, surfaceSpecToJsonRenderSpec, } from './json-render.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type Static } from '@sinclair/typebox';
|
|
2
|
+
import type { SurfaceCatalog, SurfacePolicy, SurfaceSpec } from './types.js';
|
|
3
|
+
/** json-render 的标准扁平 Spec 子集;业务字段仍由 Surface 的 TypeBox 契约校验。 */
|
|
4
|
+
export declare const jsonRenderSurfaceRootPropsSchema: import("@sinclair/typebox").TObject<{
|
|
5
|
+
layout: import("@sinclair/typebox").TObject<{
|
|
6
|
+
type: import("@sinclair/typebox").TLiteral<"grid">;
|
|
7
|
+
columns: import("@sinclair/typebox").TLiteral<12>;
|
|
8
|
+
gap: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"sm">, import("@sinclair/typebox").TLiteral<"md">, import("@sinclair/typebox").TLiteral<"lg">]>>;
|
|
9
|
+
}>;
|
|
10
|
+
title: import("@sinclair/typebox").TString;
|
|
11
|
+
catalogVersion: import("@sinclair/typebox").TString;
|
|
12
|
+
surfaceId: import("@sinclair/typebox").TString;
|
|
13
|
+
dataSources: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
|
14
|
+
id: import("@sinclair/typebox").TString;
|
|
15
|
+
type: import("@sinclair/typebox").TLiteral<"resource-list">;
|
|
16
|
+
resource: import("@sinclair/typebox").TString;
|
|
17
|
+
pageSize: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
|
|
18
|
+
sorters: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
19
|
+
field: import("@sinclair/typebox").TString;
|
|
20
|
+
order: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"asc">, import("@sinclair/typebox").TLiteral<"desc">]>;
|
|
21
|
+
}>>>;
|
|
22
|
+
filters: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
|
23
|
+
field: import("@sinclair/typebox").TString;
|
|
24
|
+
operator: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"eq">, import("@sinclair/typebox").TLiteral<"ne">, import("@sinclair/typebox").TLiteral<"lt">, import("@sinclair/typebox").TLiteral<"lte">, import("@sinclair/typebox").TLiteral<"gt">, import("@sinclair/typebox").TLiteral<"gte">, import("@sinclair/typebox").TLiteral<"contains">, import("@sinclair/typebox").TLiteral<"startswith">, import("@sinclair/typebox").TLiteral<"endswith">]>;
|
|
25
|
+
value: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TBoolean, import("@sinclair/typebox").TNull]>;
|
|
26
|
+
}>, import("@sinclair/typebox").TObject<{
|
|
27
|
+
field: import("@sinclair/typebox").TString;
|
|
28
|
+
operator: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"in">, import("@sinclair/typebox").TLiteral<"nin">]>;
|
|
29
|
+
value: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TBoolean, import("@sinclair/typebox").TNull]>>;
|
|
30
|
+
}>, import("@sinclair/typebox").TObject<{
|
|
31
|
+
field: import("@sinclair/typebox").TString;
|
|
32
|
+
operator: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"null">, import("@sinclair/typebox").TLiteral<"nnull">]>;
|
|
33
|
+
}>]>>>;
|
|
34
|
+
}>, import("@sinclair/typebox").TObject<{
|
|
35
|
+
id: import("@sinclair/typebox").TString;
|
|
36
|
+
type: import("@sinclair/typebox").TLiteral<"resource-one">;
|
|
37
|
+
resource: import("@sinclair/typebox").TString;
|
|
38
|
+
recordId: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNumber]>;
|
|
39
|
+
}>]>>;
|
|
40
|
+
}>;
|
|
41
|
+
export declare const jsonRenderUiElementSchema: import("@sinclair/typebox").TObject<{
|
|
42
|
+
type: import("@sinclair/typebox").TString;
|
|
43
|
+
props: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>;
|
|
44
|
+
children: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
45
|
+
}>;
|
|
46
|
+
export declare const jsonRenderSpecSchema: import("@sinclair/typebox").TObject<{
|
|
47
|
+
root: import("@sinclair/typebox").TString;
|
|
48
|
+
elements: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
|
|
49
|
+
type: import("@sinclair/typebox").TString;
|
|
50
|
+
props: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>;
|
|
51
|
+
children: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
52
|
+
}>>;
|
|
53
|
+
}>;
|
|
54
|
+
export type JsonRenderSurfaceElement = Static<typeof jsonRenderUiElementSchema>;
|
|
55
|
+
export type JsonRenderSurfaceSpec = Static<typeof jsonRenderSpecSchema>;
|
|
56
|
+
export type JsonRenderSurfaceResult = {
|
|
57
|
+
readonly ok: true;
|
|
58
|
+
readonly value: SurfaceSpec;
|
|
59
|
+
} | {
|
|
60
|
+
readonly ok: false;
|
|
61
|
+
readonly issues: readonly {
|
|
62
|
+
readonly path: string;
|
|
63
|
+
readonly message: string;
|
|
64
|
+
}[];
|
|
65
|
+
};
|
|
66
|
+
export declare function jsonRenderSpecToSurfaceSpec(input: unknown, catalog: SurfaceCatalog, policy: SurfacePolicy): JsonRenderSurfaceResult;
|
|
67
|
+
/** 将已校验的 SurfaceSpec 编码为 json-render 的标准扁平 Spec。 */
|
|
68
|
+
export declare function surfaceSpecToJsonRenderSpec(spec: SurfaceSpec): JsonRenderSurfaceSpec;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { Value } from '@sinclair/typebox/value';
|
|
3
|
+
import { validateSurfaceSpec } from './validation.js';
|
|
4
|
+
import { jsonPointer } from './json.js';
|
|
5
|
+
import { parseSurfaceJson } from './wire.js';
|
|
6
|
+
import { surfaceSpecSchema, surfaceWidgetSchema } from './schema.js';
|
|
7
|
+
/** json-render 的标准扁平 Spec 子集;业务字段仍由 Surface 的 TypeBox 契约校验。 */
|
|
8
|
+
export const jsonRenderSurfaceRootPropsSchema = Type.Omit(surfaceSpecSchema, ['schemaVersion', 'widgets']);
|
|
9
|
+
export const jsonRenderUiElementSchema = Type.Object({
|
|
10
|
+
type: Type.String({ minLength: 1 }),
|
|
11
|
+
props: Type.Record(Type.String(), Type.Unknown()),
|
|
12
|
+
children: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
13
|
+
}, { additionalProperties: false });
|
|
14
|
+
export const jsonRenderSpecSchema = Type.Object({
|
|
15
|
+
root: Type.String({ minLength: 1 }),
|
|
16
|
+
elements: Type.Record(Type.String({ minLength: 1 }), jsonRenderUiElementSchema),
|
|
17
|
+
}, { additionalProperties: false });
|
|
18
|
+
function issue(path, message) {
|
|
19
|
+
return { path, message };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 将 json-render Spec 转换为受 Surface 权限、目录和 TypeBox 规则保护的 SurfaceSpec。
|
|
23
|
+
* 只接受 root 类型为 surface 的子集,避免把任意 json-render 组件树当作业务页面执行。
|
|
24
|
+
*/
|
|
25
|
+
function readSurfaceRoot(candidate) {
|
|
26
|
+
const root = candidate.elements[candidate.root];
|
|
27
|
+
if (!root || root.type !== 'surface')
|
|
28
|
+
return { error: 'json-render root must reference a surface element' };
|
|
29
|
+
if (!Value.Check(jsonRenderSurfaceRootPropsSchema, root.props))
|
|
30
|
+
return { error: 'Invalid surface root properties' };
|
|
31
|
+
return root;
|
|
32
|
+
}
|
|
33
|
+
function readWidgetElement(element, id) {
|
|
34
|
+
if (element.type === 'surface')
|
|
35
|
+
return { error: 'Nested surface elements are not supported' };
|
|
36
|
+
if (element.children?.length)
|
|
37
|
+
return { error: 'Widget children are not supported' };
|
|
38
|
+
const binding = element.props['binding'];
|
|
39
|
+
const placement = element.props['placement'];
|
|
40
|
+
if (binding !== undefined && !Value.Check(surfaceWidgetSchema.properties.binding, binding))
|
|
41
|
+
return { error: 'Invalid widget binding' };
|
|
42
|
+
if (placement !== undefined && !Value.Check(surfaceWidgetSchema.properties.placement, placement))
|
|
43
|
+
return { error: 'Invalid widget placement' };
|
|
44
|
+
const props = { ...element.props };
|
|
45
|
+
delete props['binding'];
|
|
46
|
+
delete props['placement'];
|
|
47
|
+
return { id, type: element.type, props,
|
|
48
|
+
...(binding === undefined ? {} : { binding }),
|
|
49
|
+
...(placement === undefined ? {} : { placement }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function parseJsonRenderSpec(input) {
|
|
53
|
+
const parsed = parseSurfaceJson(input);
|
|
54
|
+
if (!parsed.ok)
|
|
55
|
+
return { error: parsed.issues[0]?.message ?? 'Invalid JSON', path: parsed.issues[0]?.path ?? '' };
|
|
56
|
+
if (!Value.Check(jsonRenderSpecSchema, parsed.value))
|
|
57
|
+
return { error: 'Invalid json-render spec' };
|
|
58
|
+
return parsed.value;
|
|
59
|
+
}
|
|
60
|
+
export function jsonRenderSpecToSurfaceSpec(input, catalog, policy) {
|
|
61
|
+
const parsed = parseJsonRenderSpec(input);
|
|
62
|
+
if ('error' in parsed)
|
|
63
|
+
return { ok: false, issues: [issue(parsed.path ?? '', parsed.error)] };
|
|
64
|
+
const root = readSurfaceRoot(parsed);
|
|
65
|
+
if ('error' in root)
|
|
66
|
+
return { ok: false, issues: [issue('/root', root.error)] };
|
|
67
|
+
const children = root.children ?? [];
|
|
68
|
+
const widgets = [];
|
|
69
|
+
for (const childId of children) {
|
|
70
|
+
const element = parsed.elements[childId];
|
|
71
|
+
if (!element)
|
|
72
|
+
return { ok: false, issues: [issue(jsonPointer(['elements', childId]), 'Missing child element')] };
|
|
73
|
+
const widget = readWidgetElement(element, childId);
|
|
74
|
+
if ('error' in widget)
|
|
75
|
+
return { ok: false, issues: [issue(jsonPointer(['elements', childId]), widget.error)] };
|
|
76
|
+
widgets.push(widget);
|
|
77
|
+
}
|
|
78
|
+
if (new Set(children).size !== children.length || Object.keys(parsed.elements).length !== children.length + 1)
|
|
79
|
+
return { ok: false, issues: [issue('/elements', 'Every element must occur exactly once in the surface tree')] };
|
|
80
|
+
const surface = { schemaVersion: 'surface/v1', ...root.props, widgets };
|
|
81
|
+
const validation = validateSurfaceSpec(surface, catalog, policy);
|
|
82
|
+
return validation.ok ? validation : { ok: false, issues: validation.issues.map(({ path, message }) => ({ path, message })) };
|
|
83
|
+
}
|
|
84
|
+
/** 将已校验的 SurfaceSpec 编码为 json-render 的标准扁平 Spec。 */
|
|
85
|
+
export function surfaceSpecToJsonRenderSpec(spec) {
|
|
86
|
+
const root = '__svadmin_surface_root';
|
|
87
|
+
if (spec.widgets.some(widget => widget.id === root)) {
|
|
88
|
+
throw new Error('Surface widget id is reserved by the json-render adapter');
|
|
89
|
+
}
|
|
90
|
+
const elements = {
|
|
91
|
+
[root]: {
|
|
92
|
+
type: 'surface',
|
|
93
|
+
props: {
|
|
94
|
+
catalogVersion: spec.catalogVersion,
|
|
95
|
+
surfaceId: spec.surfaceId,
|
|
96
|
+
title: spec.title,
|
|
97
|
+
layout: spec.layout,
|
|
98
|
+
dataSources: spec.dataSources,
|
|
99
|
+
},
|
|
100
|
+
children: spec.widgets.map(widget => widget.id),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
for (const widget of spec.widgets) {
|
|
104
|
+
if (Object.hasOwn(widget.props, 'binding') || Object.hasOwn(widget.props, 'placement')) {
|
|
105
|
+
throw new Error('Widget props binding and placement are reserved by the json-render adapter');
|
|
106
|
+
}
|
|
107
|
+
elements[widget.id] = {
|
|
108
|
+
type: widget.type,
|
|
109
|
+
props: {
|
|
110
|
+
...widget.props,
|
|
111
|
+
...(widget.binding ? { binding: widget.binding } : {}),
|
|
112
|
+
...(widget.placement ? { placement: widget.placement } : {}),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { root, elements };
|
|
117
|
+
}
|