@svadmin/surface 0.1.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.
@@ -0,0 +1,100 @@
1
+ <script lang="ts">
2
+ import CardContent from '@svadmin/ui/components/ui/card/card-content.svelte';
3
+ import CardHeader from '@svadmin/ui/components/ui/card/card-header.svelte';
4
+ import CardTitle from '@svadmin/ui/components/ui/card/card-title.svelte';
5
+ import Card from '@svadmin/ui/components/ui/card/card.svelte';
6
+ import TableBody from '@svadmin/ui/components/ui/table/table-body.svelte';
7
+ import TableCell from '@svadmin/ui/components/ui/table/table-cell.svelte';
8
+ import TableHead from '@svadmin/ui/components/ui/table/table-head.svelte';
9
+ import TableHeader from '@svadmin/ui/components/ui/table/table-header.svelte';
10
+ import TableRow from '@svadmin/ui/components/ui/table/table-row.svelte';
11
+ import Table from '@svadmin/ui/components/ui/table/table.svelte';
12
+ import { resourceTablePropsSchema } from '../builtin-schemas.js';
13
+ import type { SurfaceWidgetRendererProps } from '../catalog.js';
14
+ import { asRecordArray, displayTableValue } from '../widget-data.js';
15
+
16
+ let { props, data }: SurfaceWidgetRendererProps = $props();
17
+
18
+ const tableProps = $derived(resourceTablePropsSchema.parse(props));
19
+ const records = $derived(data.status === 'ready' ? asRecordArray(data.value) : null);
20
+ </script>
21
+
22
+ <Card>
23
+ <CardHeader>
24
+ <CardTitle>{tableProps.title}</CardTitle>
25
+ </CardHeader>
26
+ <CardContent>
27
+ {#if data.status === 'loading'}
28
+ <div class="table-state table-loading" role="status">Loading table</div>
29
+ {:else if data.status === 'empty'}
30
+ <p class="table-state" role="status">{tableProps.emptyLabel ?? 'No records'}</p>
31
+ {:else if data.status === 'error'}
32
+ <p class="table-state" role="alert">{data.error.message}</p>
33
+ {:else if data.status === 'ready' && records?.ok}
34
+ <div class="surface-table">
35
+ <Table aria-label={tableProps.title}>
36
+ <TableHeader>
37
+ <TableRow>
38
+ {#each tableProps.columns as column (column.field)}
39
+ <TableHead scope="col">{column.label}</TableHead>
40
+ {/each}
41
+ </TableRow>
42
+ </TableHeader>
43
+ <TableBody>
44
+ {#each records.value as record, index (index)}
45
+ <TableRow>
46
+ {#each tableProps.columns as column (column.field)}
47
+ <TableCell>{displayTableValue(record[column.field], column.format)}</TableCell>
48
+ {/each}
49
+ </TableRow>
50
+ {/each}
51
+ </TableBody>
52
+ </Table>
53
+ </div>
54
+ {:else}
55
+ <p class="table-state" role="alert">{records && !records.ok ? records.message : 'Table data is unavailable'}</p>
56
+ {/if}
57
+ </CardContent>
58
+ </Card>
59
+
60
+ <style>
61
+ .table-state {
62
+ display: grid;
63
+ min-height: 8rem;
64
+ margin: 0;
65
+ place-items: center;
66
+ color: var(--muted-foreground);
67
+ }
68
+
69
+ .table-loading {
70
+ border-radius: 0.5rem;
71
+ background: linear-gradient(90deg, var(--muted), var(--card), var(--muted));
72
+ background-size: 200% 100%;
73
+ animation: surface-pulse 1.5s ease-in-out infinite;
74
+ }
75
+
76
+ @keyframes surface-pulse {
77
+ to { background-position: -200% 0; }
78
+ }
79
+
80
+ @media (prefers-reduced-motion: reduce) {
81
+ .table-loading { animation: none; }
82
+ }
83
+
84
+ @media (max-width: 47.999rem) {
85
+ .surface-table :global([data-slot='table-container']) {
86
+ overflow-x: visible;
87
+ }
88
+
89
+ .surface-table :global([data-slot='table']) {
90
+ table-layout: fixed;
91
+ }
92
+
93
+ .surface-table :global([data-slot='table-head']),
94
+ .surface-table :global([data-slot='table-cell']) {
95
+ padding-inline: 0.375rem;
96
+ overflow-wrap: anywhere;
97
+ white-space: normal;
98
+ }
99
+ }
100
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { SurfaceWidgetRendererProps } from '../catalog.js';
2
+ declare const ResourceTableWidget: import("svelte").Component<SurfaceWidgetRendererProps, {}, "">;
3
+ type ResourceTableWidget = ReturnType<typeof ResourceTableWidget>;
4
+ export default ResourceTableWidget;
@@ -0,0 +1,231 @@
1
+ <script lang="ts">
2
+ import { canAccessAsync, captureAdminContext } from '@svadmin/core';
3
+ import { untrack } from 'svelte';
4
+ import { resolveSurfaceWidgetData } from '../binding.js';
5
+ import { defaultSurfaceCatalog } from '../catalog.js';
6
+ import type { SurfaceRenderCatalog } from '../catalog.js';
7
+ import { loadSurfaceSource } from '../runtime.js';
8
+ import type {
9
+ SurfaceDataError,
10
+ SurfaceDataProvider,
11
+ SurfaceDataSource,
12
+ SurfacePolicy,
13
+ SurfaceSourceDataState,
14
+ SurfaceValidationIssue,
15
+ } from '../types.js';
16
+ import { validateSurfaceSpec } from '../validation.js';
17
+
18
+ export type SurfaceRendererError =
19
+ | { readonly type: 'validation'; readonly issues: readonly SurfaceValidationIssue[] }
20
+ | { readonly type: 'data'; readonly error: SurfaceDataError };
21
+
22
+ export interface SurfaceRendererProps {
23
+ readonly spec: unknown;
24
+ readonly policy: SurfacePolicy;
25
+ readonly catalog?: SurfaceRenderCatalog;
26
+ readonly dataProvider?: SurfaceDataProvider;
27
+ readonly class?: string;
28
+ readonly onError?: (error: SurfaceRendererError) => void;
29
+ }
30
+
31
+ let {
32
+ spec,
33
+ policy,
34
+ catalog = defaultSurfaceCatalog,
35
+ dataProvider,
36
+ class: className = '',
37
+ onError,
38
+ }: SurfaceRendererProps = $props();
39
+
40
+ const adminContext = captureAdminContext();
41
+ const validation = $derived(validateSurfaceSpec(spec, catalog, policy));
42
+ const widgetRegistrations = $derived(new Map(catalog.widgets.map((widget) => [widget.type, widget])));
43
+ const currentSpec = $derived(validation.ok ? validation.value : null);
44
+ const sourceGenerations: Record<string, number> = Object.create(null) as Record<string, number>;
45
+ let sourceStates = $state.raw<Record<string, SurfaceSourceDataState>>({});
46
+
47
+ function nextGeneration(sourceId: string): number {
48
+ const generation = (sourceGenerations[sourceId] ?? 0) + 1;
49
+ sourceGenerations[sourceId] = generation;
50
+ return generation;
51
+ }
52
+
53
+ function setSourceState(sourceId: string, state: SurfaceSourceDataState): void {
54
+ sourceStates = { ...sourceStates, [sourceId]: state };
55
+ }
56
+
57
+ function providerFor(resource: string): SurfaceDataProvider {
58
+ return dataProvider ?? adminContext.getDataProviderForResource(resource);
59
+ }
60
+
61
+ async function authorize(resource: string, action: 'list' | 'show') {
62
+ return canAccessAsync(resource, action);
63
+ }
64
+
65
+ function providerError(sourceId: string, failure: unknown): SurfaceSourceDataState {
66
+ const message = failure instanceof Error ? failure.message : 'Data provider is unavailable';
67
+ return {
68
+ status: 'error',
69
+ sourceId,
70
+ error: { code: 'provider_failed', sourceId, message },
71
+ };
72
+ }
73
+
74
+ async function loadSource(source: SurfaceDataSource): Promise<void> {
75
+ const generation = nextGeneration(source.id);
76
+ setSourceState(source.id, { status: 'loading', sourceId: source.id });
77
+
78
+ let result: SurfaceSourceDataState;
79
+ try {
80
+ const resourcePolicy = Object.hasOwn(policy.resources, source.resource)
81
+ ? policy.resources[source.resource]
82
+ : undefined;
83
+ if (!resourcePolicy) throw new Error(`Resource "${source.resource}" is not allowed`);
84
+ result = await loadSurfaceSource({
85
+ source,
86
+ resourcePolicy,
87
+ provider: providerFor(source.resource),
88
+ authorize,
89
+ });
90
+ } catch (failure) {
91
+ result = providerError(source.id, failure);
92
+ }
93
+
94
+ if (sourceGenerations[source.id] !== generation) return;
95
+ setSourceState(source.id, result);
96
+ if (result.status === 'error') onError?.({ type: 'data', error: result.error });
97
+ }
98
+
99
+ async function loadSources(sources: readonly SurfaceDataSource[]): Promise<void> {
100
+ await Promise.all(sources.map(loadSource));
101
+ }
102
+
103
+ function invalidateCurrentSources(): void {
104
+ for (const sourceId of Object.keys(sourceGenerations)) nextGeneration(sourceId);
105
+ sourceStates = {};
106
+ }
107
+
108
+ export async function refresh(sourceId?: string): Promise<void> {
109
+ const activeSpec = currentSpec;
110
+ if (!activeSpec) return;
111
+ const sources = sourceId === undefined
112
+ ? activeSpec.dataSources
113
+ : activeSpec.dataSources.filter((source) => source.id === sourceId);
114
+ await loadSources(sources);
115
+ }
116
+
117
+ $effect(() => {
118
+ const validatedSpec = validation;
119
+ void dataProvider;
120
+ invalidateCurrentSources();
121
+ if (!validatedSpec.ok) {
122
+ onError?.({ type: 'validation', issues: validatedSpec.issues });
123
+ return;
124
+ }
125
+
126
+ untrack(() => {
127
+ void loadSources(validatedSpec.value.dataSources);
128
+ });
129
+ });
130
+ </script>
131
+
132
+ {#if validation.ok}
133
+ <section
134
+ class="surface {className}"
135
+ aria-labelledby="surface-{validation.value.surfaceId}-title"
136
+ data-surface-id={validation.value.surfaceId}
137
+ >
138
+ <header class="surface-header">
139
+ <h2 id="surface-{validation.value.surfaceId}-title">{validation.value.title}</h2>
140
+ </header>
141
+ <div class="surface-grid surface-gap-{validation.value.layout.gap ?? 'md'}">
142
+ {#each validation.value.widgets as widget (widget.id)}
143
+ {@const registration = widgetRegistrations.get(widget.type)}
144
+ {@const WidgetComponent = registration?.component}
145
+ <div
146
+ class="surface-widget surface-span-{widget.placement?.columnSpan ?? 12}"
147
+ data-testid="surface-widget-{widget.id}"
148
+ >
149
+ {#if WidgetComponent}
150
+ <WidgetComponent
151
+ widgetId={widget.id}
152
+ props={widget.props}
153
+ data={resolveSurfaceWidgetData(widget, sourceStates)}
154
+ />
155
+ {/if}
156
+ </div>
157
+ {/each}
158
+ </div>
159
+ </section>
160
+ {:else}
161
+ <section class="surface-error {className}" role="alert" data-surface-error>
162
+ <h2>Surface could not be rendered</h2>
163
+ <ul>
164
+ {#each validation.issues as issue, issueIndex (`${issue.code}:${issue.path}:${issueIndex}`)}
165
+ <li><code>{issue.code}</code>: {issue.message}</li>
166
+ {/each}
167
+ </ul>
168
+ </section>
169
+ {/if}
170
+
171
+ <style>
172
+ .surface,
173
+ .surface-error {
174
+ width: 100%;
175
+ min-width: 0;
176
+ }
177
+
178
+ .surface-header {
179
+ margin-bottom: 1rem;
180
+ }
181
+
182
+ .surface-header h2,
183
+ .surface-error h2 {
184
+ margin: 0;
185
+ color: var(--foreground);
186
+ font-size: clamp(1.25rem, 2vw, 1.75rem);
187
+ line-height: 1.2;
188
+ }
189
+
190
+ .surface-grid {
191
+ display: grid;
192
+ grid-template-columns: repeat(12, minmax(0, 1fr));
193
+ }
194
+
195
+ .surface-gap-sm { gap: 0.5rem; }
196
+ .surface-gap-md { gap: 1rem; }
197
+ .surface-gap-lg { gap: 1.5rem; }
198
+
199
+ .surface-widget {
200
+ grid-column: 1 / -1;
201
+ min-width: 0;
202
+ }
203
+
204
+ .surface-error {
205
+ padding: 1rem;
206
+ border: 1px solid var(--destructive);
207
+ border-radius: 0.75rem;
208
+ background: var(--card);
209
+ color: var(--destructive);
210
+ }
211
+
212
+ .surface-error ul {
213
+ margin: 0.75rem 0 0;
214
+ padding-left: 1.25rem;
215
+ }
216
+
217
+ @media (min-width: 48rem) {
218
+ .surface-span-1 { grid-column: span 1; }
219
+ .surface-span-2 { grid-column: span 2; }
220
+ .surface-span-3 { grid-column: span 3; }
221
+ .surface-span-4 { grid-column: span 4; }
222
+ .surface-span-5 { grid-column: span 5; }
223
+ .surface-span-6 { grid-column: span 6; }
224
+ .surface-span-7 { grid-column: span 7; }
225
+ .surface-span-8 { grid-column: span 8; }
226
+ .surface-span-9 { grid-column: span 9; }
227
+ .surface-span-10 { grid-column: span 10; }
228
+ .surface-span-11 { grid-column: span 11; }
229
+ .surface-span-12 { grid-column: span 12; }
230
+ }
231
+ </style>
@@ -0,0 +1,22 @@
1
+ import type { SurfaceRenderCatalog } from '../catalog.js';
2
+ import type { SurfaceDataError, SurfaceDataProvider, SurfacePolicy, SurfaceValidationIssue } from '../types.js';
3
+ export type SurfaceRendererError = {
4
+ readonly type: 'validation';
5
+ readonly issues: readonly SurfaceValidationIssue[];
6
+ } | {
7
+ readonly type: 'data';
8
+ readonly error: SurfaceDataError;
9
+ };
10
+ export interface SurfaceRendererProps {
11
+ readonly spec: unknown;
12
+ readonly policy: SurfacePolicy;
13
+ readonly catalog?: SurfaceRenderCatalog;
14
+ readonly dataProvider?: SurfaceDataProvider;
15
+ readonly class?: string;
16
+ readonly onError?: (error: SurfaceRendererError) => void;
17
+ }
18
+ declare const SurfaceRenderer: import("svelte").Component<SurfaceRendererProps, {
19
+ refresh: (sourceId?: string) => Promise<void>;
20
+ }, "">;
21
+ type SurfaceRenderer = ReturnType<typeof SurfaceRenderer>;
22
+ export default SurfaceRenderer;
@@ -0,0 +1,3 @@
1
+ export { SURFACE_LIMITS, SURFACE_SCHEMA_VERSION } from './types.js';
2
+ export type { JsonObject, JsonPrimitive, JsonValue, ResourceListDataSource, ResourceListSource, ResourceOneDataSource, ResourceOneSource, SurfaceBinding, SurfaceCatalog, SurfaceCatalogDataKind, SurfaceDataError, SurfaceDataProvider, SurfaceDataSource, SurfaceFilter, SurfaceGridLayout, SurfaceGridSpan, SurfacePolicy, SurfaceResourcePolicy, SurfaceSort, SurfaceSpec, SurfaceValidationCode, SurfaceValidationIssue, SurfaceValidationResult, SurfaceWidget, SurfaceWidgetDataState, SurfaceWidgetDefinition, } from './types.js';
3
+ export { validateSurfaceSpec } from './validation.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { SURFACE_LIMITS, SURFACE_SCHEMA_VERSION } from './types.js';
2
+ export { validateSurfaceSpec } from './validation.js';
package/dist/json.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { JsonValue } from './types.js';
2
+ export interface JsonValueIssue {
3
+ readonly path: readonly (string | number)[];
4
+ readonly message: string;
5
+ }
6
+ export declare function escapedJsonPointerToken(token: string): string;
7
+ export declare function jsonPointer(pathSegments: readonly (string | number)[]): string;
8
+ export declare function decodedJsonPointerToken(token: string): string | null;
9
+ export declare function jsonValueIssue(input: unknown): JsonValueIssue | null;
10
+ export declare function isJsonValue(input: unknown): input is JsonValue;
package/dist/json.js ADDED
@@ -0,0 +1,115 @@
1
+ import { SURFACE_LIMITS } from './types.js';
2
+ const dangerousObjectKeys = new Set(['__proto__', 'constructor', 'prototype']);
3
+ export function escapedJsonPointerToken(token) {
4
+ return token.replaceAll('~', '~0').replaceAll('/', '~1');
5
+ }
6
+ export function jsonPointer(pathSegments) {
7
+ return pathSegments.length === 0
8
+ ? ''
9
+ : `/${pathSegments.map((segment) => escapedJsonPointerToken(String(segment))).join('/')}`;
10
+ }
11
+ export function decodedJsonPointerToken(token) {
12
+ if (/~(?:[^01]|$)/u.test(token))
13
+ return null;
14
+ const decoded = token.replaceAll('~1', '/').replaceAll('~0', '~');
15
+ return dangerousObjectKeys.has(decoded) ? null : decoded;
16
+ }
17
+ function primitiveIssue(candidate, path) {
18
+ if (candidate === null || typeof candidate === 'string' || typeof candidate === 'boolean')
19
+ return null;
20
+ if (typeof candidate === 'number')
21
+ return Number.isFinite(candidate) ? null : { path, message: 'Numbers must be finite' };
22
+ return { path, message: 'Value must be JSON serializable' };
23
+ }
24
+ export function jsonValueIssue(input) {
25
+ const ancestors = new WeakSet();
26
+ const pending = [{ kind: 'visit', candidate: input, path: [], depth: 0 }];
27
+ let visitedNodes = 0;
28
+ try {
29
+ while (pending.length > 0) {
30
+ const frame = pending.pop();
31
+ if (!frame)
32
+ break;
33
+ if (frame.kind === 'leave') {
34
+ ancestors.delete(frame.candidate);
35
+ continue;
36
+ }
37
+ const { candidate, path, depth } = frame;
38
+ visitedNodes += 1;
39
+ if (visitedNodes > SURFACE_LIMITS.maxJsonNodes) {
40
+ return { path, message: 'JSON value exceeds the supported node count' };
41
+ }
42
+ if (depth > SURFACE_LIMITS.maxJsonDepth) {
43
+ return { path, message: 'JSON nesting exceeds the supported depth' };
44
+ }
45
+ if (typeof candidate !== 'object' || candidate === null) {
46
+ const issue = primitiveIssue(candidate, path);
47
+ if (issue)
48
+ return issue;
49
+ continue;
50
+ }
51
+ if (ancestors.has(candidate))
52
+ return { path, message: 'Cyclic values are not allowed' };
53
+ const prototype = Object.getPrototypeOf(candidate);
54
+ if (!Array.isArray(candidate) && prototype !== Object.prototype && prototype !== null) {
55
+ return { path, message: 'Only plain JSON objects are allowed' };
56
+ }
57
+ ancestors.add(candidate);
58
+ pending.push({ kind: 'leave', candidate });
59
+ if (Array.isArray(candidate)) {
60
+ if (candidate.length + visitedNodes > SURFACE_LIMITS.maxJsonNodes) {
61
+ return { path, message: 'JSON value exceeds the supported node count' };
62
+ }
63
+ for (const key of Reflect.ownKeys(candidate)) {
64
+ if (key === 'length')
65
+ continue;
66
+ if (typeof key !== 'string')
67
+ return { path, message: 'Symbol properties are not allowed' };
68
+ if (!/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= candidate.length) {
69
+ return { path: [...path, key], message: 'Only JSON array indexes are allowed' };
70
+ }
71
+ }
72
+ for (let index = candidate.length - 1; index >= 0; index -= 1) {
73
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, String(index));
74
+ if (!descriptor)
75
+ return { path: [...path, index], message: 'Sparse arrays are not allowed' };
76
+ if (!descriptor.enumerable) {
77
+ return { path: [...path, index], message: 'Non-enumerable properties are not allowed' };
78
+ }
79
+ if (!('value' in descriptor)) {
80
+ return { path: [...path, index], message: 'Accessor properties are not allowed' };
81
+ }
82
+ pending.push({ kind: 'visit', candidate: descriptor.value, path: [...path, index], depth: depth + 1 });
83
+ }
84
+ continue;
85
+ }
86
+ const entries = [];
87
+ for (const key of Reflect.ownKeys(candidate)) {
88
+ if (typeof key !== 'string')
89
+ return { path, message: 'Symbol properties are not allowed' };
90
+ if (dangerousObjectKeys.has(key)) {
91
+ return { path: [...path, key], message: `Property "${key}" is not allowed` };
92
+ }
93
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
94
+ if (!descriptor || !descriptor.enumerable) {
95
+ return { path: [...path, key], message: 'Non-enumerable properties are not allowed' };
96
+ }
97
+ if (!('value' in descriptor)) {
98
+ return { path: [...path, key], message: 'Accessor properties are not allowed' };
99
+ }
100
+ entries.push([key, descriptor.value]);
101
+ }
102
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
103
+ const [key, entry] = entries[index];
104
+ pending.push({ kind: 'visit', candidate: entry, path: [...path, key], depth: depth + 1 });
105
+ }
106
+ }
107
+ }
108
+ catch {
109
+ return { path: [], message: 'Value could not be inspected safely' };
110
+ }
111
+ return null;
112
+ }
113
+ export function isJsonValue(input) {
114
+ return jsonValueIssue(input) === null;
115
+ }
@@ -0,0 +1,17 @@
1
+ import type { SurfaceDataProvider, SurfaceDataSource, SurfaceResourcePolicy, SurfaceWidgetDataState } from './types.js';
2
+ export type SurfaceReadAction = 'list' | 'show';
3
+ export interface SurfaceAccessDecision {
4
+ readonly can: boolean;
5
+ readonly reason?: string;
6
+ }
7
+ export type SurfaceAuthorizer = (resource: string, action: SurfaceReadAction) => Promise<SurfaceAccessDecision>;
8
+ export interface LoadSurfaceSourceRequest {
9
+ readonly source: SurfaceDataSource;
10
+ readonly resourcePolicy: SurfaceResourcePolicy;
11
+ readonly provider: SurfaceDataProvider;
12
+ readonly authorize: SurfaceAuthorizer;
13
+ }
14
+ export type SurfaceLoadedSourceState = Extract<SurfaceWidgetDataState, {
15
+ status: 'ready' | 'error';
16
+ }>;
17
+ export declare function loadSurfaceSource(request: LoadSurfaceSourceRequest): Promise<SurfaceLoadedSourceState>;
@@ -0,0 +1,128 @@
1
+ import { isJsonValue, jsonValueIssue } from './json.js';
2
+ function isPlainRecord(value) {
3
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
4
+ return false;
5
+ const prototype = Object.getPrototypeOf(value);
6
+ return prototype === Object.prototype || prototype === null;
7
+ }
8
+ function ownDataProperty(record, property) {
9
+ const descriptor = Object.getOwnPropertyDescriptor(record, property);
10
+ if (!descriptor)
11
+ return { ok: false, message: `Provider result must define "${property}"` };
12
+ return 'value' in descriptor
13
+ ? { ok: true, value: descriptor.value }
14
+ : { ok: false, message: `Provider result field "${property}" must be a data property` };
15
+ }
16
+ function projectRecord(record, readableFields) {
17
+ if (!isPlainRecord(record))
18
+ return { ok: false, message: 'Provider records must be plain objects' };
19
+ const projectedRecord = Object.create(null);
20
+ for (const field of readableFields) {
21
+ const descriptor = Object.getOwnPropertyDescriptor(record, field);
22
+ if (!descriptor)
23
+ continue;
24
+ if (!('value' in descriptor)) {
25
+ return { ok: false, message: `Field "${field}" must be a data property` };
26
+ }
27
+ const fieldValue = descriptor.value;
28
+ if (!isJsonValue(fieldValue)) {
29
+ const issue = jsonValueIssue(fieldValue);
30
+ return { ok: false, message: `Field "${field}" is not JSON serializable: ${issue?.message ?? 'invalid value'}` };
31
+ }
32
+ projectedRecord[field] = fieldValue;
33
+ }
34
+ return { ok: true, value: projectedRecord };
35
+ }
36
+ function projectRecords(records, readableFields) {
37
+ const projectedRecords = [];
38
+ for (const record of records) {
39
+ const projection = projectRecord(record, readableFields);
40
+ if (!projection.ok)
41
+ return projection;
42
+ projectedRecords.push(projection.value);
43
+ }
44
+ return { ok: true, value: projectedRecords };
45
+ }
46
+ function sourceError(sourceId, code, message) {
47
+ return { status: 'error', sourceId, error: { code, sourceId, message } };
48
+ }
49
+ function listParams(source) {
50
+ const sorters = source.sorters?.map((sorter) => ({ ...sorter })) ?? [];
51
+ const filters = source.filters?.map((filter) => {
52
+ if (!('value' in filter))
53
+ return { ...filter, value: null };
54
+ return Array.isArray(filter.value)
55
+ ? { ...filter, value: [...filter.value] }
56
+ : { ...filter };
57
+ }) ?? [];
58
+ return {
59
+ resource: source.resource,
60
+ pagination: { current: 1, pageSize: source.pageSize ?? 10 },
61
+ sorters,
62
+ filters,
63
+ };
64
+ }
65
+ async function loadListSource(source, resourcePolicy, provider) {
66
+ const providerResponse = await provider.getList(listParams(source));
67
+ if (!isPlainRecord(providerResponse)) {
68
+ return sourceError(source.id, 'provider_result_not_json', 'List result must be a plain object');
69
+ }
70
+ const recordsProperty = ownDataProperty(providerResponse, 'data');
71
+ if (!recordsProperty.ok)
72
+ return sourceError(source.id, 'provider_result_not_json', recordsProperty.message);
73
+ const totalProperty = ownDataProperty(providerResponse, 'total');
74
+ if (!totalProperty.ok)
75
+ return sourceError(source.id, 'provider_result_not_json', totalProperty.message);
76
+ const records = recordsProperty.value;
77
+ const total = totalProperty.value;
78
+ if (!Array.isArray(records)) {
79
+ return sourceError(source.id, 'provider_result_not_json', 'List data must be an array');
80
+ }
81
+ if (typeof total !== 'number' || !Number.isFinite(total) || total < 0) {
82
+ return sourceError(source.id, 'provider_result_not_json', 'List total must be a non-negative finite number');
83
+ }
84
+ const projection = projectRecords(records, resourcePolicy.readFields);
85
+ if (!projection.ok)
86
+ return sourceError(source.id, 'provider_result_not_json', projection.message);
87
+ return {
88
+ status: 'ready',
89
+ sourceId: source.id,
90
+ value: { items: projection.value, total },
91
+ };
92
+ }
93
+ async function loadOneSource(source, resourcePolicy, provider) {
94
+ const providerResponse = await provider.getOne({ resource: source.resource, id: source.recordId });
95
+ if (!isPlainRecord(providerResponse)) {
96
+ return sourceError(source.id, 'provider_result_not_json', 'Get-one result must be a plain object');
97
+ }
98
+ const dataProperty = ownDataProperty(providerResponse, 'data');
99
+ if (!dataProperty.ok)
100
+ return sourceError(source.id, 'provider_result_not_json', dataProperty.message);
101
+ const projection = projectRecord(dataProperty.value, resourcePolicy.readFields);
102
+ return projection.ok
103
+ ? { status: 'ready', sourceId: source.id, value: projection.value }
104
+ : sourceError(source.id, 'provider_result_not_json', projection.message);
105
+ }
106
+ export async function loadSurfaceSource(request) {
107
+ const { source, resourcePolicy, provider, authorize } = request;
108
+ const action = source.type === 'resource-list' ? 'list' : 'show';
109
+ let accessDecision;
110
+ try {
111
+ accessDecision = await authorize(source.resource, action);
112
+ }
113
+ catch (accessFailure) {
114
+ const message = accessFailure instanceof Error ? accessFailure.message : 'Access check failed';
115
+ return sourceError(source.id, 'access_check_failed', message);
116
+ }
117
+ if (!accessDecision.can)
118
+ return sourceError(source.id, 'access_denied', accessDecision.reason ?? 'Access denied');
119
+ try {
120
+ return source.type === 'resource-list'
121
+ ? await loadListSource(source, resourcePolicy, provider)
122
+ : await loadOneSource(source, resourcePolicy, provider);
123
+ }
124
+ catch (providerFailure) {
125
+ const message = providerFailure instanceof Error ? providerFailure.message : 'Data provider request failed';
126
+ return sourceError(source.id, 'provider_failed', message);
127
+ }
128
+ }
@@ -0,0 +1,4 @@
1
+ export { default as SurfaceRenderer } from './components/SurfaceRenderer.svelte';
2
+ export type { SurfaceRendererError, SurfaceRendererProps, } from './components/SurfaceRenderer.svelte';
3
+ export { DEFAULT_SURFACE_CATALOG_VERSION, defaultSurfaceCatalog, defineSurfaceCatalog, } from './catalog.js';
4
+ export type { SurfaceRenderCatalog, SurfaceWidgetRegistration, SurfaceWidgetRendererProps, } from './catalog.js';
package/dist/svelte.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default as SurfaceRenderer } from './components/SurfaceRenderer.svelte';
2
+ export { DEFAULT_SURFACE_CATALOG_VERSION, defaultSurfaceCatalog, defineSurfaceCatalog, } from './catalog.js';