@svadmin/surface 0.4.7 → 0.6.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 +2 -2
- package/compatibility.json +1 -1
- package/dist/builtin-schemas.d.ts +172 -42
- package/dist/builtin-schemas.js +76 -31
- package/dist/catalog.d.ts +151 -37
- package/dist/types.d.ts +7 -2
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +165 -103
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ The MVP supports Svelte 5 + Vite, a fixed 12-column grid, four built-in widgets,
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
bun add @svadmin/surface @svadmin/core @svadmin/ui @tanstack/svelte-query svelte
|
|
10
|
+
bun add @svadmin/surface @svadmin/core @svadmin/ui @tanstack/svelte-query svelte @sinclair/typebox
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Import protocol types and validation from the DOM-free root entry. Import rendering code from the Svelte subpath.
|
|
@@ -89,7 +89,7 @@ When rendered inside `AdminApp`, the renderer resolves the configured provider f
|
|
|
89
89
|
|
|
90
90
|
The catalog version is `svadmin/v1`. `catalogVersion` must match exactly.
|
|
91
91
|
|
|
92
|
-
Custom catalogs are trusted runtime configuration. Every registration must use a strict
|
|
92
|
+
Custom catalogs are trusted runtime configuration. Every registration must use a strict TypeBox object schema and a trusted Svelte component. Item widgets that read record fields must expose those fields through `getReferencedFields`; validation then checks them against `SurfacePolicy.readFields`.
|
|
93
93
|
|
|
94
94
|
```ts
|
|
95
95
|
const catalog = defineSurfaceCatalog({
|
package/compatibility.json
CHANGED
|
@@ -1,42 +1,172 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
1
|
+
import { type Static, type TSchema } from "@sinclair/typebox";
|
|
2
|
+
export declare function createParsedSchema<T extends TSchema>(schema: T): T & {
|
|
3
|
+
parse: (value: unknown) => Static<T>;
|
|
4
|
+
safeParse: (value: unknown) => {
|
|
5
|
+
success: true;
|
|
6
|
+
data: Static<T>;
|
|
7
|
+
} | {
|
|
8
|
+
success: false;
|
|
9
|
+
error: {
|
|
10
|
+
issues: Array<{
|
|
11
|
+
path: string[];
|
|
12
|
+
message: string;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
Check: (value: unknown) => boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare const metricPropsSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
|
19
|
+
label: import("@sinclair/typebox").TString;
|
|
20
|
+
format: import("@sinclair/typebox").TLiteral<"currency">;
|
|
21
|
+
currency: import("@sinclair/typebox").TString;
|
|
22
|
+
description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
23
|
+
}>, import("@sinclair/typebox").TObject<{
|
|
24
|
+
label: import("@sinclair/typebox").TString;
|
|
25
|
+
format: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"number">, import("@sinclair/typebox").TLiteral<"percent">]>;
|
|
26
|
+
currency: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNever>;
|
|
27
|
+
description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
28
|
+
}>]> & {
|
|
29
|
+
parse: (value: unknown) => {
|
|
30
|
+
description?: string | undefined;
|
|
31
|
+
currency: string;
|
|
32
|
+
label: string;
|
|
33
|
+
format: "currency";
|
|
34
|
+
} | {
|
|
35
|
+
description?: string | undefined;
|
|
36
|
+
currency?: undefined;
|
|
37
|
+
label: string;
|
|
38
|
+
format: "number" | "percent";
|
|
39
|
+
};
|
|
40
|
+
safeParse: (value: unknown) => {
|
|
41
|
+
success: false;
|
|
42
|
+
error: {
|
|
43
|
+
issues: Array<{
|
|
44
|
+
path: string[];
|
|
45
|
+
message: string;
|
|
46
|
+
}>;
|
|
47
|
+
};
|
|
48
|
+
} | {
|
|
49
|
+
success: true;
|
|
50
|
+
data: {
|
|
51
|
+
description?: string | undefined;
|
|
52
|
+
currency: string;
|
|
53
|
+
label: string;
|
|
54
|
+
format: "currency";
|
|
55
|
+
} | {
|
|
56
|
+
description?: string | undefined;
|
|
57
|
+
currency?: undefined;
|
|
58
|
+
label: string;
|
|
59
|
+
format: "number" | "percent";
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
Check: (value: unknown) => boolean;
|
|
63
|
+
};
|
|
64
|
+
export declare const resourceTablePropsSchema: import("@sinclair/typebox").TObject<{
|
|
65
|
+
title: import("@sinclair/typebox").TString;
|
|
66
|
+
emptyLabel: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
67
|
+
columns: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
68
|
+
field: import("@sinclair/typebox").TString;
|
|
69
|
+
label: import("@sinclair/typebox").TString;
|
|
70
|
+
format: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"text">, import("@sinclair/typebox").TLiteral<"number">, import("@sinclair/typebox").TLiteral<"date">, import("@sinclair/typebox").TLiteral<"boolean">]>>;
|
|
71
|
+
}>>;
|
|
72
|
+
}> & {
|
|
73
|
+
parse: (value: unknown) => {
|
|
74
|
+
emptyLabel?: string | undefined;
|
|
75
|
+
title: string;
|
|
76
|
+
columns: {
|
|
77
|
+
format?: "number" | "boolean" | "text" | "date" | undefined;
|
|
78
|
+
field: string;
|
|
79
|
+
label: string;
|
|
80
|
+
}[];
|
|
81
|
+
};
|
|
82
|
+
safeParse: (value: unknown) => {
|
|
83
|
+
success: false;
|
|
84
|
+
error: {
|
|
85
|
+
issues: Array<{
|
|
86
|
+
path: string[];
|
|
87
|
+
message: string;
|
|
88
|
+
}>;
|
|
89
|
+
};
|
|
90
|
+
} | {
|
|
91
|
+
success: true;
|
|
92
|
+
data: {
|
|
93
|
+
emptyLabel?: string | undefined;
|
|
94
|
+
title: string;
|
|
95
|
+
columns: {
|
|
96
|
+
format?: "number" | "boolean" | "text" | "date" | undefined;
|
|
97
|
+
field: string;
|
|
98
|
+
label: string;
|
|
99
|
+
}[];
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
Check: (value: unknown) => boolean;
|
|
103
|
+
};
|
|
104
|
+
export declare const barChartPropsSchema: import("@sinclair/typebox").TObject<{
|
|
105
|
+
title: import("@sinclair/typebox").TString;
|
|
106
|
+
labelField: import("@sinclair/typebox").TString;
|
|
107
|
+
valueField: import("@sinclair/typebox").TString;
|
|
108
|
+
showValues: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
109
|
+
}> & {
|
|
110
|
+
parse: (value: unknown) => {
|
|
111
|
+
showValues?: boolean | undefined;
|
|
112
|
+
title: string;
|
|
113
|
+
labelField: string;
|
|
114
|
+
valueField: string;
|
|
115
|
+
};
|
|
116
|
+
safeParse: (value: unknown) => {
|
|
117
|
+
success: false;
|
|
118
|
+
error: {
|
|
119
|
+
issues: Array<{
|
|
120
|
+
path: string[];
|
|
121
|
+
message: string;
|
|
122
|
+
}>;
|
|
123
|
+
};
|
|
124
|
+
} | {
|
|
125
|
+
success: true;
|
|
126
|
+
data: {
|
|
127
|
+
showValues?: boolean | undefined;
|
|
128
|
+
title: string;
|
|
129
|
+
labelField: string;
|
|
130
|
+
valueField: string;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
Check: (value: unknown) => boolean;
|
|
134
|
+
};
|
|
135
|
+
export declare const lineChartPropsSchema: import("@sinclair/typebox").TObject<{
|
|
136
|
+
title: import("@sinclair/typebox").TString;
|
|
137
|
+
labelField: import("@sinclair/typebox").TString;
|
|
138
|
+
valueField: import("@sinclair/typebox").TString;
|
|
139
|
+
showDots: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
140
|
+
fill: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
141
|
+
}> & {
|
|
142
|
+
parse: (value: unknown) => {
|
|
143
|
+
fill?: boolean | undefined;
|
|
144
|
+
showDots?: boolean | undefined;
|
|
145
|
+
title: string;
|
|
146
|
+
labelField: string;
|
|
147
|
+
valueField: string;
|
|
148
|
+
};
|
|
149
|
+
safeParse: (value: unknown) => {
|
|
150
|
+
success: false;
|
|
151
|
+
error: {
|
|
152
|
+
issues: Array<{
|
|
153
|
+
path: string[];
|
|
154
|
+
message: string;
|
|
155
|
+
}>;
|
|
156
|
+
};
|
|
157
|
+
} | {
|
|
158
|
+
success: true;
|
|
159
|
+
data: {
|
|
160
|
+
fill?: boolean | undefined;
|
|
161
|
+
showDots?: boolean | undefined;
|
|
162
|
+
title: string;
|
|
163
|
+
labelField: string;
|
|
164
|
+
valueField: string;
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
Check: (value: unknown) => boolean;
|
|
168
|
+
};
|
|
169
|
+
export type MetricProps = Static<typeof metricPropsSchema>;
|
|
170
|
+
export type ResourceTableProps = Static<typeof resourceTablePropsSchema>;
|
|
171
|
+
export type BarChartProps = Static<typeof barChartPropsSchema>;
|
|
172
|
+
export type LineChartProps = Static<typeof lineChartPropsSchema>;
|
package/dist/builtin-schemas.js
CHANGED
|
@@ -1,37 +1,82 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
3
|
+
export function createParsedSchema(schema) {
|
|
4
|
+
const compiled = TypeCompiler.Compile(schema);
|
|
5
|
+
const parse = (value) => {
|
|
6
|
+
if (compiled.Check(value))
|
|
7
|
+
return value;
|
|
8
|
+
const errors = [...compiled.Errors(value)];
|
|
9
|
+
const message = errors.map((e) => (e.path ? e.path + ": " : "") + e.message).join(", ") || "Validation error";
|
|
10
|
+
const err = new Error(message);
|
|
11
|
+
err.issues = errors.map((e) => ({
|
|
12
|
+
path: e.path ? (e.path.startsWith("/") ? e.path.slice(1) : e.path).split("/") : [],
|
|
13
|
+
message: e.message,
|
|
14
|
+
}));
|
|
15
|
+
throw err;
|
|
16
|
+
};
|
|
17
|
+
const safeParse = (value) => {
|
|
18
|
+
if (compiled.Check(value)) {
|
|
19
|
+
return { success: true, data: value };
|
|
20
|
+
}
|
|
21
|
+
const errors = [...compiled.Errors(value)];
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
error: {
|
|
25
|
+
issues: errors.map((e) => ({
|
|
26
|
+
path: e.path ? (e.path.startsWith("/") ? e.path.slice(1) : e.path).split("/") : [],
|
|
27
|
+
message: e.message,
|
|
28
|
+
})),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
return Object.assign(schema, {
|
|
33
|
+
parse,
|
|
34
|
+
safeParse,
|
|
35
|
+
Check: (val) => compiled.Check(val),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const catalogFieldSchema = Type.String({
|
|
39
|
+
minLength: 1,
|
|
40
|
+
maxLength: 64,
|
|
41
|
+
pattern: "^[A-Za-z][A-Za-z0-9_-]*$",
|
|
15
42
|
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
43
|
+
const currencyMetric = Type.Object({
|
|
44
|
+
label: Type.String({ minLength: 1, maxLength: 80 }),
|
|
45
|
+
format: Type.Literal("currency"),
|
|
46
|
+
currency: Type.String({ pattern: "^[A-Z]{3}$" }),
|
|
47
|
+
description: Type.Optional(Type.String({ minLength: 1, maxLength: 160 })),
|
|
48
|
+
}, { additionalProperties: false });
|
|
49
|
+
const otherMetric = Type.Object({
|
|
50
|
+
label: Type.String({ minLength: 1, maxLength: 80 }),
|
|
51
|
+
format: Type.Union([Type.Literal("number"), Type.Literal("percent")]),
|
|
52
|
+
currency: Type.Optional(Type.Never()),
|
|
53
|
+
description: Type.Optional(Type.String({ minLength: 1, maxLength: 160 })),
|
|
54
|
+
}, { additionalProperties: false });
|
|
55
|
+
export const metricPropsSchema = createParsedSchema(Type.Union([currencyMetric, otherMetric]));
|
|
56
|
+
export const resourceTablePropsSchema = createParsedSchema(Type.Object({
|
|
57
|
+
title: Type.String({ minLength: 1, maxLength: 80 }),
|
|
58
|
+
emptyLabel: Type.Optional(Type.String({ minLength: 1, maxLength: 80 })),
|
|
59
|
+
columns: Type.Array(Type.Object({
|
|
20
60
|
field: catalogFieldSchema,
|
|
21
|
-
label:
|
|
22
|
-
format:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
61
|
+
label: Type.String({ minLength: 1, maxLength: 60 }),
|
|
62
|
+
format: Type.Optional(Type.Union([
|
|
63
|
+
Type.Literal("text"),
|
|
64
|
+
Type.Literal("number"),
|
|
65
|
+
Type.Literal("date"),
|
|
66
|
+
Type.Literal("boolean"),
|
|
67
|
+
])),
|
|
68
|
+
}, { additionalProperties: false }), { minItems: 1, maxItems: 8 }),
|
|
69
|
+
}, { additionalProperties: false }));
|
|
70
|
+
export const barChartPropsSchema = createParsedSchema(Type.Object({
|
|
71
|
+
title: Type.String({ minLength: 1, maxLength: 80 }),
|
|
27
72
|
labelField: catalogFieldSchema,
|
|
28
73
|
valueField: catalogFieldSchema,
|
|
29
|
-
showValues:
|
|
30
|
-
})
|
|
31
|
-
export const lineChartPropsSchema =
|
|
32
|
-
title:
|
|
74
|
+
showValues: Type.Optional(Type.Boolean()),
|
|
75
|
+
}, { additionalProperties: false }));
|
|
76
|
+
export const lineChartPropsSchema = createParsedSchema(Type.Object({
|
|
77
|
+
title: Type.String({ minLength: 1, maxLength: 80 }),
|
|
33
78
|
labelField: catalogFieldSchema,
|
|
34
79
|
valueField: catalogFieldSchema,
|
|
35
|
-
showDots:
|
|
36
|
-
fill:
|
|
37
|
-
})
|
|
80
|
+
showDots: Type.Optional(Type.Boolean()),
|
|
81
|
+
fill: Type.Optional(Type.Boolean()),
|
|
82
|
+
}, { additionalProperties: false }));
|
package/dist/catalog.d.ts
CHANGED
|
@@ -21,57 +21,171 @@ export declare const defaultSurfaceCatalog: {
|
|
|
21
21
|
readonly widgets: readonly [{
|
|
22
22
|
readonly type: "metric";
|
|
23
23
|
readonly dataKind: "scalar";
|
|
24
|
-
readonly propsSchema: import("
|
|
25
|
-
label: import("
|
|
26
|
-
format: import("
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
readonly propsSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
|
25
|
+
label: import("@sinclair/typebox").TString;
|
|
26
|
+
format: import("@sinclair/typebox").TLiteral<"currency">;
|
|
27
|
+
currency: import("@sinclair/typebox").TString;
|
|
28
|
+
description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
29
|
+
}>, import("@sinclair/typebox").TObject<{
|
|
30
|
+
label: import("@sinclair/typebox").TString;
|
|
31
|
+
format: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"number">, import("@sinclair/typebox").TLiteral<"percent">]>;
|
|
32
|
+
currency: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNever>;
|
|
33
|
+
description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
34
|
+
}>]> & {
|
|
35
|
+
parse: (value: unknown) => {
|
|
36
|
+
description?: string | undefined;
|
|
37
|
+
currency: string;
|
|
38
|
+
label: string;
|
|
39
|
+
format: "currency";
|
|
40
|
+
} | {
|
|
41
|
+
description?: string | undefined;
|
|
42
|
+
currency?: undefined;
|
|
43
|
+
label: string;
|
|
44
|
+
format: "number" | "percent";
|
|
45
|
+
};
|
|
46
|
+
safeParse: (value: unknown) => {
|
|
47
|
+
success: false;
|
|
48
|
+
error: {
|
|
49
|
+
issues: Array<{
|
|
50
|
+
path: string[];
|
|
51
|
+
message: string;
|
|
52
|
+
}>;
|
|
53
|
+
};
|
|
54
|
+
} | {
|
|
55
|
+
success: true;
|
|
56
|
+
data: {
|
|
57
|
+
description?: string | undefined;
|
|
58
|
+
currency: string;
|
|
59
|
+
label: string;
|
|
60
|
+
format: "currency";
|
|
61
|
+
} | {
|
|
62
|
+
description?: string | undefined;
|
|
63
|
+
currency?: undefined;
|
|
64
|
+
label: string;
|
|
65
|
+
format: "number" | "percent";
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
Check: (value: unknown) => boolean;
|
|
69
|
+
};
|
|
34
70
|
readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
|
|
35
71
|
}, {
|
|
36
72
|
readonly type: "resource-table";
|
|
37
73
|
readonly dataKind: "items";
|
|
38
|
-
readonly propsSchema: import("
|
|
39
|
-
title: import("
|
|
40
|
-
emptyLabel: import("
|
|
41
|
-
columns: import("
|
|
42
|
-
field: import("
|
|
43
|
-
label: import("
|
|
44
|
-
format: import("
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
readonly propsSchema: import("@sinclair/typebox").TObject<{
|
|
75
|
+
title: import("@sinclair/typebox").TString;
|
|
76
|
+
emptyLabel: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
77
|
+
columns: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
78
|
+
field: import("@sinclair/typebox").TString;
|
|
79
|
+
label: import("@sinclair/typebox").TString;
|
|
80
|
+
format: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"text">, import("@sinclair/typebox").TLiteral<"number">, import("@sinclair/typebox").TLiteral<"date">, import("@sinclair/typebox").TLiteral<"boolean">]>>;
|
|
81
|
+
}>>;
|
|
82
|
+
}> & {
|
|
83
|
+
parse: (value: unknown) => {
|
|
84
|
+
emptyLabel?: string | undefined;
|
|
85
|
+
title: string;
|
|
86
|
+
columns: {
|
|
87
|
+
format?: "number" | "boolean" | "text" | "date" | undefined;
|
|
88
|
+
field: string;
|
|
89
|
+
label: string;
|
|
90
|
+
}[];
|
|
91
|
+
};
|
|
92
|
+
safeParse: (value: unknown) => {
|
|
93
|
+
success: false;
|
|
94
|
+
error: {
|
|
95
|
+
issues: Array<{
|
|
96
|
+
path: string[];
|
|
97
|
+
message: string;
|
|
98
|
+
}>;
|
|
99
|
+
};
|
|
100
|
+
} | {
|
|
101
|
+
success: true;
|
|
102
|
+
data: {
|
|
103
|
+
emptyLabel?: string | undefined;
|
|
104
|
+
title: string;
|
|
105
|
+
columns: {
|
|
106
|
+
format?: "number" | "boolean" | "text" | "date" | undefined;
|
|
107
|
+
field: string;
|
|
108
|
+
label: string;
|
|
109
|
+
}[];
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
Check: (value: unknown) => boolean;
|
|
113
|
+
};
|
|
52
114
|
readonly getReferencedFields: typeof tableFields;
|
|
53
115
|
readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
|
|
54
116
|
}, {
|
|
55
117
|
readonly type: "bar-chart";
|
|
56
118
|
readonly dataKind: "items";
|
|
57
|
-
readonly propsSchema: import("
|
|
58
|
-
title: import("
|
|
59
|
-
labelField: import("
|
|
60
|
-
valueField: import("
|
|
61
|
-
showValues: import("
|
|
62
|
-
}
|
|
119
|
+
readonly propsSchema: import("@sinclair/typebox").TObject<{
|
|
120
|
+
title: import("@sinclair/typebox").TString;
|
|
121
|
+
labelField: import("@sinclair/typebox").TString;
|
|
122
|
+
valueField: import("@sinclair/typebox").TString;
|
|
123
|
+
showValues: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
124
|
+
}> & {
|
|
125
|
+
parse: (value: unknown) => {
|
|
126
|
+
showValues?: boolean | undefined;
|
|
127
|
+
title: string;
|
|
128
|
+
labelField: string;
|
|
129
|
+
valueField: string;
|
|
130
|
+
};
|
|
131
|
+
safeParse: (value: unknown) => {
|
|
132
|
+
success: false;
|
|
133
|
+
error: {
|
|
134
|
+
issues: Array<{
|
|
135
|
+
path: string[];
|
|
136
|
+
message: string;
|
|
137
|
+
}>;
|
|
138
|
+
};
|
|
139
|
+
} | {
|
|
140
|
+
success: true;
|
|
141
|
+
data: {
|
|
142
|
+
showValues?: boolean | undefined;
|
|
143
|
+
title: string;
|
|
144
|
+
labelField: string;
|
|
145
|
+
valueField: string;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
Check: (value: unknown) => boolean;
|
|
149
|
+
};
|
|
63
150
|
readonly getReferencedFields: typeof barChartFields;
|
|
64
151
|
readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
|
|
65
152
|
}, {
|
|
66
153
|
readonly type: "line-chart";
|
|
67
154
|
readonly dataKind: "items";
|
|
68
|
-
readonly propsSchema: import("
|
|
69
|
-
title: import("
|
|
70
|
-
labelField: import("
|
|
71
|
-
valueField: import("
|
|
72
|
-
showDots: import("
|
|
73
|
-
fill: import("
|
|
74
|
-
}
|
|
155
|
+
readonly propsSchema: import("@sinclair/typebox").TObject<{
|
|
156
|
+
title: import("@sinclair/typebox").TString;
|
|
157
|
+
labelField: import("@sinclair/typebox").TString;
|
|
158
|
+
valueField: import("@sinclair/typebox").TString;
|
|
159
|
+
showDots: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
160
|
+
fill: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
161
|
+
}> & {
|
|
162
|
+
parse: (value: unknown) => {
|
|
163
|
+
fill?: boolean | undefined;
|
|
164
|
+
showDots?: boolean | undefined;
|
|
165
|
+
title: string;
|
|
166
|
+
labelField: string;
|
|
167
|
+
valueField: string;
|
|
168
|
+
};
|
|
169
|
+
safeParse: (value: unknown) => {
|
|
170
|
+
success: false;
|
|
171
|
+
error: {
|
|
172
|
+
issues: Array<{
|
|
173
|
+
path: string[];
|
|
174
|
+
message: string;
|
|
175
|
+
}>;
|
|
176
|
+
};
|
|
177
|
+
} | {
|
|
178
|
+
success: true;
|
|
179
|
+
data: {
|
|
180
|
+
fill?: boolean | undefined;
|
|
181
|
+
showDots?: boolean | undefined;
|
|
182
|
+
title: string;
|
|
183
|
+
labelField: string;
|
|
184
|
+
valueField: string;
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
Check: (value: unknown) => boolean;
|
|
188
|
+
};
|
|
75
189
|
readonly getReferencedFields: typeof lineChartFields;
|
|
76
190
|
readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
|
|
77
191
|
}];
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DataProvider } from '@svadmin/core';
|
|
2
|
-
import type {
|
|
2
|
+
import type { TSchema } from '@sinclair/typebox';
|
|
3
3
|
export declare const SURFACE_SCHEMA_VERSION: "surface/v1";
|
|
4
4
|
export declare const SURFACE_LIMITS: {
|
|
5
5
|
readonly maxDataSources: 8;
|
|
@@ -92,7 +92,12 @@ export type SurfaceCatalogDataKind = 'none' | 'scalar' | 'items';
|
|
|
92
92
|
export interface SurfaceWidgetDefinition {
|
|
93
93
|
readonly type: string;
|
|
94
94
|
readonly dataKind: SurfaceCatalogDataKind;
|
|
95
|
-
readonly propsSchema:
|
|
95
|
+
readonly propsSchema: TSchema | {
|
|
96
|
+
Check?: (data: unknown) => boolean;
|
|
97
|
+
safeParse?: (data: unknown) => {
|
|
98
|
+
success: boolean;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
96
101
|
/** 组件从绑定记录中读取的字段,由可信 Catalog 提供。 */
|
|
97
102
|
readonly getReferencedFields?: (props: JsonObject) => readonly string[];
|
|
98
103
|
}
|
package/dist/validation.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { type SurfaceCatalog, type SurfacePolicy, type SurfaceValidationResult } from
|
|
1
|
+
import { type SurfaceCatalog, type SurfacePolicy, type SurfaceValidationResult } from "./types.js";
|
|
2
2
|
export declare function validateSurfaceSpec(input: unknown, catalog: SurfaceCatalog, policy: SurfacePolicy): SurfaceValidationResult;
|
package/dist/validation.js
CHANGED
|
@@ -1,75 +1,126 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
3
|
+
import { Value } from "@sinclair/typebox/value";
|
|
4
|
+
import { decodedJsonPointerToken, jsonPointer, jsonValueIssue } from "./json.js";
|
|
5
|
+
import { SURFACE_LIMITS, SURFACE_SCHEMA_VERSION, } from "./types.js";
|
|
6
|
+
const idSchema = Type.String({
|
|
7
|
+
minLength: 1,
|
|
8
|
+
maxLength: SURFACE_LIMITS.maxIdLength,
|
|
9
|
+
pattern: "^[A-Za-z][A-Za-z0-9_-]*$",
|
|
10
|
+
});
|
|
11
|
+
const fieldSchema = Type.String({
|
|
12
|
+
minLength: 1,
|
|
13
|
+
maxLength: SURFACE_LIMITS.maxIdLength,
|
|
14
|
+
});
|
|
15
|
+
const jsonPrimitiveSchema = Type.Union([
|
|
16
|
+
Type.String(),
|
|
17
|
+
Type.Number(),
|
|
18
|
+
Type.Boolean(),
|
|
19
|
+
Type.Null(),
|
|
20
|
+
]);
|
|
21
|
+
const surfaceFilterSchema = Type.Union([
|
|
22
|
+
Type.Object({
|
|
9
23
|
field: fieldSchema,
|
|
10
|
-
operator:
|
|
24
|
+
operator: Type.Union([
|
|
25
|
+
Type.Literal("eq"),
|
|
26
|
+
Type.Literal("ne"),
|
|
27
|
+
Type.Literal("lt"),
|
|
28
|
+
Type.Literal("lte"),
|
|
29
|
+
Type.Literal("gt"),
|
|
30
|
+
Type.Literal("gte"),
|
|
31
|
+
Type.Literal("contains"),
|
|
32
|
+
Type.Literal("startswith"),
|
|
33
|
+
Type.Literal("endswith"),
|
|
34
|
+
]),
|
|
11
35
|
value: jsonPrimitiveSchema,
|
|
12
|
-
})
|
|
13
|
-
|
|
36
|
+
}, { additionalProperties: false }),
|
|
37
|
+
Type.Object({
|
|
14
38
|
field: fieldSchema,
|
|
15
|
-
operator:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
operator: Type.Union([
|
|
40
|
+
Type.Literal("in"),
|
|
41
|
+
Type.Literal("nin"),
|
|
42
|
+
]),
|
|
43
|
+
value: Type.Array(jsonPrimitiveSchema),
|
|
44
|
+
}, { additionalProperties: false }),
|
|
45
|
+
Type.Object({
|
|
19
46
|
field: fieldSchema,
|
|
20
|
-
operator:
|
|
21
|
-
|
|
47
|
+
operator: Type.Union([
|
|
48
|
+
Type.Literal("null"),
|
|
49
|
+
Type.Literal("nnull"),
|
|
50
|
+
]),
|
|
51
|
+
}, { additionalProperties: false }),
|
|
22
52
|
]);
|
|
23
|
-
const resourceListSchema =
|
|
53
|
+
const resourceListSchema = Type.Object({
|
|
24
54
|
id: idSchema,
|
|
25
|
-
type:
|
|
55
|
+
type: Type.Literal("resource-list"),
|
|
26
56
|
resource: idSchema,
|
|
27
|
-
pageSize:
|
|
28
|
-
sorters:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
57
|
+
pageSize: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
58
|
+
sorters: Type.Optional(Type.Array(Type.Object({
|
|
59
|
+
field: fieldSchema,
|
|
60
|
+
order: Type.Union([Type.Literal("asc"), Type.Literal("desc")]),
|
|
61
|
+
}, { additionalProperties: false }))),
|
|
62
|
+
filters: Type.Optional(Type.Array(surfaceFilterSchema)),
|
|
63
|
+
}, { additionalProperties: false });
|
|
64
|
+
const resourceOneSchema = Type.Object({
|
|
32
65
|
id: idSchema,
|
|
33
|
-
type:
|
|
66
|
+
type: Type.Literal("resource-one"),
|
|
34
67
|
resource: idSchema,
|
|
35
|
-
recordId:
|
|
36
|
-
})
|
|
37
|
-
const surfaceSpecSchema =
|
|
38
|
-
schemaVersion:
|
|
39
|
-
catalogVersion:
|
|
68
|
+
recordId: Type.Union([Type.String(), Type.Number()]),
|
|
69
|
+
}, { additionalProperties: false });
|
|
70
|
+
const surfaceSpecSchema = Type.Object({
|
|
71
|
+
schemaVersion: Type.String(),
|
|
72
|
+
catalogVersion: Type.String({ minLength: 1 }),
|
|
40
73
|
surfaceId: idSchema,
|
|
41
|
-
title:
|
|
42
|
-
layout:
|
|
43
|
-
type:
|
|
44
|
-
columns:
|
|
45
|
-
gap:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
title: Type.String({ minLength: 1, maxLength: SURFACE_LIMITS.maxTitleLength }),
|
|
75
|
+
layout: Type.Object({
|
|
76
|
+
type: Type.Literal("grid"),
|
|
77
|
+
columns: Type.Literal(12),
|
|
78
|
+
gap: Type.Optional(Type.Union([
|
|
79
|
+
Type.Literal("sm"),
|
|
80
|
+
Type.Literal("md"),
|
|
81
|
+
Type.Literal("lg"),
|
|
82
|
+
])),
|
|
83
|
+
}, { additionalProperties: false }),
|
|
84
|
+
dataSources: Type.Array(Type.Union([resourceListSchema, resourceOneSchema])),
|
|
85
|
+
widgets: Type.Array(Type.Object({
|
|
49
86
|
id: idSchema,
|
|
50
87
|
type: idSchema,
|
|
51
|
-
props:
|
|
52
|
-
binding:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
})
|
|
88
|
+
props: Type.Record(Type.String(), Type.Unknown()),
|
|
89
|
+
binding: Type.Optional(Type.Object({
|
|
90
|
+
sourceId: idSchema,
|
|
91
|
+
pointer: Type.Optional(Type.String()),
|
|
92
|
+
}, { additionalProperties: false })),
|
|
93
|
+
placement: Type.Optional(Type.Object({
|
|
94
|
+
columnSpan: Type.Optional(Type.Integer({ minimum: 1, maximum: 12 })),
|
|
95
|
+
}, { additionalProperties: false })),
|
|
96
|
+
}, { additionalProperties: false })),
|
|
97
|
+
}, { additionalProperties: false });
|
|
98
|
+
const compiledSurfaceSpec = TypeCompiler.Compile(surfaceSpecSchema);
|
|
56
99
|
const forbiddenPropertyNames = new Set([
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
100
|
+
"class",
|
|
101
|
+
"className",
|
|
102
|
+
"color",
|
|
103
|
+
"href",
|
|
104
|
+
"html",
|
|
105
|
+
"innerHTML",
|
|
106
|
+
"src",
|
|
107
|
+
"style",
|
|
108
|
+
"url",
|
|
66
109
|
]);
|
|
67
|
-
const strictSchemaProbeKey =
|
|
110
|
+
const strictSchemaProbeKey = "__surface_unknown_property_probe__";
|
|
68
111
|
function invalidJsonIssue(pathSegments, message) {
|
|
69
|
-
return { code:
|
|
112
|
+
return { code: "invalid_json", path: jsonPointer(pathSegments), message };
|
|
70
113
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
114
|
+
function schemaErrors(errors) {
|
|
115
|
+
const issues = [];
|
|
116
|
+
for (const err of errors) {
|
|
117
|
+
issues.push({
|
|
118
|
+
code: "invalid_json",
|
|
119
|
+
path: err.path || "",
|
|
120
|
+
message: err.message,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return issues;
|
|
73
124
|
}
|
|
74
125
|
function catalogDefinitions(catalog) {
|
|
75
126
|
return new Map(catalog.widgets.map((widgetDefinition) => [widgetDefinition.type, widgetDefinition]));
|
|
@@ -81,25 +132,25 @@ function sourcePolicyIssue(source, policy, index) {
|
|
|
81
132
|
const resourcePolicy = resourcePolicyFor(policy, source.resource);
|
|
82
133
|
if (!resourcePolicy) {
|
|
83
134
|
return {
|
|
84
|
-
code:
|
|
135
|
+
code: "resource_denied",
|
|
85
136
|
path: `/dataSources/${index}/resource`,
|
|
86
137
|
message: `Resource "${source.resource}" is not allowed`,
|
|
87
138
|
sourceId: source.id,
|
|
88
139
|
};
|
|
89
140
|
}
|
|
90
|
-
if (source.type ===
|
|
141
|
+
if (source.type === "resource-one" && resourcePolicy.allowGetOne !== true) {
|
|
91
142
|
return {
|
|
92
|
-
code:
|
|
143
|
+
code: "resource_denied",
|
|
93
144
|
path: `/dataSources/${index}`,
|
|
94
145
|
message: `Resource "${source.resource}" does not allow getOne`,
|
|
95
146
|
sourceId: source.id,
|
|
96
147
|
};
|
|
97
148
|
}
|
|
98
|
-
if (source.type ===
|
|
149
|
+
if (source.type === "resource-list") {
|
|
99
150
|
const pageSizeLimit = Math.min(resourcePolicy.maxPageSize ?? SURFACE_LIMITS.maxPageSize, SURFACE_LIMITS.maxPageSize);
|
|
100
151
|
if ((source.pageSize ?? 10) > pageSizeLimit) {
|
|
101
152
|
return {
|
|
102
|
-
code:
|
|
153
|
+
code: "limit_exceeded",
|
|
103
154
|
path: `/dataSources/${index}/pageSize`,
|
|
104
155
|
message: `Page size exceeds ${pageSizeLimit}`,
|
|
105
156
|
sourceId: source.id,
|
|
@@ -108,7 +159,7 @@ function sourcePolicyIssue(source, policy, index) {
|
|
|
108
159
|
const disallowedSorter = source.sorters?.find((sorter) => !resourcePolicy.sortFields?.includes(sorter.field));
|
|
109
160
|
if (disallowedSorter) {
|
|
110
161
|
return {
|
|
111
|
-
code:
|
|
162
|
+
code: "field_denied",
|
|
112
163
|
path: `/dataSources/${index}/sorters`,
|
|
113
164
|
message: `Sorting by "${disallowedSorter.field}" is not allowed`,
|
|
114
165
|
sourceId: source.id,
|
|
@@ -117,7 +168,7 @@ function sourcePolicyIssue(source, policy, index) {
|
|
|
117
168
|
const disallowedFilter = source.filters?.find((filter) => !resourcePolicy.filterFields?.includes(filter.field));
|
|
118
169
|
if (disallowedFilter) {
|
|
119
170
|
return {
|
|
120
|
-
code:
|
|
171
|
+
code: "field_denied",
|
|
121
172
|
path: `/dataSources/${index}/filters`,
|
|
122
173
|
message: `Filtering by "${disallowedFilter.field}" is not allowed`,
|
|
123
174
|
sourceId: source.id,
|
|
@@ -130,14 +181,14 @@ function propsContainForbiddenKey(props) {
|
|
|
130
181
|
const pending = [props];
|
|
131
182
|
while (pending.length > 0) {
|
|
132
183
|
const current = pending.pop();
|
|
133
|
-
if (current === undefined || current === null || typeof current !==
|
|
184
|
+
if (current === undefined || current === null || typeof current !== "object")
|
|
134
185
|
continue;
|
|
135
186
|
if (Array.isArray(current)) {
|
|
136
187
|
pending.push(...current);
|
|
137
188
|
continue;
|
|
138
189
|
}
|
|
139
190
|
for (const [key, entry] of Object.entries(current)) {
|
|
140
|
-
if (forbiddenPropertyNames.has(key) || key.startsWith(
|
|
191
|
+
if (forbiddenPropertyNames.has(key) || key.startsWith("on"))
|
|
141
192
|
return key;
|
|
142
193
|
pending.push(entry);
|
|
143
194
|
}
|
|
@@ -153,18 +204,18 @@ function referencedFieldIssue(widget, widgetIndex, definition, source, policy) {
|
|
|
153
204
|
}
|
|
154
205
|
catch {
|
|
155
206
|
return {
|
|
156
|
-
code:
|
|
207
|
+
code: "invalid_widget_props",
|
|
157
208
|
path: `/widgets/${widgetIndex}/props`,
|
|
158
|
-
message:
|
|
209
|
+
message: "Widget field references could not be resolved",
|
|
159
210
|
widgetId: widget.id,
|
|
160
211
|
};
|
|
161
212
|
}
|
|
162
213
|
const readableFields = resourcePolicyFor(policy, source.resource)?.readFields ?? [];
|
|
163
|
-
const deniedField = referencedFields.find((field) => (typeof field !==
|
|
214
|
+
const deniedField = referencedFields.find((field) => (typeof field !== "string" || !readableFields.includes(field)));
|
|
164
215
|
return deniedField === undefined
|
|
165
216
|
? null
|
|
166
217
|
: {
|
|
167
|
-
code:
|
|
218
|
+
code: "field_denied",
|
|
168
219
|
path: `/widgets/${widgetIndex}/props`,
|
|
169
220
|
message: `Field "${String(deniedField)}" is not readable`,
|
|
170
221
|
widgetId: widget.id,
|
|
@@ -172,44 +223,55 @@ function referencedFieldIssue(widget, widgetIndex, definition, source, policy) {
|
|
|
172
223
|
};
|
|
173
224
|
}
|
|
174
225
|
function resourceOnePointerField(pointer) {
|
|
175
|
-
if (!pointer.startsWith(
|
|
226
|
+
if (!pointer.startsWith("/") || pointer.slice(1).includes("/"))
|
|
176
227
|
return null;
|
|
177
228
|
const field = decodedJsonPointerToken(pointer.slice(1));
|
|
178
229
|
return field && field.length <= SURFACE_LIMITS.maxIdLength ? field : null;
|
|
179
230
|
}
|
|
231
|
+
function checkPropsSchema(schema, data) {
|
|
232
|
+
if (!schema || typeof schema !== "object")
|
|
233
|
+
return false;
|
|
234
|
+
if ("Check" in schema && typeof schema.Check === "function") {
|
|
235
|
+
return schema.Check(data);
|
|
236
|
+
}
|
|
237
|
+
if ("safeParse" in schema && typeof schema.safeParse === "function") {
|
|
238
|
+
return schema.safeParse(data).success;
|
|
239
|
+
}
|
|
240
|
+
return Value.Check(schema, data);
|
|
241
|
+
}
|
|
180
242
|
function widgetIssue(widget, widgetIndex, definition, sources, policy) {
|
|
181
243
|
if (!definition) {
|
|
182
244
|
return {
|
|
183
|
-
code:
|
|
245
|
+
code: "unknown_widget_type",
|
|
184
246
|
path: `/widgets/${widgetIndex}/type`,
|
|
185
247
|
message: `Widget type "${widget.type}" is not registered`,
|
|
186
248
|
widgetId: widget.id,
|
|
187
249
|
};
|
|
188
250
|
}
|
|
189
251
|
const forbiddenKey = propsContainForbiddenKey(widget.props);
|
|
190
|
-
const
|
|
191
|
-
const strictSchemaProbe = definition.propsSchema
|
|
252
|
+
const propsValid = checkPropsSchema(definition.propsSchema, widget.props);
|
|
253
|
+
const strictSchemaProbe = checkPropsSchema(definition.propsSchema, {
|
|
192
254
|
...widget.props,
|
|
193
255
|
[strictSchemaProbeKey]: null,
|
|
194
256
|
});
|
|
195
|
-
if (forbiddenKey || !
|
|
257
|
+
if (forbiddenKey || !propsValid || strictSchemaProbe) {
|
|
196
258
|
return {
|
|
197
|
-
code:
|
|
259
|
+
code: "invalid_widget_props",
|
|
198
260
|
path: `/widgets/${widgetIndex}/props`,
|
|
199
261
|
message: forbiddenKey
|
|
200
262
|
? `Property "${forbiddenKey}" is not allowed`
|
|
201
|
-
: strictSchemaProbe
|
|
202
|
-
?
|
|
203
|
-
:
|
|
263
|
+
: strictSchemaProbe
|
|
264
|
+
? "Catalog props schema must reject unknown properties"
|
|
265
|
+
: "Widget props do not match the catalog schema",
|
|
204
266
|
widgetId: widget.id,
|
|
205
267
|
};
|
|
206
268
|
}
|
|
207
|
-
if (definition.dataKind ===
|
|
269
|
+
if (definition.dataKind === "none") {
|
|
208
270
|
return widget.binding
|
|
209
271
|
? {
|
|
210
|
-
code:
|
|
272
|
+
code: "invalid_binding_pointer",
|
|
211
273
|
path: `/widgets/${widgetIndex}/binding`,
|
|
212
|
-
message:
|
|
274
|
+
message: "This widget does not accept a data binding",
|
|
213
275
|
widgetId: widget.id,
|
|
214
276
|
}
|
|
215
277
|
: null;
|
|
@@ -217,23 +279,23 @@ function widgetIssue(widget, widgetIndex, definition, sources, policy) {
|
|
|
217
279
|
const source = widget.binding ? sources.get(widget.binding.sourceId) : undefined;
|
|
218
280
|
if (!source) {
|
|
219
281
|
return {
|
|
220
|
-
code:
|
|
282
|
+
code: "unknown_data_source",
|
|
221
283
|
path: `/widgets/${widgetIndex}/binding/sourceId`,
|
|
222
|
-
message:
|
|
284
|
+
message: "Widget binding must reference an existing data source",
|
|
223
285
|
widgetId: widget.id,
|
|
224
286
|
};
|
|
225
287
|
}
|
|
226
|
-
const pointer = widget.binding?.pointer ??
|
|
227
|
-
const resourceOneField = source.type ===
|
|
228
|
-
const pointerIsValid = definition.dataKind ===
|
|
229
|
-
? source.type ===
|
|
230
|
-
: (source.type ===
|
|
288
|
+
const pointer = widget.binding?.pointer ?? "";
|
|
289
|
+
const resourceOneField = source.type === "resource-one" ? resourceOnePointerField(pointer) : null;
|
|
290
|
+
const pointerIsValid = definition.dataKind === "items"
|
|
291
|
+
? source.type === "resource-list" && pointer === "/items"
|
|
292
|
+
: (source.type === "resource-list" && pointer === "/total")
|
|
231
293
|
|| resourceOneField !== null;
|
|
232
294
|
if (!pointerIsValid) {
|
|
233
295
|
return {
|
|
234
|
-
code:
|
|
296
|
+
code: "invalid_binding_pointer",
|
|
235
297
|
path: `/widgets/${widgetIndex}/binding/pointer`,
|
|
236
|
-
message:
|
|
298
|
+
message: "Binding pointer is not valid for this widget and data source",
|
|
237
299
|
widgetId: widget.id,
|
|
238
300
|
sourceId: source.id,
|
|
239
301
|
};
|
|
@@ -241,10 +303,10 @@ function widgetIssue(widget, widgetIndex, definition, sources, policy) {
|
|
|
241
303
|
const deniedReferencedField = referencedFieldIssue(widget, widgetIndex, definition, source, policy);
|
|
242
304
|
if (deniedReferencedField)
|
|
243
305
|
return deniedReferencedField;
|
|
244
|
-
if (source.type ===
|
|
306
|
+
if (source.type === "resource-one" && resourceOneField !== null) {
|
|
245
307
|
if (!resourcePolicyFor(policy, source.resource)?.readFields.includes(resourceOneField)) {
|
|
246
308
|
return {
|
|
247
|
-
code:
|
|
309
|
+
code: "field_denied",
|
|
248
310
|
path: `/widgets/${widgetIndex}/binding/pointer`,
|
|
249
311
|
message: `Field "${resourceOneField}" is not readable`,
|
|
250
312
|
widgetId: widget.id,
|
|
@@ -262,7 +324,7 @@ function duplicateIdIssue(spec) {
|
|
|
262
324
|
];
|
|
263
325
|
for (const entry of entries) {
|
|
264
326
|
if (ids.has(entry.id))
|
|
265
|
-
return { code:
|
|
327
|
+
return { code: "duplicate_id", path: entry.path, message: `Duplicate id "${entry.id}"` };
|
|
266
328
|
ids.add(entry.id);
|
|
267
329
|
}
|
|
268
330
|
return null;
|
|
@@ -271,18 +333,18 @@ export function validateSurfaceSpec(input, catalog, policy) {
|
|
|
271
333
|
const unsafeJson = jsonValueIssue(input);
|
|
272
334
|
if (unsafeJson)
|
|
273
335
|
return { ok: false, issues: [invalidJsonIssue(unsafeJson.path, unsafeJson.message)] };
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const spec =
|
|
336
|
+
if (!compiledSurfaceSpec.Check(input)) {
|
|
337
|
+
return { ok: false, issues: schemaErrors(compiledSurfaceSpec.Errors(input)) };
|
|
338
|
+
}
|
|
339
|
+
const spec = input;
|
|
278
340
|
if (spec.schemaVersion !== SURFACE_SCHEMA_VERSION) {
|
|
279
|
-
return { ok: false, issues: [{ code:
|
|
341
|
+
return { ok: false, issues: [{ code: "unsupported_schema_version", path: "/schemaVersion", message: `Expected ${SURFACE_SCHEMA_VERSION}` }] };
|
|
280
342
|
}
|
|
281
343
|
if (spec.catalogVersion !== catalog.version) {
|
|
282
|
-
return { ok: false, issues: [{ code:
|
|
344
|
+
return { ok: false, issues: [{ code: "catalog_version_mismatch", path: "/catalogVersion", message: `Expected ${catalog.version}` }] };
|
|
283
345
|
}
|
|
284
346
|
if (spec.dataSources.length > SURFACE_LIMITS.maxDataSources || spec.widgets.length > SURFACE_LIMITS.maxWidgets) {
|
|
285
|
-
return { ok: false, issues: [{ code:
|
|
347
|
+
return { ok: false, issues: [{ code: "limit_exceeded", path: "", message: "Surface exceeds the source or widget limit" }] };
|
|
286
348
|
}
|
|
287
349
|
const duplicateIssue = duplicateIdIssue(spec);
|
|
288
350
|
if (duplicateIssue)
|
|
@@ -291,9 +353,9 @@ export function validateSurfaceSpec(input, catalog, policy) {
|
|
|
291
353
|
const issue = sourcePolicyIssue(source, policy, sourceIndex);
|
|
292
354
|
if (issue)
|
|
293
355
|
return { ok: false, issues: [issue] };
|
|
294
|
-
if ((source.type ===
|
|
295
|
-
|| (source.type ===
|
|
296
|
-
return { ok: false, issues: [{ code:
|
|
356
|
+
if ((source.type === "resource-list" && (source.filters?.length ?? 0) > SURFACE_LIMITS.maxFilters)
|
|
357
|
+
|| (source.type === "resource-list" && (source.sorters?.length ?? 0) > SURFACE_LIMITS.maxSorters)) {
|
|
358
|
+
return { ok: false, issues: [{ code: "limit_exceeded", path: `/dataSources/${sourceIndex}`, message: "Source exceeds the filter or sorter limit", sourceId: source.id }] };
|
|
297
359
|
}
|
|
298
360
|
}
|
|
299
361
|
const sources = new Map(spec.dataSources.map((source) => [source.id, source]));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/surface",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Validated declarative surfaces for svadmin dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -25,20 +25,20 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@svadmin/core": ">=0.34.2 <0.
|
|
29
|
-
"@svadmin/ui": ">=0.40.6 <0.
|
|
28
|
+
"@svadmin/core": ">=0.34.2 <0.46.0",
|
|
29
|
+
"@svadmin/ui": ">=0.40.6 <0.64.0",
|
|
30
30
|
"svelte": "^5.56.8"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"
|
|
33
|
+
"@sinclair/typebox": "^0.34.52"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
37
37
|
"@testing-library/dom": "^10.4.1",
|
|
38
38
|
"@testing-library/svelte": "^5.4.2",
|
|
39
|
-
"@testing-library/user-event": "^14.6.
|
|
40
|
-
"happy-dom": "^20.11.
|
|
41
|
-
"vitest": "^4.1.
|
|
39
|
+
"@testing-library/user-event": "^14.6.6",
|
|
40
|
+
"happy-dom": "^20.11.12",
|
|
41
|
+
"vitest": "^4.1.11"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "node ../../node_modules/@sveltejs/package/svelte-package.js -i src -o dist",
|