@jskit-ai/kernel 0.1.37 → 0.1.39
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/client/index.d.ts +1 -0
- package/client/moduleBootstrap.js +4 -0
- package/client/moduleBootstrap.test.js +13 -0
- package/client/shellBootstrap.js +2 -0
- package/client/shellBootstrap.test.js +3 -0
- package/package.json +2 -1
- package/server/registries/bootstrapPayloadContributorRegistry.js +15 -2
- package/server/runtime/bootstrapContributors.test.js +52 -0
- package/shared/support/visibility.js +38 -1
- package/shared/support/visibility.test.js +19 -1
- package/shared/surface/apiPaths.js +74 -1
- package/shared/surface/apiPaths.test.js +31 -0
- package/shared/surface/index.js +4 -0
package/client/index.d.ts
CHANGED
|
@@ -482,6 +482,7 @@ function normalizeClientModuleEntries(clientModules) {
|
|
|
482
482
|
function createClientRuntimeApp({
|
|
483
483
|
profile = "client",
|
|
484
484
|
app,
|
|
485
|
+
pinia = null,
|
|
485
486
|
router,
|
|
486
487
|
env,
|
|
487
488
|
logger,
|
|
@@ -496,6 +497,7 @@ function createClientRuntimeApp({
|
|
|
496
497
|
runtimeApp.instance("jskit.client.runtime.app", runtimeApp);
|
|
497
498
|
runtimeApp.instance("jskit.client.router", router || null);
|
|
498
499
|
runtimeApp.instance("jskit.client.vue.app", app || null);
|
|
500
|
+
runtimeApp.instance("jskit.client.pinia", pinia);
|
|
499
501
|
runtimeApp.instance("jskit.client.env", isRecord(env) ? { ...env } : {});
|
|
500
502
|
runtimeApp.instance("jskit.client.surface.runtime", surfaceRuntime || null);
|
|
501
503
|
runtimeApp.instance("jskit.client.surface.mode", String(surfaceMode || "").trim());
|
|
@@ -507,6 +509,7 @@ function createClientRuntimeApp({
|
|
|
507
509
|
async function bootClientModules({
|
|
508
510
|
clientModules = [],
|
|
509
511
|
app,
|
|
512
|
+
pinia = null,
|
|
510
513
|
router,
|
|
511
514
|
surfaceRuntime,
|
|
512
515
|
surfaceMode,
|
|
@@ -525,6 +528,7 @@ async function bootClientModules({
|
|
|
525
528
|
const runtimeApp = createClientRuntimeApp({
|
|
526
529
|
profile: String(surfaceRuntime.normalizeSurfaceMode(surfaceMode) || "client"),
|
|
527
530
|
app,
|
|
531
|
+
pinia,
|
|
528
532
|
router,
|
|
529
533
|
env,
|
|
530
534
|
logger: log,
|
|
@@ -112,11 +112,14 @@ test("bootClientModules registers descriptor and clientRoutes with providers onl
|
|
|
112
112
|
const surfaceRuntime = createSurfaceRuntimeFixture();
|
|
113
113
|
const events = [];
|
|
114
114
|
const loginComponent = {};
|
|
115
|
+
const pinia = { id: "pinia-instance" };
|
|
116
|
+
const implicitPinia = { id: "implicit-vue-global-pinia" };
|
|
115
117
|
class ExampleClientProvider {
|
|
116
118
|
static id = "example.client";
|
|
117
119
|
register(app) {
|
|
118
120
|
events.push("register");
|
|
119
121
|
app.instance("example.value", 42);
|
|
122
|
+
app.instance("example.pinia", app.make("jskit.client.pinia"));
|
|
120
123
|
}
|
|
121
124
|
boot() {
|
|
122
125
|
events.push("boot");
|
|
@@ -179,6 +182,14 @@ test("bootClientModules registers descriptor and clientRoutes with providers onl
|
|
|
179
182
|
}
|
|
180
183
|
}
|
|
181
184
|
],
|
|
185
|
+
app: {
|
|
186
|
+
config: {
|
|
187
|
+
globalProperties: {
|
|
188
|
+
$pinia: implicitPinia
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
pinia,
|
|
182
193
|
router,
|
|
183
194
|
surfaceRuntime,
|
|
184
195
|
surfaceMode: "all",
|
|
@@ -194,6 +205,8 @@ test("bootClientModules registers descriptor and clientRoutes with providers onl
|
|
|
194
205
|
assert.equal(router.routes[1].path, "/auth/login");
|
|
195
206
|
assert.equal(router.routes[1].component, loginComponent);
|
|
196
207
|
assert.equal(result.runtimeApp.make("example.value"), 42);
|
|
208
|
+
assert.equal(result.runtimeApp.make("example.pinia"), pinia);
|
|
209
|
+
assert.notEqual(result.runtimeApp.make("example.pinia"), implicitPinia);
|
|
197
210
|
});
|
|
198
211
|
|
|
199
212
|
test("bootClientModules does not auto-discover providers from module exports", async () => {
|
package/client/shellBootstrap.js
CHANGED
|
@@ -108,6 +108,7 @@ async function bootstrapClientShellApp({
|
|
|
108
108
|
rootComponent,
|
|
109
109
|
appConfig = {},
|
|
110
110
|
appPlugins = [],
|
|
111
|
+
pinia = null,
|
|
111
112
|
router,
|
|
112
113
|
bootClientModules,
|
|
113
114
|
surfaceRuntime,
|
|
@@ -158,6 +159,7 @@ async function bootstrapClientShellApp({
|
|
|
158
159
|
|
|
159
160
|
const clientBootstrap = await bootClientModules({
|
|
160
161
|
app,
|
|
162
|
+
pinia,
|
|
161
163
|
router,
|
|
162
164
|
surfaceRuntime,
|
|
163
165
|
surfaceMode,
|
|
@@ -85,6 +85,7 @@ test("bootstrapClientShellApp boots modules, reinstalls fallback route, and moun
|
|
|
85
85
|
const logs = [];
|
|
86
86
|
const surfaceRuntime = createSurfaceRuntimeFixture();
|
|
87
87
|
const plugin = { name: "vuetify-like-plugin" };
|
|
88
|
+
const pinia = { id: "pinia-instance" };
|
|
88
89
|
const fallbackRoute = {
|
|
89
90
|
name: "not-found",
|
|
90
91
|
path: "/:pathMatch(.*)*",
|
|
@@ -140,10 +141,12 @@ test("bootstrapClientShellApp boots modules, reinstalls fallback route, and moun
|
|
|
140
141
|
}
|
|
141
142
|
},
|
|
142
143
|
appPlugins: [plugin],
|
|
144
|
+
pinia,
|
|
143
145
|
router,
|
|
144
146
|
bootClientModules: async (context) => {
|
|
145
147
|
calls.push("bootClientModules");
|
|
146
148
|
assert.equal(context.app, app);
|
|
149
|
+
assert.equal(context.pinia, pinia);
|
|
147
150
|
assert.equal(context.router, router);
|
|
148
151
|
assert.equal(typeof context.logger.debug, "function");
|
|
149
152
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/kernel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"typebox": "^1.0.81"
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"./server/actions": "./server/actions/index.js",
|
|
20
20
|
"./server/http": "./server/http/index.js",
|
|
21
21
|
"./server/platform": "./server/platform/index.js",
|
|
22
|
+
"./server/registries": "./server/registries/index.js",
|
|
22
23
|
"./server/runtime": "./server/runtime/index.js",
|
|
23
24
|
"./server/runtime/errors": "./server/runtime/errors.js",
|
|
24
25
|
"./server/runtime/requestUrl": "./server/runtime/requestUrl.js",
|
|
@@ -9,8 +9,21 @@ function registerBootstrapPayloadContributor(app, token, factory) {
|
|
|
9
9
|
|
|
10
10
|
function resolveBootstrapPayloadContributors(scope) {
|
|
11
11
|
return resolveTaggedEntries(scope, "jskit.runtime.bootstrap.payloadContributors")
|
|
12
|
-
.map((entry) =>
|
|
13
|
-
|
|
12
|
+
.map((entry, index) => ({
|
|
13
|
+
contributor: normalizeContributorEntry(entry),
|
|
14
|
+
index
|
|
15
|
+
}))
|
|
16
|
+
.filter((entry) => Boolean(entry.contributor))
|
|
17
|
+
.sort((left, right) => {
|
|
18
|
+
const leftOrder = Number.isFinite(left.contributor?.order) ? Number(left.contributor.order) : 0;
|
|
19
|
+
const rightOrder = Number.isFinite(right.contributor?.order) ? Number(right.contributor.order) : 0;
|
|
20
|
+
if (leftOrder !== rightOrder) {
|
|
21
|
+
return leftOrder - rightOrder;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return left.index - right.index;
|
|
25
|
+
})
|
|
26
|
+
.map((entry) => entry.contributor);
|
|
14
27
|
}
|
|
15
28
|
|
|
16
29
|
async function resolveBootstrapPayload(scope, context = {}) {
|
|
@@ -87,6 +87,58 @@ test("resolveBootstrapPayload applies contributors in deterministic token order"
|
|
|
87
87
|
});
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test("resolveBootstrapPayload honors explicit contributor order before token order", async () => {
|
|
91
|
+
const app = createContainer();
|
|
92
|
+
const calls = [];
|
|
93
|
+
|
|
94
|
+
registerBootstrapPayloadContributor(app, "test.bootstrap.alpha", () => ({
|
|
95
|
+
contributorId: "alpha",
|
|
96
|
+
order: 200,
|
|
97
|
+
contribute({ payload }) {
|
|
98
|
+
calls.push({
|
|
99
|
+
contributorId: "alpha",
|
|
100
|
+
payload
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
alpha: true
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
registerBootstrapPayloadContributor(app, "test.bootstrap.zeta", () => ({
|
|
109
|
+
contributorId: "zeta",
|
|
110
|
+
order: 100,
|
|
111
|
+
contribute({ payload }) {
|
|
112
|
+
calls.push({
|
|
113
|
+
contributorId: "zeta",
|
|
114
|
+
payload
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
zeta: true
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
const payload = await resolveBootstrapPayload(app);
|
|
123
|
+
|
|
124
|
+
assert.deepEqual(calls, [
|
|
125
|
+
{
|
|
126
|
+
contributorId: "zeta",
|
|
127
|
+
payload: {}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
contributorId: "alpha",
|
|
131
|
+
payload: {
|
|
132
|
+
zeta: true
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]);
|
|
136
|
+
assert.deepEqual(payload, {
|
|
137
|
+
zeta: true,
|
|
138
|
+
alpha: true
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
90
142
|
test("resolveBootstrapPayload ignores non-object contributions", async () => {
|
|
91
143
|
const app = createContainer();
|
|
92
144
|
|
|
@@ -3,6 +3,15 @@ import { ROUTE_VISIBILITY_PUBLIC, ROUTE_VISIBILITY_USER } from "./policies.js";
|
|
|
3
3
|
|
|
4
4
|
const ROUTE_VISIBILITY_LEVELS = Object.freeze([ROUTE_VISIBILITY_PUBLIC, ROUTE_VISIBILITY_USER]);
|
|
5
5
|
const ROUTE_VISIBILITY_LEVEL_SET = new Set(ROUTE_VISIBILITY_LEVELS);
|
|
6
|
+
const ROUTE_VISIBILITY_WORKSPACE = "workspace";
|
|
7
|
+
const ROUTE_VISIBILITY_WORKSPACE_USER = "workspace_user";
|
|
8
|
+
const ROUTE_VISIBILITY_TOKENS = Object.freeze([
|
|
9
|
+
ROUTE_VISIBILITY_PUBLIC,
|
|
10
|
+
ROUTE_VISIBILITY_USER,
|
|
11
|
+
ROUTE_VISIBILITY_WORKSPACE,
|
|
12
|
+
ROUTE_VISIBILITY_WORKSPACE_USER
|
|
13
|
+
]);
|
|
14
|
+
const ROUTE_VISIBILITY_TOKEN_SET = new Set(ROUTE_VISIBILITY_TOKENS);
|
|
6
15
|
|
|
7
16
|
function normalizeRouteVisibilityToken(value, { fallback = ROUTE_VISIBILITY_PUBLIC } = {}) {
|
|
8
17
|
const normalized = normalizeText(value).toLowerCase();
|
|
@@ -34,6 +43,22 @@ function normalizeRouteVisibility(value, { fallback = ROUTE_VISIBILITY_PUBLIC }
|
|
|
34
43
|
return ROUTE_VISIBILITY_PUBLIC;
|
|
35
44
|
}
|
|
36
45
|
|
|
46
|
+
function checkRouteVisibility(value, { context = "checkRouteVisibility" } = {}) {
|
|
47
|
+
const normalized = normalizeRouteVisibilityToken(value);
|
|
48
|
+
if (ROUTE_VISIBILITY_TOKEN_SET.has(normalized)) {
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new TypeError(`${context} must be one of: ${ROUTE_VISIBILITY_TOKENS.join(", ")}.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isWorkspaceRouteVisibility(value = "") {
|
|
56
|
+
const normalized = checkRouteVisibility(value, {
|
|
57
|
+
context: "isWorkspaceRouteVisibility visibility"
|
|
58
|
+
});
|
|
59
|
+
return normalized === ROUTE_VISIBILITY_WORKSPACE || normalized === ROUTE_VISIBILITY_WORKSPACE_USER;
|
|
60
|
+
}
|
|
61
|
+
|
|
37
62
|
function normalizeVisibilityScopeKind(value) {
|
|
38
63
|
const normalized = normalizeText(value).toLowerCase();
|
|
39
64
|
return normalized || null;
|
|
@@ -53,4 +78,16 @@ function normalizeVisibilityContext(value = {}) {
|
|
|
53
78
|
});
|
|
54
79
|
}
|
|
55
80
|
|
|
56
|
-
export {
|
|
81
|
+
export {
|
|
82
|
+
ROUTE_VISIBILITY_LEVELS,
|
|
83
|
+
ROUTE_VISIBILITY_PUBLIC,
|
|
84
|
+
ROUTE_VISIBILITY_USER,
|
|
85
|
+
ROUTE_VISIBILITY_WORKSPACE,
|
|
86
|
+
ROUTE_VISIBILITY_WORKSPACE_USER,
|
|
87
|
+
ROUTE_VISIBILITY_TOKENS,
|
|
88
|
+
normalizeRouteVisibilityToken,
|
|
89
|
+
normalizeRouteVisibility,
|
|
90
|
+
checkRouteVisibility,
|
|
91
|
+
isWorkspaceRouteVisibility,
|
|
92
|
+
normalizeVisibilityContext
|
|
93
|
+
};
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
checkRouteVisibility,
|
|
5
|
+
isWorkspaceRouteVisibility,
|
|
6
|
+
normalizeRouteVisibilityToken,
|
|
7
|
+
normalizeRouteVisibility,
|
|
8
|
+
normalizeVisibilityContext
|
|
9
|
+
} from "./visibility.js";
|
|
4
10
|
|
|
5
11
|
test("normalizeRouteVisibility keeps kernel core visibility contract", () => {
|
|
6
12
|
assert.equal(normalizeRouteVisibility("PUBLIC"), "public");
|
|
@@ -18,6 +24,18 @@ test("normalizeRouteVisibilityToken normalizes visibility tokens for module-leve
|
|
|
18
24
|
assert.equal(normalizeRouteVisibilityToken("", { fallback: "workspace" }), "workspace");
|
|
19
25
|
});
|
|
20
26
|
|
|
27
|
+
test("checkRouteVisibility accepts workspace visibility tokens", () => {
|
|
28
|
+
assert.equal(checkRouteVisibility("workspace"), "workspace");
|
|
29
|
+
assert.equal(checkRouteVisibility("workspace_user"), "workspace_user");
|
|
30
|
+
assert.throws(() => checkRouteVisibility("invalid"), /must be one of/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("isWorkspaceRouteVisibility matches only workspace-scoped tokens", () => {
|
|
34
|
+
assert.equal(isWorkspaceRouteVisibility("workspace"), true);
|
|
35
|
+
assert.equal(isWorkspaceRouteVisibility("workspace_user"), true);
|
|
36
|
+
assert.equal(isWorkspaceRouteVisibility("public"), false);
|
|
37
|
+
});
|
|
38
|
+
|
|
21
39
|
test("normalizeVisibilityContext normalizes mode and owner identifiers", () => {
|
|
22
40
|
assert.deepEqual(normalizeVisibilityContext({ visibility: "user", userId: "7" }), {
|
|
23
41
|
visibility: "user",
|
|
@@ -7,6 +7,76 @@ const API_DOCS_PATH = `${API_PREFIX}/docs`;
|
|
|
7
7
|
const API_REALTIME_PATH = `${API_PREFIX}/realtime`;
|
|
8
8
|
const VERSIONED_API_PATH_PATTERN = /^\/api\/v[0-9]+(?:$|\/)/;
|
|
9
9
|
|
|
10
|
+
function normalizeRouteTemplateParams(params = null) {
|
|
11
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
return params;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function materializeRouteTemplate(routeTemplate = "/", { params = {}, strictParams = true, context = "materializeRouteTemplate" } = {}) {
|
|
18
|
+
const normalizedTemplate = normalizePathname(routeTemplate);
|
|
19
|
+
const normalizedParams = normalizeRouteTemplateParams(params);
|
|
20
|
+
const missingParams = new Set();
|
|
21
|
+
const outputPath = String(normalizedTemplate || "/").replace(/:([A-Za-z0-9_]+)/g, (_full, rawName) => {
|
|
22
|
+
const paramName = String(rawName || "").trim();
|
|
23
|
+
const rawValue = normalizedParams[paramName];
|
|
24
|
+
const paramValue = String(Array.isArray(rawValue) ? rawValue[0] : rawValue ?? "").trim();
|
|
25
|
+
if (!paramValue) {
|
|
26
|
+
missingParams.add(paramName);
|
|
27
|
+
return `:${paramName}`;
|
|
28
|
+
}
|
|
29
|
+
return encodeURIComponent(paramValue);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (strictParams && missingParams.size > 0) {
|
|
33
|
+
throw new Error(`${context} missing required route params: ${[...missingParams].sort().join(", ")}.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return outputPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveScopedRouteBase(routeBase = "/") {
|
|
40
|
+
const normalizedRouteBase = normalizePathname(routeBase);
|
|
41
|
+
const segments = normalizedRouteBase.split("/").filter(Boolean);
|
|
42
|
+
let lastDynamicIndex = -1;
|
|
43
|
+
|
|
44
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
45
|
+
const segment = segments[index];
|
|
46
|
+
if (segment.startsWith(":") && segment.length > 1) {
|
|
47
|
+
lastDynamicIndex = index;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (lastDynamicIndex < 0) {
|
|
52
|
+
return "/";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `/${segments.slice(0, lastDynamicIndex + 1).join("/")}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveScopedApiBasePath({
|
|
59
|
+
routeBase = "/",
|
|
60
|
+
relativePath = "/",
|
|
61
|
+
params = {},
|
|
62
|
+
strictParams = true
|
|
63
|
+
} = {}) {
|
|
64
|
+
const scopedRouteBase = resolveScopedRouteBase(routeBase);
|
|
65
|
+
const materializedScopeBase = materializeRouteTemplate(scopedRouteBase, {
|
|
66
|
+
params,
|
|
67
|
+
strictParams,
|
|
68
|
+
context: "resolveScopedApiBasePath"
|
|
69
|
+
});
|
|
70
|
+
const basePath = materializedScopeBase === "/" ? API_BASE_PATH : `${API_BASE_PATH}${materializedScopeBase}`;
|
|
71
|
+
const normalizedRelativePath = normalizePathname(relativePath || "/");
|
|
72
|
+
|
|
73
|
+
if (normalizedRelativePath === "/") {
|
|
74
|
+
return basePath;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return `${basePath}${normalizedRelativePath}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
10
80
|
function isApiPath(pathname) {
|
|
11
81
|
return matchesPathPrefix(pathname, API_BASE_PATH);
|
|
12
82
|
}
|
|
@@ -74,11 +144,14 @@ export {
|
|
|
74
144
|
API_PREFIX_SLASH,
|
|
75
145
|
API_DOCS_PATH,
|
|
76
146
|
API_REALTIME_PATH,
|
|
147
|
+
materializeRouteTemplate,
|
|
77
148
|
normalizePathname,
|
|
78
149
|
isApiPath,
|
|
79
150
|
isVersionedApiPath,
|
|
80
151
|
toVersionedApiPath,
|
|
81
152
|
toVersionedApiPrefix,
|
|
82
153
|
buildVersionedApiPath,
|
|
83
|
-
isVersionedApiPrefixMatch
|
|
154
|
+
isVersionedApiPrefixMatch,
|
|
155
|
+
resolveScopedRouteBase,
|
|
156
|
+
resolveScopedApiBasePath
|
|
84
157
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { resolveScopedApiBasePath, resolveScopedRouteBase } from "./apiPaths.js";
|
|
4
|
+
|
|
5
|
+
test("resolveScopedRouteBase keeps the route prefix through the last dynamic segment", () => {
|
|
6
|
+
assert.equal(resolveScopedRouteBase("/"), "/");
|
|
7
|
+
assert.equal(resolveScopedRouteBase("/home"), "/");
|
|
8
|
+
assert.equal(resolveScopedRouteBase("/w/:workspaceSlug"), "/w/:workspaceSlug");
|
|
9
|
+
assert.equal(resolveScopedRouteBase("/w/:workspaceSlug/admin"), "/w/:workspaceSlug");
|
|
10
|
+
assert.equal(resolveScopedRouteBase("/org/:orgId/team/:teamId/admin"), "/org/:orgId/team/:teamId");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("resolveScopedApiBasePath materializes scoped API paths from route templates", () => {
|
|
14
|
+
assert.equal(resolveScopedApiBasePath({ routeBase: "/home", relativePath: "/settings" }), "/api/settings");
|
|
15
|
+
assert.equal(
|
|
16
|
+
resolveScopedApiBasePath({
|
|
17
|
+
routeBase: "/w/:workspaceSlug/admin",
|
|
18
|
+
relativePath: "/members",
|
|
19
|
+
params: { workspaceSlug: "acme" }
|
|
20
|
+
}),
|
|
21
|
+
"/api/w/acme/members"
|
|
22
|
+
);
|
|
23
|
+
assert.throws(
|
|
24
|
+
() =>
|
|
25
|
+
resolveScopedApiBasePath({
|
|
26
|
+
routeBase: "/w/:workspaceSlug/admin",
|
|
27
|
+
relativePath: "/members"
|
|
28
|
+
}),
|
|
29
|
+
/missing required route params/
|
|
30
|
+
);
|
|
31
|
+
});
|