@vertesia/tools-sdk 1.5.0-dev.20260714.072725Z → 1.5.0-dev.20260722.120446Z
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/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/server/app-package.d.ts +8 -0
- package/lib/server/app-package.d.ts.map +1 -1
- package/lib/server/app-package.js +72 -30
- package/lib/server/app-package.js.map +1 -1
- package/lib/server/content-types.d.ts.map +1 -1
- package/lib/server/content-types.js +40 -10
- package/lib/server/content-types.js.map +1 -1
- package/lib/server/dashboards.d.ts +4 -0
- package/lib/server/dashboards.d.ts.map +1 -0
- package/lib/server/dashboards.js +25 -0
- package/lib/server/dashboards.js.map +1 -0
- package/lib/server/types.d.ts +9 -1
- package/lib/server/types.d.ts.map +1 -1
- package/lib/server/views.d.ts +4 -0
- package/lib/server/views.d.ts.map +1 -0
- package/lib/server/views.js +25 -0
- package/lib/server/views.js.map +1 -0
- package/lib/server.d.ts.map +1 -1
- package/lib/server.js +7 -1
- package/lib/server.js.map +1 -1
- package/package.json +8 -7
- package/src/index.ts +2 -0
- package/src/server/app-package.test.ts +140 -0
- package/src/server/app-package.ts +89 -34
- package/src/server/content-types.test.ts +64 -0
- package/src/server/content-types.ts +43 -10
- package/src/server/dashboards.ts +31 -0
- package/src/server/types.ts +15 -1
- package/src/server/views.test.ts +45 -0
- package/src/server/views.ts +31 -0
- package/src/server.ts +7 -0
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { copyRuntimeAssets } from './copy-assets.js';
|
|
|
5
5
|
export * from './InteractionCollection.js';
|
|
6
6
|
export * from './RenderingTemplateCollection.js';
|
|
7
7
|
export * from './SkillCollection.js';
|
|
8
|
+
export type { BuildAppPackageOptions } from './server/app-package.js';
|
|
9
|
+
export { buildAppPackage } from './server/app-package.js';
|
|
8
10
|
export * from './server/types.js';
|
|
9
11
|
export * from './server.js';
|
|
10
12
|
export * from './site/templates.js';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { PromptRole } from '@llumiverse/common';
|
|
2
|
+
import { PROCESS_DEFINITION_FORMAT_VERSION, TemplateType } from '@vertesia/common';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { ContentTypesCollection } from '../ContentTypesCollection.js';
|
|
5
|
+
import { InteractionCollection } from '../InteractionCollection.js';
|
|
6
|
+
import { buildAppPackage } from './app-package.js';
|
|
7
|
+
import type { ToolServerConfig } from './types.js';
|
|
8
|
+
|
|
9
|
+
describe('buildAppPackage', () => {
|
|
10
|
+
it('builds the same package artifact inventory used by the package route', async () => {
|
|
11
|
+
const config = {
|
|
12
|
+
interactions: [
|
|
13
|
+
new InteractionCollection({
|
|
14
|
+
name: 'claims',
|
|
15
|
+
interactions: [
|
|
16
|
+
{
|
|
17
|
+
name: 'review',
|
|
18
|
+
title: 'Review Claim',
|
|
19
|
+
prompts: [
|
|
20
|
+
{
|
|
21
|
+
role: PromptRole.user,
|
|
22
|
+
content: '{{user_prompt}}',
|
|
23
|
+
content_type: TemplateType.handlebars,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
types: [
|
|
31
|
+
new ContentTypesCollection({
|
|
32
|
+
name: 'claims',
|
|
33
|
+
types: [
|
|
34
|
+
{
|
|
35
|
+
name: 'claim',
|
|
36
|
+
object_schema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
status: { type: 'string' },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
}),
|
|
45
|
+
],
|
|
46
|
+
processes: [
|
|
47
|
+
{
|
|
48
|
+
id: 'claims:intake',
|
|
49
|
+
name: 'Claims Intake',
|
|
50
|
+
definition: {
|
|
51
|
+
format_version: PROCESS_DEFINITION_FORMAT_VERSION,
|
|
52
|
+
process: 'claims_intake',
|
|
53
|
+
initial: 'done',
|
|
54
|
+
context: {
|
|
55
|
+
schema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
additionalProperties: true,
|
|
58
|
+
},
|
|
59
|
+
initial: {},
|
|
60
|
+
},
|
|
61
|
+
nodes: {
|
|
62
|
+
done: { type: 'final', title: 'Done' },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
dashboards: [
|
|
68
|
+
{
|
|
69
|
+
id: 'claims:ops',
|
|
70
|
+
title: 'Claims Ops',
|
|
71
|
+
spec: {},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
uiConfig: {
|
|
75
|
+
src: '/lib/plugin.js',
|
|
76
|
+
available_in: ['app_portal'],
|
|
77
|
+
},
|
|
78
|
+
} satisfies ToolServerConfig;
|
|
79
|
+
|
|
80
|
+
const pkg = await buildAppPackage(config, {
|
|
81
|
+
origin: 'https://apps.example.test',
|
|
82
|
+
scope: 'all',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(pkg.interactions?.map((interaction) => interaction.id)).toEqual(['claims:review']);
|
|
86
|
+
expect(pkg.types?.map((type) => type.id)).toEqual(['claim']);
|
|
87
|
+
expect(pkg.processes?.map((process) => process.id)).toEqual(['claims:intake']);
|
|
88
|
+
expect(pkg.dashboards?.map((dashboard) => dashboard.id)).toEqual(['claims:ops']);
|
|
89
|
+
expect(pkg.ui?.src).toBe('https://apps.example.test/lib/plugin.js');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('honors package scopes', async () => {
|
|
93
|
+
const config = {
|
|
94
|
+
interactions: [
|
|
95
|
+
new InteractionCollection({
|
|
96
|
+
name: 'claims',
|
|
97
|
+
interactions: [{ name: 'review', prompts: [] }],
|
|
98
|
+
}),
|
|
99
|
+
],
|
|
100
|
+
types: [
|
|
101
|
+
new ContentTypesCollection({
|
|
102
|
+
name: 'claims',
|
|
103
|
+
types: [{ name: 'claim' }],
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
} satisfies ToolServerConfig;
|
|
107
|
+
|
|
108
|
+
const pkg = await buildAppPackage(config, { scope: 'types' });
|
|
109
|
+
|
|
110
|
+
expect(pkg.types?.map((type) => type.id)).toEqual(['claim']);
|
|
111
|
+
expect(pkg.interactions).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('buildAppPackage type identity', () => {
|
|
116
|
+
it('exposes the bare type name as the public id (collections are code organization only)', async () => {
|
|
117
|
+
const config = {
|
|
118
|
+
types: [
|
|
119
|
+
new ContentTypesCollection({ name: 'claims', types: [{ name: 'claim' }] }),
|
|
120
|
+
new ContentTypesCollection({ name: 'audit', types: [{ name: 'audit_event' }] }),
|
|
121
|
+
],
|
|
122
|
+
} satisfies ToolServerConfig;
|
|
123
|
+
|
|
124
|
+
const pkg = await buildAppPackage(config, { scope: 'types' });
|
|
125
|
+
expect(pkg.types?.map((type) => type.id)).toEqual(['claim', 'audit_event']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('fails the package build when a type name is duplicated across collections', async () => {
|
|
129
|
+
const config = {
|
|
130
|
+
types: [
|
|
131
|
+
new ContentTypesCollection({ name: 'claims', types: [{ name: 'claim' }] }),
|
|
132
|
+
new ContentTypesCollection({ name: 'legacy', types: [{ name: 'claim' }] }),
|
|
133
|
+
],
|
|
134
|
+
} satisfies ToolServerConfig;
|
|
135
|
+
|
|
136
|
+
await expect(buildAppPackage(config, { scope: 'types' })).rejects.toThrow(
|
|
137
|
+
/Duplicate content type name 'claim'/,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AppDashboardDefinition,
|
|
2
3
|
AppPackage,
|
|
3
4
|
AppPackageScope,
|
|
4
5
|
AppWidgetInfo,
|
|
@@ -15,14 +16,19 @@ function getRequestPayload<T>(c: Context): Promise<T | undefined> {
|
|
|
15
16
|
return c.req.method === 'POST' ? c.req.json<T>() : Promise.resolve(undefined);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
export interface BuildAppPackageOptions {
|
|
20
|
+
scope?: AppPackageScope | AppPackageScope[] | string;
|
|
21
|
+
origin?: string;
|
|
22
|
+
toolUseContext?: ToolUseContext;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type AppPackageBuilder = (pkg: AppPackage, config: ToolServerConfig, options: BuildAppPackageOptions) => Promise<void>;
|
|
26
|
+
|
|
27
|
+
const builders: Record<Exclude<AppPackageScope, 'all'>, AppPackageBuilder> = {
|
|
28
|
+
async tools(pkg: AppPackage, config: ToolServerConfig, options: BuildAppPackageOptions) {
|
|
23
29
|
const { tools: toolCollections = [], skills: skillCollections = [] } = config;
|
|
24
30
|
|
|
25
|
-
const filterContext =
|
|
31
|
+
const filterContext = options.toolUseContext;
|
|
26
32
|
|
|
27
33
|
// Aggregate all tools from all collections
|
|
28
34
|
const allTools = toolCollections.flatMap((collection) => collection.getToolDefinitions(filterContext));
|
|
@@ -59,13 +65,23 @@ const builders: Record<
|
|
|
59
65
|
pkg.interactions = allInteractions;
|
|
60
66
|
},
|
|
61
67
|
async types(pkg: AppPackage, config: ToolServerConfig) {
|
|
68
|
+
// A type's public id is its BARE name: stored objects reference it as the portable
|
|
69
|
+
// `app:<app>:<type>` string, so the in-code collection (a code-organization grouping,
|
|
70
|
+
// unlike tool/skill collections which are semantic) must NOT leak into the id.
|
|
71
|
+
// Names must therefore be unique across collections — fail the package build otherwise.
|
|
62
72
|
const allTypes: InCodeTypeDefinition[] = [];
|
|
73
|
+
const seen = new Map<string, string>();
|
|
63
74
|
for (const coll of config.types || []) {
|
|
64
75
|
for (const type of coll.types) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
const existing = seen.get(type.name);
|
|
77
|
+
if (existing) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Duplicate content type name '${type.name}' (collections '${existing}' and '${coll.name}'). ` +
|
|
80
|
+
`Type names must be unique across collections so the portable 'app:<app>:${type.name}' ref resolves.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
seen.set(type.name, coll.name);
|
|
84
|
+
allTypes.push({ ...type, id: type.name });
|
|
69
85
|
}
|
|
70
86
|
}
|
|
71
87
|
pkg.types = allTypes;
|
|
@@ -77,6 +93,9 @@ const builders: Record<
|
|
|
77
93
|
}
|
|
78
94
|
pkg.processes = allProcesses;
|
|
79
95
|
},
|
|
96
|
+
async views(pkg: AppPackage, config: ToolServerConfig) {
|
|
97
|
+
pkg.views = [...(config.views ?? [])];
|
|
98
|
+
},
|
|
80
99
|
async templates(pkg: AppPackage, config: ToolServerConfig) {
|
|
81
100
|
const basePath = `${config.prefix || '/api'}/templates`;
|
|
82
101
|
pkg.templates = (config.templates || []).flatMap((coll) =>
|
|
@@ -86,6 +105,19 @@ const builders: Record<
|
|
|
86
105
|
})),
|
|
87
106
|
);
|
|
88
107
|
},
|
|
108
|
+
async dashboards(pkg: AppPackage, config: ToolServerConfig) {
|
|
109
|
+
const seen = new Set<string>();
|
|
110
|
+
const dashboards: AppDashboardDefinition[] = [];
|
|
111
|
+
for (const dashboard of config.dashboards || []) {
|
|
112
|
+
if (seen.has(dashboard.id)) {
|
|
113
|
+
console.warn(`[app-package] Duplicate dashboard id "${dashboard.id}", skipping`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
seen.add(dashboard.id);
|
|
117
|
+
dashboards.push(dashboard);
|
|
118
|
+
}
|
|
119
|
+
pkg.dashboards = dashboards;
|
|
120
|
+
},
|
|
89
121
|
async widgets(pkg: AppPackage, config: ToolServerConfig) {
|
|
90
122
|
const { skills: skillCollections = [] } = config;
|
|
91
123
|
const widgets: Record<string, AppWidgetInfo> = {};
|
|
@@ -102,10 +134,10 @@ const builders: Record<
|
|
|
102
134
|
}
|
|
103
135
|
pkg.widgets = widgets;
|
|
104
136
|
},
|
|
105
|
-
async ui(pkg: AppPackage, config: ToolServerConfig,
|
|
137
|
+
async ui(pkg: AppPackage, config: ToolServerConfig, options: BuildAppPackageOptions) {
|
|
106
138
|
if (config.uiConfig) {
|
|
107
139
|
pkg.ui = { ...config.uiConfig };
|
|
108
|
-
const origin =
|
|
140
|
+
const origin = options.origin || 'http://localhost';
|
|
109
141
|
pkg.ui.src = new URL(pkg.ui.src, origin).toString();
|
|
110
142
|
if (!pkg.ui.isolation) {
|
|
111
143
|
pkg.ui.isolation = 'shadow';
|
|
@@ -128,52 +160,75 @@ const builders: Record<
|
|
|
128
160
|
},
|
|
129
161
|
};
|
|
130
162
|
|
|
131
|
-
|
|
132
|
-
const
|
|
163
|
+
function normalizeScopes(scope: BuildAppPackageOptions['scope']): Set<AppPackageScope> {
|
|
164
|
+
const values = Array.isArray(scope) ? scope : typeof scope === 'string' ? scope.split(',') : [scope || 'all'];
|
|
165
|
+
return new Set(values.filter(Boolean) as AppPackageScope[]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function buildAppPackage(
|
|
169
|
+
config: ToolServerConfig,
|
|
170
|
+
options: BuildAppPackageOptions = {},
|
|
171
|
+
): Promise<AppPackage> {
|
|
133
172
|
const pkg: AppPackage = {};
|
|
134
173
|
|
|
135
|
-
const scopes =
|
|
136
|
-
// TODO build pkg based on the query param scope
|
|
174
|
+
const scopes = normalizeScopes(options.scope);
|
|
137
175
|
if (scopes.has('all')) {
|
|
138
|
-
await builders.tools(pkg, config,
|
|
139
|
-
await builders.interactions(pkg, config,
|
|
140
|
-
await builders.types(pkg, config,
|
|
141
|
-
await builders.processes(pkg, config,
|
|
142
|
-
await builders.
|
|
143
|
-
await builders.
|
|
144
|
-
await builders.
|
|
145
|
-
await builders.
|
|
146
|
-
await builders.
|
|
176
|
+
await builders.tools(pkg, config, options);
|
|
177
|
+
await builders.interactions(pkg, config, options);
|
|
178
|
+
await builders.types(pkg, config, options);
|
|
179
|
+
await builders.processes(pkg, config, options);
|
|
180
|
+
await builders.views(pkg, config, options);
|
|
181
|
+
await builders.templates(pkg, config, options);
|
|
182
|
+
await builders.dashboards(pkg, config, options);
|
|
183
|
+
await builders.widgets(pkg, config, options);
|
|
184
|
+
await builders.ui(pkg, config, options);
|
|
185
|
+
await builders.settings(pkg, config, options);
|
|
186
|
+
await builders.activities(pkg, config, options);
|
|
147
187
|
} else {
|
|
148
188
|
if (scopes.has('tools')) {
|
|
149
|
-
await builders.tools(pkg, config,
|
|
189
|
+
await builders.tools(pkg, config, options);
|
|
150
190
|
}
|
|
151
191
|
if (scopes.has('interactions')) {
|
|
152
|
-
await builders.interactions(pkg, config,
|
|
192
|
+
await builders.interactions(pkg, config, options);
|
|
153
193
|
}
|
|
154
194
|
if (scopes.has('types')) {
|
|
155
|
-
await builders.types(pkg, config,
|
|
195
|
+
await builders.types(pkg, config, options);
|
|
156
196
|
}
|
|
157
197
|
if (scopes.has('processes')) {
|
|
158
|
-
await builders.processes(pkg, config,
|
|
198
|
+
await builders.processes(pkg, config, options);
|
|
199
|
+
}
|
|
200
|
+
if (scopes.has('views')) {
|
|
201
|
+
await builders.views(pkg, config, options);
|
|
159
202
|
}
|
|
160
203
|
if (scopes.has('templates')) {
|
|
161
|
-
await builders.templates(pkg, config,
|
|
204
|
+
await builders.templates(pkg, config, options);
|
|
205
|
+
}
|
|
206
|
+
if (scopes.has('dashboards')) {
|
|
207
|
+
await builders.dashboards(pkg, config, options);
|
|
162
208
|
}
|
|
163
209
|
if (scopes.has('widgets')) {
|
|
164
|
-
await builders.widgets(pkg, config,
|
|
210
|
+
await builders.widgets(pkg, config, options);
|
|
165
211
|
}
|
|
166
212
|
if (scopes.has('ui')) {
|
|
167
|
-
await builders.ui(pkg, config,
|
|
213
|
+
await builders.ui(pkg, config, options);
|
|
168
214
|
}
|
|
169
215
|
if (scopes.has('settings')) {
|
|
170
|
-
await builders.settings(pkg, config,
|
|
216
|
+
await builders.settings(pkg, config, options);
|
|
171
217
|
}
|
|
172
218
|
if (scopes.has('activities')) {
|
|
173
|
-
await builders.activities(pkg, config,
|
|
219
|
+
await builders.activities(pkg, config, options);
|
|
174
220
|
}
|
|
175
221
|
}
|
|
176
222
|
|
|
223
|
+
return pkg;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function handlePackageRequest(c: Context, config: ToolServerConfig) {
|
|
227
|
+
const pkg = await buildAppPackage(config, {
|
|
228
|
+
scope: c.req.query('scope') || 'all',
|
|
229
|
+
origin: new URL(c.req.url).origin,
|
|
230
|
+
toolUseContext: await getRequestPayload<ToolUseContext>(c),
|
|
231
|
+
});
|
|
177
232
|
return c.json(pkg);
|
|
178
233
|
}
|
|
179
234
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { ContentTypesCollection } from '../ContentTypesCollection.js';
|
|
4
|
+
import { createContentTypesRoute } from './content-types.js';
|
|
5
|
+
import type { ToolServerConfig } from './types.js';
|
|
6
|
+
|
|
7
|
+
function makeApp(config: ToolServerConfig): Hono {
|
|
8
|
+
const app = new Hono();
|
|
9
|
+
createContentTypesRoute(app, '/api/types', config);
|
|
10
|
+
return app;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const config = {
|
|
14
|
+
types: [
|
|
15
|
+
new ContentTypesCollection({
|
|
16
|
+
name: 'contracts',
|
|
17
|
+
types: [{ name: 'contract' }, { name: 'review_note' }],
|
|
18
|
+
}),
|
|
19
|
+
new ContentTypesCollection({
|
|
20
|
+
name: 'audit',
|
|
21
|
+
types: [{ name: 'audit_event' }],
|
|
22
|
+
}),
|
|
23
|
+
],
|
|
24
|
+
} satisfies ToolServerConfig;
|
|
25
|
+
|
|
26
|
+
describe('content types route', () => {
|
|
27
|
+
it('lists all types with bare ids (collection is not part of the type identity)', async () => {
|
|
28
|
+
const res = await makeApp(config).request('/api/types');
|
|
29
|
+
expect(res.status).toBe(200);
|
|
30
|
+
const body = (await res.json()) as { types: Array<{ id: string }> };
|
|
31
|
+
expect(body.types.map((t) => t.id)).toEqual(['contract', 'review_note', 'audit_event']);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('resolves the canonical bare type name (the app:<app>:<type> ref convention)', async () => {
|
|
35
|
+
const res = await makeApp(config).request('/api/types/contract');
|
|
36
|
+
expect(res.status).toBe(200);
|
|
37
|
+
const body = (await res.json()) as { id: string; name: string };
|
|
38
|
+
expect(body.id).toBe('contract');
|
|
39
|
+
expect(body.name).toBe('contract');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('still resolves the legacy collection:type alias, returning the bare id', async () => {
|
|
43
|
+
const res = await makeApp(config).request('/api/types/contracts:contract');
|
|
44
|
+
expect(res.status).toBe(200);
|
|
45
|
+
const body = (await res.json()) as { id: string };
|
|
46
|
+
expect(body.id).toBe('contract');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('409s on an ambiguous bare name so the collision is loud, not silent', async () => {
|
|
50
|
+
const ambiguous = {
|
|
51
|
+
types: [
|
|
52
|
+
new ContentTypesCollection({ name: 'a', types: [{ name: 'note' }] }),
|
|
53
|
+
new ContentTypesCollection({ name: 'b', types: [{ name: 'note' }] }),
|
|
54
|
+
],
|
|
55
|
+
} satisfies ToolServerConfig;
|
|
56
|
+
const res = await makeApp(ambiguous).request('/api/types/note');
|
|
57
|
+
expect(res.status).toBe(409);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('404s for an unknown type name', async () => {
|
|
61
|
+
const res = await makeApp(config).request('/api/types/nope');
|
|
62
|
+
expect(res.status).toBe(404);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -10,13 +10,15 @@ import type { ToolServerConfig } from './types.js';
|
|
|
10
10
|
export function createContentTypesRoute(app: Hono, basePath: string, config: ToolServerConfig) {
|
|
11
11
|
const { types = [] } = config;
|
|
12
12
|
|
|
13
|
-
// GET /api/types - Returns all
|
|
13
|
+
// GET /api/types - Returns all content types from all collections.
|
|
14
|
+
// A type's public id is its declared BARE name, verbatim (`app:<app>:<type>` once
|
|
15
|
+
// app-prefixed): collections organize code, they are not part of the type identity.
|
|
14
16
|
app.get(basePath, (c) => {
|
|
15
17
|
const allTypes: InCodeTypeDefinition[] = [];
|
|
16
18
|
|
|
17
19
|
for (const coll of types) {
|
|
18
20
|
for (const type of coll.types) {
|
|
19
|
-
allTypes.push({ ...type, id:
|
|
21
|
+
allTypes.push({ ...type, id: type.name });
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -37,20 +39,51 @@ export function createContentTypesRoute(app: Hono, basePath: string, config: Too
|
|
|
37
39
|
app.route(`${basePath}/${coll.name}`, createContentTypeEndpoints(coll));
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
// GET /api/types/:name - Direct access to content type
|
|
42
|
+
// GET /api/types/:name - Direct access to content type.
|
|
43
|
+
// Canonical form is the BARE type name (`<type>`), matching the portable
|
|
44
|
+
// `app:<app>:<type>` ref convention used by objects.create/search — the
|
|
45
|
+
// collection is an in-code grouping, not part of the type's public id.
|
|
46
|
+
// `<collection>:<type>` is also accepted as a backward-compatible alias.
|
|
41
47
|
app.get(`${basePath}/:name`, async (c) => {
|
|
42
48
|
const name = c.req.param('name');
|
|
43
49
|
const parts = name.split(':');
|
|
50
|
+
if (parts.length === 1) {
|
|
51
|
+
// Match the declared name verbatim; fall back to the toPathName variant for
|
|
52
|
+
// leniency with legacy refs that used the path-mangled form.
|
|
53
|
+
const matches = types.flatMap((coll) =>
|
|
54
|
+
coll.types
|
|
55
|
+
.filter((t) => t.name === name || toPathName(t.name) === name)
|
|
56
|
+
.map((t) => ({ coll, type: t })),
|
|
57
|
+
);
|
|
58
|
+
if (matches.length === 1) {
|
|
59
|
+
return c.json({ ...matches[0].type, id: matches[0].type.name });
|
|
60
|
+
}
|
|
61
|
+
if (matches.length > 1) {
|
|
62
|
+
const colls = matches.map((m) => m.coll.name).join(', ');
|
|
63
|
+
throw new HTTPException(409, {
|
|
64
|
+
message:
|
|
65
|
+
`Ambiguous content type name '${name}': defined in collections ${colls}. ` +
|
|
66
|
+
'Type names must be unique across collections so the portable ' +
|
|
67
|
+
`'app:<app>:${name}' ref resolves; rename one of them, or address it as '<collection>:<type>'.`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
throw new HTTPException(404, {
|
|
71
|
+
message: `No content type found with name: ${name}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
44
74
|
if (parts.length !== 2) {
|
|
45
75
|
throw new HTTPException(400, {
|
|
46
|
-
message: "Invalid content type name. Expected
|
|
76
|
+
message: "Invalid content type name. Expected '<type>' or 'collection:type'",
|
|
47
77
|
});
|
|
48
78
|
}
|
|
49
79
|
const collName = parts[0];
|
|
50
|
-
const typeName =
|
|
51
|
-
const ctype = types
|
|
80
|
+
const typeName = parts[1];
|
|
81
|
+
const ctype = types
|
|
82
|
+
.find((t) => t.name === collName)
|
|
83
|
+
?.types.find((t) => t.name === typeName || toPathName(t.name) === toPathName(typeName));
|
|
52
84
|
if (ctype) {
|
|
53
|
-
|
|
85
|
+
// Alias lookup still returns the canonical bare id.
|
|
86
|
+
return c.json({ ...ctype, id: ctype.name });
|
|
54
87
|
}
|
|
55
88
|
|
|
56
89
|
throw new HTTPException(404, {
|
|
@@ -63,12 +96,12 @@ function createContentTypeEndpoints(coll: ContentTypesCollection): Hono {
|
|
|
63
96
|
const endpoint = new Hono();
|
|
64
97
|
|
|
65
98
|
endpoint.get('/', (c: Context) => {
|
|
66
|
-
return c.json(coll.types.map((t) => ({ ...t, id:
|
|
99
|
+
return c.json(coll.types.map((t) => ({ ...t, id: t.name })));
|
|
67
100
|
});
|
|
68
101
|
|
|
69
102
|
endpoint.get('/:name', (c: Context) => {
|
|
70
103
|
const name = c.req.param('name');
|
|
71
|
-
const ctype = coll.types.find((t) => toPathName(t.name) === name);
|
|
104
|
+
const ctype = coll.types.find((t) => t.name === name || toPathName(t.name) === name);
|
|
72
105
|
if (!ctype) {
|
|
73
106
|
throw new HTTPException(404, {
|
|
74
107
|
message: `No content type found with name: ${name}`,
|
|
@@ -76,7 +109,7 @@ function createContentTypeEndpoints(coll: ContentTypesCollection): Hono {
|
|
|
76
109
|
}
|
|
77
110
|
return c.json({
|
|
78
111
|
...ctype,
|
|
79
|
-
id:
|
|
112
|
+
id: ctype.name,
|
|
80
113
|
});
|
|
81
114
|
});
|
|
82
115
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AppDashboardDefinition } from '@vertesia/common';
|
|
2
|
+
import type { Hono } from 'hono';
|
|
3
|
+
import { HTTPException } from 'hono/http-exception';
|
|
4
|
+
import type { ToolServerConfig } from './types.js';
|
|
5
|
+
|
|
6
|
+
export function createDashboardsRoute(app: Hono, basePath: string, config: ToolServerConfig) {
|
|
7
|
+
const { dashboards = [] } = config;
|
|
8
|
+
|
|
9
|
+
app.get(basePath, (c) => {
|
|
10
|
+
return c.json({
|
|
11
|
+
title: 'All Dashboards',
|
|
12
|
+
description: 'All available dashboard definitions',
|
|
13
|
+
dashboards,
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
app.get(`${basePath}/:id`, (c) => {
|
|
18
|
+
const id = c.req.param('id');
|
|
19
|
+
const dashboard = findDashboard(dashboards, id);
|
|
20
|
+
if (!dashboard) {
|
|
21
|
+
throw new HTTPException(404, {
|
|
22
|
+
message: `No dashboard found with id: ${id}`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return c.json(dashboard);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findDashboard(dashboards: AppDashboardDefinition[], id: string): AppDashboardDefinition | undefined {
|
|
30
|
+
return dashboards.find((dashboard) => dashboard.id === id || dashboard.name === id || dashboard.title === id);
|
|
31
|
+
}
|
package/src/server/types.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { JSONSchema } from '@llumiverse/common';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
AppDashboardDefinition,
|
|
4
|
+
AppUIConfig,
|
|
5
|
+
InCodeProcessDefinition,
|
|
6
|
+
InCodeViewDefinition,
|
|
7
|
+
ProjectConfiguration,
|
|
8
|
+
} from '@vertesia/common';
|
|
3
9
|
import type { Context } from 'hono';
|
|
4
10
|
import type { ActivityCollection } from '../ActivityCollection.js';
|
|
5
11
|
import type { AuthSession } from '../auth.js';
|
|
@@ -63,6 +69,14 @@ export interface ToolServerConfig {
|
|
|
63
69
|
* Process definitions to expose as app-contributed processes.
|
|
64
70
|
*/
|
|
65
71
|
processes?: InCodeProcessDefinition[];
|
|
72
|
+
/**
|
|
73
|
+
* Dashboard definitions to expose as app-contributed dashboards.
|
|
74
|
+
*/
|
|
75
|
+
dashboards?: AppDashboardDefinition[];
|
|
76
|
+
/**
|
|
77
|
+
* View Experiences to expose as app-contributed in-code definitions.
|
|
78
|
+
*/
|
|
79
|
+
views?: InCodeViewDefinition[];
|
|
66
80
|
/**
|
|
67
81
|
* Skill collections to expose
|
|
68
82
|
*/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { InCodeViewDefinition } from '@vertesia/common';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createToolServer } from '../server.js';
|
|
4
|
+
|
|
5
|
+
const view: InCodeViewDefinition = {
|
|
6
|
+
id: 'document-library',
|
|
7
|
+
name: 'document-library',
|
|
8
|
+
title: 'Document Library',
|
|
9
|
+
tags: ['content'],
|
|
10
|
+
definition: {
|
|
11
|
+
name: 'Document Library',
|
|
12
|
+
layout: { mode: 'browse' },
|
|
13
|
+
results: {
|
|
14
|
+
default_display: 'list',
|
|
15
|
+
displays: [
|
|
16
|
+
{
|
|
17
|
+
id: 'list',
|
|
18
|
+
label: 'List',
|
|
19
|
+
type: 'list',
|
|
20
|
+
title: { field: 'name' },
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe('in-code View app package', () => {
|
|
28
|
+
it('exposes Views through the package scope and direct resource route', async () => {
|
|
29
|
+
const app = createToolServer({ views: [view], disableHtml: true });
|
|
30
|
+
const packageResponse = await app.request('/api/package?scope=views');
|
|
31
|
+
const directResponse = await app.request('/api/views/document-library');
|
|
32
|
+
|
|
33
|
+
expect(packageResponse.status).toBe(200);
|
|
34
|
+
expect(await packageResponse.json()).toEqual({ views: [view] });
|
|
35
|
+
expect(directResponse.status).toBe(200);
|
|
36
|
+
expect(await directResponse.json()).toEqual(view);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns 404 for an unknown View', async () => {
|
|
40
|
+
const app = createToolServer({ views: [view], disableHtml: true });
|
|
41
|
+
const response = await app.request('/api/views/missing');
|
|
42
|
+
|
|
43
|
+
expect(response.status).toBe(404);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { InCodeViewDefinition } from '@vertesia/common';
|
|
2
|
+
import type { Hono } from 'hono';
|
|
3
|
+
import { HTTPException } from 'hono/http-exception';
|
|
4
|
+
import type { ToolServerConfig } from './types.js';
|
|
5
|
+
|
|
6
|
+
export function createViewsRoute(app: Hono, basePath: string, config: ToolServerConfig) {
|
|
7
|
+
const { views = [] } = config;
|
|
8
|
+
|
|
9
|
+
app.get(basePath, (c) => {
|
|
10
|
+
return c.json({
|
|
11
|
+
title: 'All Views',
|
|
12
|
+
description: 'All available View Experience definitions',
|
|
13
|
+
views,
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
app.get(`${basePath}/:name`, (c) => {
|
|
18
|
+
const name = c.req.param('name');
|
|
19
|
+
const view = findView(views, name);
|
|
20
|
+
if (!view) {
|
|
21
|
+
throw new HTTPException(404, {
|
|
22
|
+
message: `No View found with name: ${name}`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return c.json(view);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findView(views: InCodeViewDefinition[], name: string): InCodeViewDefinition | undefined {
|
|
30
|
+
return views.find((view) => view.id === name || view.name === name);
|
|
31
|
+
}
|