@checkmate-monitor/catalog-common 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.
- package/CHANGELOG.md +57 -0
- package/package.json +26 -0
- package/src/index.ts +6 -0
- package/src/permissions.ts +17 -0
- package/src/plugin-metadata.ts +9 -0
- package/src/routes.ts +10 -0
- package/src/rpc-contract.ts +228 -0
- package/src/slots.ts +74 -0
- package/src/types.ts +36 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @checkmate-monitor/catalog-common
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ffc28f6: ### Anonymous Role and Public Access
|
|
8
|
+
|
|
9
|
+
Introduces a configurable "anonymous" role for managing permissions available to unauthenticated users.
|
|
10
|
+
|
|
11
|
+
**Core Changes:**
|
|
12
|
+
|
|
13
|
+
- Added `userType: "public"` - endpoints accessible by both authenticated users (with their permissions) and anonymous users (with anonymous role permissions)
|
|
14
|
+
- Renamed `userType: "both"` to `"authenticated"` for clarity
|
|
15
|
+
- Renamed `isDefault` to `isAuthenticatedDefault` on Permission interface
|
|
16
|
+
- Added `isPublicDefault` flag for permissions that should be granted to the anonymous role by default
|
|
17
|
+
|
|
18
|
+
**Backend Infrastructure:**
|
|
19
|
+
|
|
20
|
+
- New `anonymous` system role created during auth-backend initialization
|
|
21
|
+
- New `disabled_public_default_permission` table tracks admin-disabled public defaults
|
|
22
|
+
- `autoAuthMiddleware` now checks anonymous role permissions for unauthenticated public endpoint access
|
|
23
|
+
- `AuthService.getAnonymousPermissions()` with 1-minute caching for performance
|
|
24
|
+
- Anonymous role filtered from `getRoles` endpoint (not assignable to users)
|
|
25
|
+
- Validation prevents assigning anonymous role to users
|
|
26
|
+
|
|
27
|
+
**Catalog Integration:**
|
|
28
|
+
|
|
29
|
+
- `catalog.read` permission now has both `isAuthenticatedDefault` and `isPublicDefault`
|
|
30
|
+
- Read endpoints (`getSystems`, `getGroups`, `getEntities`) now use `userType: "public"`
|
|
31
|
+
|
|
32
|
+
**UI:**
|
|
33
|
+
|
|
34
|
+
- New `PermissionGate` component for conditionally rendering content based on permissions
|
|
35
|
+
|
|
36
|
+
- 4dd644d: Enable external application (API key) access to management endpoints
|
|
37
|
+
|
|
38
|
+
Changed `userType: "user"` to `userType: "authenticated"` for 52 endpoints across 5 packages, allowing external applications (service accounts with API keys) to call these endpoints programmatically while maintaining RBAC permission checks:
|
|
39
|
+
|
|
40
|
+
- **incident-common**: createIncident, updateIncident, addUpdate, resolveIncident, deleteIncident
|
|
41
|
+
- **maintenance-common**: createMaintenance, updateMaintenance, addUpdate, closeMaintenance, deleteMaintenance
|
|
42
|
+
- **catalog-common**: System CRUD, Group CRUD, addSystemToGroup, removeSystemFromGroup
|
|
43
|
+
- **healthcheck-common**: Configuration management, system associations, retention config, detailed history
|
|
44
|
+
- **integration-common**: Subscription management, connection management, event discovery, delivery logs
|
|
45
|
+
|
|
46
|
+
This enables automation use cases such as:
|
|
47
|
+
|
|
48
|
+
- Creating incidents from external monitoring systems (Prometheus, Grafana)
|
|
49
|
+
- Scheduling maintenances from CI/CD pipelines
|
|
50
|
+
- Managing catalog systems from infrastructure-as-code tools
|
|
51
|
+
- Configuring health checks from deployment scripts
|
|
52
|
+
|
|
53
|
+
### Patch Changes
|
|
54
|
+
|
|
55
|
+
- Updated dependencies [ffc28f6]
|
|
56
|
+
- @checkmate-monitor/common@0.1.0
|
|
57
|
+
- @checkmate-monitor/frontend-api@0.0.2
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkmate-monitor/catalog-common",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./src/index.ts"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@checkmate-monitor/common": "workspace:*",
|
|
12
|
+
"@checkmate-monitor/frontend-api": "workspace:*",
|
|
13
|
+
"@orpc/contract": "^1.13.2",
|
|
14
|
+
"zod": "^4.2.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.7.2",
|
|
18
|
+
"@checkmate-monitor/tsconfig": "workspace:*",
|
|
19
|
+
"@checkmate-monitor/scripts": "workspace:*"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"lint": "bun run lint:code",
|
|
24
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createPermission } from "@checkmate-monitor/common";
|
|
2
|
+
|
|
3
|
+
export const permissions = {
|
|
4
|
+
catalogRead: createPermission(
|
|
5
|
+
"catalog",
|
|
6
|
+
"read",
|
|
7
|
+
"Read Catalog (Systems and Groups)",
|
|
8
|
+
{ isAuthenticatedDefault: true, isPublicDefault: true }
|
|
9
|
+
),
|
|
10
|
+
catalogManage: createPermission(
|
|
11
|
+
"catalog",
|
|
12
|
+
"manage",
|
|
13
|
+
"Full management of Catalog (Systems and Groups)"
|
|
14
|
+
),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const permissionList = Object.values(permissions);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { definePluginMetadata } from "@checkmate-monitor/common";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin metadata for the catalog plugin.
|
|
5
|
+
* Exported from the common package so both backend and frontend can reference it.
|
|
6
|
+
*/
|
|
7
|
+
export const pluginMetadata = definePluginMetadata({
|
|
8
|
+
pluginId: "catalog",
|
|
9
|
+
});
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import {
|
|
3
|
+
createClientDefinition,
|
|
4
|
+
type ProcedureMetadata,
|
|
5
|
+
} from "@checkmate-monitor/common";
|
|
6
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { SystemSchema, GroupSchema, ViewSchema } from "./types";
|
|
9
|
+
import { permissions } from "./permissions";
|
|
10
|
+
|
|
11
|
+
// Base builder with full metadata support
|
|
12
|
+
const _base = oc.$meta<ProcedureMetadata>({});
|
|
13
|
+
|
|
14
|
+
// Input schemas that match the service layer expectations
|
|
15
|
+
const CreateSystemInputSchema = z.object({
|
|
16
|
+
name: z.string(),
|
|
17
|
+
description: z.string().optional(),
|
|
18
|
+
owner: z.string().optional(),
|
|
19
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const UpdateSystemInputSchema = z.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
data: z.object({
|
|
25
|
+
name: z.string().optional(),
|
|
26
|
+
description: z.string().nullable().optional(), // Allow nullable for updates
|
|
27
|
+
owner: z.string().nullable().optional(),
|
|
28
|
+
metadata: z.record(z.string(), z.unknown()).nullable().optional(), // Allow nullable
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const CreateGroupInputSchema = z.object({
|
|
33
|
+
name: z.string(),
|
|
34
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const UpdateGroupInputSchema = z.object({
|
|
38
|
+
id: z.string(),
|
|
39
|
+
data: z.object({
|
|
40
|
+
name: z.string().optional(),
|
|
41
|
+
metadata: z.record(z.string(), z.unknown()).nullable().optional(), // Allow nullable
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const CreateViewInputSchema = z.object({
|
|
46
|
+
name: z.string(),
|
|
47
|
+
description: z.string().optional(),
|
|
48
|
+
configuration: z.unknown(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Catalog RPC Contract using oRPC's contract-first pattern
|
|
52
|
+
export const catalogContract = {
|
|
53
|
+
// ==========================================================================
|
|
54
|
+
// ENTITY READ ENDPOINTS (userType: "public" - accessible by anyone with permission)
|
|
55
|
+
// ==========================================================================
|
|
56
|
+
|
|
57
|
+
getEntities: _base
|
|
58
|
+
.meta({ userType: "public", permissions: [permissions.catalogRead.id] })
|
|
59
|
+
.output(
|
|
60
|
+
z.object({
|
|
61
|
+
systems: z.array(SystemSchema),
|
|
62
|
+
groups: z.array(GroupSchema),
|
|
63
|
+
})
|
|
64
|
+
),
|
|
65
|
+
|
|
66
|
+
getSystems: _base
|
|
67
|
+
.meta({ userType: "public", permissions: [permissions.catalogRead.id] })
|
|
68
|
+
.output(z.array(SystemSchema)),
|
|
69
|
+
|
|
70
|
+
getSystem: _base
|
|
71
|
+
.meta({ userType: "public", permissions: [permissions.catalogRead.id] })
|
|
72
|
+
.input(z.object({ systemId: z.string() }))
|
|
73
|
+
.output(SystemSchema.nullable()),
|
|
74
|
+
|
|
75
|
+
getGroups: _base
|
|
76
|
+
.meta({ userType: "public", permissions: [permissions.catalogRead.id] })
|
|
77
|
+
.output(z.array(GroupSchema)),
|
|
78
|
+
|
|
79
|
+
// ==========================================================================
|
|
80
|
+
// SYSTEM MANAGEMENT (userType: "authenticated" with manage permission)
|
|
81
|
+
// ==========================================================================
|
|
82
|
+
|
|
83
|
+
createSystem: _base
|
|
84
|
+
.meta({
|
|
85
|
+
userType: "authenticated",
|
|
86
|
+
permissions: [permissions.catalogManage.id],
|
|
87
|
+
})
|
|
88
|
+
.input(CreateSystemInputSchema)
|
|
89
|
+
.output(SystemSchema),
|
|
90
|
+
|
|
91
|
+
updateSystem: _base
|
|
92
|
+
.meta({
|
|
93
|
+
userType: "authenticated",
|
|
94
|
+
permissions: [permissions.catalogManage.id],
|
|
95
|
+
})
|
|
96
|
+
.input(UpdateSystemInputSchema)
|
|
97
|
+
.output(SystemSchema),
|
|
98
|
+
|
|
99
|
+
deleteSystem: _base
|
|
100
|
+
.meta({
|
|
101
|
+
userType: "authenticated",
|
|
102
|
+
permissions: [permissions.catalogManage.id],
|
|
103
|
+
})
|
|
104
|
+
.input(z.string())
|
|
105
|
+
.output(z.object({ success: z.boolean() })),
|
|
106
|
+
|
|
107
|
+
// ==========================================================================
|
|
108
|
+
// GROUP MANAGEMENT (userType: "authenticated" with manage permission)
|
|
109
|
+
// ==========================================================================
|
|
110
|
+
|
|
111
|
+
createGroup: _base
|
|
112
|
+
.meta({
|
|
113
|
+
userType: "authenticated",
|
|
114
|
+
permissions: [permissions.catalogManage.id],
|
|
115
|
+
})
|
|
116
|
+
.input(CreateGroupInputSchema)
|
|
117
|
+
.output(GroupSchema),
|
|
118
|
+
|
|
119
|
+
updateGroup: _base
|
|
120
|
+
.meta({
|
|
121
|
+
userType: "authenticated",
|
|
122
|
+
permissions: [permissions.catalogManage.id],
|
|
123
|
+
})
|
|
124
|
+
.input(UpdateGroupInputSchema)
|
|
125
|
+
.output(GroupSchema),
|
|
126
|
+
|
|
127
|
+
deleteGroup: _base
|
|
128
|
+
.meta({
|
|
129
|
+
userType: "authenticated",
|
|
130
|
+
permissions: [permissions.catalogManage.id],
|
|
131
|
+
})
|
|
132
|
+
.input(z.string())
|
|
133
|
+
.output(z.object({ success: z.boolean() })),
|
|
134
|
+
|
|
135
|
+
// ==========================================================================
|
|
136
|
+
// SYSTEM-GROUP RELATIONSHIPS (userType: "authenticated" with manage permission)
|
|
137
|
+
// ==========================================================================
|
|
138
|
+
|
|
139
|
+
addSystemToGroup: _base
|
|
140
|
+
.meta({
|
|
141
|
+
userType: "authenticated",
|
|
142
|
+
permissions: [permissions.catalogManage.id],
|
|
143
|
+
})
|
|
144
|
+
.input(
|
|
145
|
+
z.object({
|
|
146
|
+
groupId: z.string(),
|
|
147
|
+
systemId: z.string(),
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
.output(z.object({ success: z.boolean() })),
|
|
151
|
+
|
|
152
|
+
removeSystemFromGroup: _base
|
|
153
|
+
.meta({
|
|
154
|
+
userType: "authenticated",
|
|
155
|
+
permissions: [permissions.catalogManage.id],
|
|
156
|
+
})
|
|
157
|
+
.input(
|
|
158
|
+
z.object({
|
|
159
|
+
groupId: z.string(),
|
|
160
|
+
systemId: z.string(),
|
|
161
|
+
})
|
|
162
|
+
)
|
|
163
|
+
.output(z.object({ success: z.boolean() })),
|
|
164
|
+
|
|
165
|
+
// ==========================================================================
|
|
166
|
+
// VIEW MANAGEMENT (userType: "user")
|
|
167
|
+
// ==========================================================================
|
|
168
|
+
|
|
169
|
+
getViews: _base
|
|
170
|
+
.meta({ userType: "user", permissions: [permissions.catalogRead.id] })
|
|
171
|
+
.output(z.array(ViewSchema)),
|
|
172
|
+
|
|
173
|
+
createView: _base
|
|
174
|
+
.meta({ userType: "user", permissions: [permissions.catalogManage.id] })
|
|
175
|
+
.input(CreateViewInputSchema)
|
|
176
|
+
.output(ViewSchema),
|
|
177
|
+
|
|
178
|
+
// ==========================================================================
|
|
179
|
+
// SERVICE INTERFACE (userType: "service" - backend-to-backend only)
|
|
180
|
+
// ==========================================================================
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Notify all users subscribed to a system (and optionally its groups).
|
|
184
|
+
* This is used by other plugins (e.g., maintenance) to send notifications
|
|
185
|
+
* to system subscribers without needing direct access to the notification service.
|
|
186
|
+
*
|
|
187
|
+
* Deduplication: If includeGroupSubscribers is true, subscribers are
|
|
188
|
+
* deduplicated so users subscribed to both the system AND its groups
|
|
189
|
+
* receive only one notification.
|
|
190
|
+
*/
|
|
191
|
+
notifySystemSubscribers: _base
|
|
192
|
+
.meta({ userType: "service" })
|
|
193
|
+
.input(
|
|
194
|
+
z.object({
|
|
195
|
+
systemId: z
|
|
196
|
+
.string()
|
|
197
|
+
.describe("The system ID to notify subscribers for"),
|
|
198
|
+
title: z.string().describe("Notification title"),
|
|
199
|
+
/** Notification body in markdown format */
|
|
200
|
+
body: z.string().describe("Notification body (supports markdown)"),
|
|
201
|
+
importance: z.enum(["info", "warning", "critical"]).optional(),
|
|
202
|
+
/** Primary action button */
|
|
203
|
+
action: z
|
|
204
|
+
.object({
|
|
205
|
+
label: z.string(),
|
|
206
|
+
url: z.string(),
|
|
207
|
+
})
|
|
208
|
+
.optional(),
|
|
209
|
+
includeGroupSubscribers: z
|
|
210
|
+
.boolean()
|
|
211
|
+
.optional()
|
|
212
|
+
.describe(
|
|
213
|
+
"If true, also notify subscribers of groups that contain this system"
|
|
214
|
+
),
|
|
215
|
+
})
|
|
216
|
+
)
|
|
217
|
+
.output(z.object({ notifiedCount: z.number() })),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Export contract type
|
|
221
|
+
export type CatalogContract = typeof catalogContract;
|
|
222
|
+
|
|
223
|
+
// Export client definition for type-safe forPlugin usage
|
|
224
|
+
// Use: const client = rpcApi.forPlugin(CatalogApi);
|
|
225
|
+
export const CatalogApi = createClientDefinition(
|
|
226
|
+
catalogContract,
|
|
227
|
+
pluginMetadata
|
|
228
|
+
);
|
package/src/slots.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createSlot } from "@checkmate-monitor/frontend-api";
|
|
2
|
+
import type { System } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Slot for extending the top of the System Details page.
|
|
6
|
+
* Use for important alerts like active maintenances that should be shown prominently.
|
|
7
|
+
* Extensions receive the full system object.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* extensions: [{
|
|
11
|
+
* id: "my-plugin.system-details-top",
|
|
12
|
+
* slotId: SystemDetailsTopSlot.id,
|
|
13
|
+
* component: ({ system }) => <MaintenanceAlert system={system} />,
|
|
14
|
+
* }]
|
|
15
|
+
*/
|
|
16
|
+
export const SystemDetailsTopSlot = createSlot<{ system: System }>(
|
|
17
|
+
"plugin.catalog.system-details-top"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Slot for extending the System Details page with additional content.
|
|
22
|
+
* Extensions receive the full system object.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // In your plugin
|
|
26
|
+
* import { SystemDetailsSlot } from "@checkmate-monitor/catalog-common";
|
|
27
|
+
*
|
|
28
|
+
* extensions: [{
|
|
29
|
+
* id: "my-plugin.system-details",
|
|
30
|
+
* slotId: SystemDetailsSlot.id,
|
|
31
|
+
* component: ({ system }) => <MyComponent system={system} />,
|
|
32
|
+
* }]
|
|
33
|
+
*/
|
|
34
|
+
export const SystemDetailsSlot = createSlot<{ system: System }>(
|
|
35
|
+
"plugin.catalog.system-details"
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Slot for adding actions to the catalog system configuration page.
|
|
40
|
+
* Extensions receive the system ID and name.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // In your plugin
|
|
44
|
+
* import { CatalogSystemActionsSlot } from "@checkmate-monitor/catalog-common";
|
|
45
|
+
*
|
|
46
|
+
* extensions: [{
|
|
47
|
+
* id: "my-plugin.system-actions",
|
|
48
|
+
* slotId: CatalogSystemActionsSlot.id,
|
|
49
|
+
* component: ({ systemId, systemName }) => <MyAction systemId={systemId} />,
|
|
50
|
+
* }]
|
|
51
|
+
*/
|
|
52
|
+
export const CatalogSystemActionsSlot = createSlot<{
|
|
53
|
+
systemId: string;
|
|
54
|
+
systemName: string;
|
|
55
|
+
}>("plugin.catalog.system-actions");
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Slot for displaying system state badges.
|
|
59
|
+
* Plugins use this to contribute state indicators (e.g., health status, maintenance status).
|
|
60
|
+
* Extensions receive the system and should render badge components.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // In your plugin
|
|
64
|
+
* import { SystemStateBadgesSlot } from "@checkmate-monitor/catalog-common";
|
|
65
|
+
*
|
|
66
|
+
* extensions: [{
|
|
67
|
+
* id: "my-plugin.system-state-badge",
|
|
68
|
+
* slotId: SystemStateBadgesSlot.id,
|
|
69
|
+
* component: ({ system }) => <MyStatusBadge systemId={system.id} />,
|
|
70
|
+
* }]
|
|
71
|
+
*/
|
|
72
|
+
export const SystemStateBadgesSlot = createSlot<{ system: System }>(
|
|
73
|
+
"plugin.catalog.system-state-badges"
|
|
74
|
+
);
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Domain type schemas for catalog entities
|
|
4
|
+
// These match the database output types exactly
|
|
5
|
+
// JSON serialization will handle Date -> ISO string conversion automatically
|
|
6
|
+
|
|
7
|
+
export const SystemSchema = z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
name: z.string(),
|
|
10
|
+
description: z.string().nullable(),
|
|
11
|
+
owner: z.string().nullable(),
|
|
12
|
+
metadata: z.record(z.string(), z.unknown()).nullable(),
|
|
13
|
+
createdAt: z.date(),
|
|
14
|
+
updatedAt: z.date(),
|
|
15
|
+
});
|
|
16
|
+
export type System = z.infer<typeof SystemSchema>;
|
|
17
|
+
|
|
18
|
+
export const GroupSchema = z.object({
|
|
19
|
+
id: z.string(),
|
|
20
|
+
name: z.string(),
|
|
21
|
+
systemIds: z.array(z.string()), // Required field from the service layer
|
|
22
|
+
metadata: z.record(z.string(), z.unknown()).nullable(),
|
|
23
|
+
createdAt: z.date(),
|
|
24
|
+
updatedAt: z.date(),
|
|
25
|
+
});
|
|
26
|
+
export type Group = z.infer<typeof GroupSchema>;
|
|
27
|
+
|
|
28
|
+
export const ViewSchema = z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
name: z.string(),
|
|
31
|
+
description: z.string().nullable(),
|
|
32
|
+
configuration: z.unknown(),
|
|
33
|
+
createdAt: z.date(),
|
|
34
|
+
updatedAt: z.date(),
|
|
35
|
+
});
|
|
36
|
+
export type View = z.infer<typeof ViewSchema>;
|