@raketa-cloud/admin 1.6.2 → 1.6.3
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/package.json +3 -3
- package/src/HonoAdapter.js +84 -39
- package/src/tests/HonoAdapter.test.js +90 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raketa-cloud/admin",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/",
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
"@repo/typescript-config": "*",
|
|
19
19
|
"@tailwindcss/forms": "0.5.7",
|
|
20
20
|
"@tailwindcss/typography": "0.5.13",
|
|
21
|
-
"@types/node": "22.15.
|
|
21
|
+
"@types/node": "22.15.21",
|
|
22
22
|
"jest": "29.7.0",
|
|
23
23
|
"tailwindcss": "3.4.4",
|
|
24
24
|
"typescript": "5.8.3"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"ejs": "3.1.10",
|
|
28
|
-
"graphql-request": "7.
|
|
28
|
+
"graphql-request": "7.2.0"
|
|
29
29
|
}
|
|
30
30
|
}
|
package/src/HonoAdapter.js
CHANGED
|
@@ -16,14 +16,14 @@ import utils from "./helpers/utils";
|
|
|
16
16
|
|
|
17
17
|
function escapeHtml(string) {
|
|
18
18
|
const escape = {
|
|
19
|
-
'"':
|
|
20
|
-
|
|
21
|
-
"'":
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
'"': """,
|
|
20
|
+
"&": "&",
|
|
21
|
+
"'": "'",
|
|
22
|
+
"<": "<",
|
|
23
|
+
">": ">",
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
return String(string).replace(/["&'<>]/g, char => escape[char]);
|
|
26
|
+
return String(string).replace(/["&'<>]/g, (char) => escape[char]);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const renderError = (message, stack) => {
|
|
@@ -38,7 +38,8 @@ const renderError = (message, stack) => {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
// Convert to array if needed
|
|
41
|
-
const toArray =
|
|
41
|
+
const toArray = (value) =>
|
|
42
|
+
Array.isArray(value) ? value : [value].filter(Boolean);
|
|
42
43
|
|
|
43
44
|
// Handle nested form values
|
|
44
45
|
function putIn(result, key, value) {
|
|
@@ -50,9 +51,8 @@ function putIn(result, key, value) {
|
|
|
50
51
|
|
|
51
52
|
if (isLastSegment) {
|
|
52
53
|
// Set value at final segment
|
|
53
|
-
current[segment] =
|
|
54
|
-
? toArray(value)
|
|
55
|
-
: value;
|
|
54
|
+
current[segment] =
|
|
55
|
+
key.endsWith("[]") || Array.isArray(value) ? toArray(value) : value;
|
|
56
56
|
} else {
|
|
57
57
|
// Create path to next segment
|
|
58
58
|
current[segment] ??= {};
|
|
@@ -62,22 +62,26 @@ function putIn(result, key, value) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Normalize paths for consistent handling
|
|
65
|
-
const sanitizePath = filePath =>
|
|
65
|
+
const sanitizePath = (filePath) =>
|
|
66
|
+
path.normalize(path.join("/", ...filePath.split("/")));
|
|
66
67
|
|
|
67
68
|
// Find template path with fallbacks
|
|
68
69
|
function getPartialPath(partialPath, views, viewsFallback) {
|
|
69
70
|
const paths = [
|
|
70
71
|
sanitizePath(`${process.cwd()}/src/views/${views}/${partialPath}`),
|
|
71
72
|
sanitizePath(`${currentPath}/views/${views}/${partialPath}`),
|
|
72
|
-
viewsFallback &&
|
|
73
|
+
viewsFallback &&
|
|
74
|
+
sanitizePath(`${currentPath}/views/${viewsFallback}/${partialPath}`),
|
|
73
75
|
].filter(Boolean);
|
|
74
76
|
|
|
75
77
|
// Return first existing path
|
|
76
|
-
const existingPath = paths.find(path => existsSync(path));
|
|
78
|
+
const existingPath = paths.find((path) => existsSync(path));
|
|
77
79
|
|
|
78
80
|
if (existingPath) return existingPath;
|
|
79
81
|
|
|
80
|
-
throw new Error(
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Template not found: "${partialPath}"\nSearched in:\n${paths.join("\n")}`
|
|
84
|
+
);
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
// Parse request body based on content type
|
|
@@ -109,6 +113,7 @@ export default class HonoAdapter {
|
|
|
109
113
|
this.globalConfig = config;
|
|
110
114
|
this.globalBeforeFilters = [];
|
|
111
115
|
this.globalAfterFilters = [];
|
|
116
|
+
this.localRouter = [];
|
|
112
117
|
|
|
113
118
|
// TODO: Pass in a path to redirect to after login?
|
|
114
119
|
if (config) {
|
|
@@ -261,21 +266,48 @@ export default class HonoAdapter {
|
|
|
261
266
|
});
|
|
262
267
|
}
|
|
263
268
|
|
|
264
|
-
|
|
265
|
-
|
|
269
|
+
addRoute(verb, path, handler) {
|
|
270
|
+
this.localRouter = this.localRouter.filter((routeItem) => {
|
|
271
|
+
return !(routeItem.verb === verb && routeItem.path === path);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
this.localRouter.push({
|
|
275
|
+
verb,
|
|
276
|
+
path,
|
|
277
|
+
handler,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
link() {
|
|
282
|
+
// Clean up all of hono's routes before linking to the adapter locally build routes
|
|
283
|
+
// this.router.routes = [];
|
|
284
|
+
// this.router.routes.length = 0;
|
|
285
|
+
|
|
286
|
+
this.localRouter.forEach((routeItem) => {
|
|
287
|
+
const method = this.getMethod(routeItem.verb);
|
|
288
|
+
|
|
289
|
+
method(routeItem.path, routeItem.handler);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async createAction(
|
|
294
|
+
[verb, path, resource, views, action, handler],
|
|
295
|
+
resourceConfig
|
|
296
|
+
) {
|
|
266
297
|
const { i18n } = this.globalConfig;
|
|
267
298
|
|
|
268
299
|
Object.keys(i18n.locales).forEach((currentLocale) => {
|
|
269
300
|
const completePath = `/${currentLocale}${path}`;
|
|
270
301
|
|
|
271
|
-
|
|
272
|
-
this.router.routes = this.router.routes.filter(route =>
|
|
273
|
-
route[0] !== verb || route[1] !== completePath
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
method(completePath, async (routerCtx, next) => {
|
|
302
|
+
this.addRoute(verb, completePath, async (routerCtx, next) => {
|
|
277
303
|
// Create context with request data
|
|
278
|
-
const ctx = await this.createContext(
|
|
304
|
+
const ctx = await this.createContext(
|
|
305
|
+
routerCtx,
|
|
306
|
+
next,
|
|
307
|
+
verb,
|
|
308
|
+
resource,
|
|
309
|
+
action
|
|
310
|
+
);
|
|
279
311
|
|
|
280
312
|
// Set context properties
|
|
281
313
|
Object.assign(ctx, {
|
|
@@ -285,7 +317,7 @@ export default class HonoAdapter {
|
|
|
285
317
|
maxOpenRevisions: this.globalConfig?.maxOpenRevisions || 3,
|
|
286
318
|
locale: currentLocale,
|
|
287
319
|
remoteConfig: { ...this.globalConfig.remoteConfig },
|
|
288
|
-
logger: Logger
|
|
320
|
+
logger: Logger,
|
|
289
321
|
});
|
|
290
322
|
|
|
291
323
|
// Build context object
|
|
@@ -301,15 +333,17 @@ export default class HonoAdapter {
|
|
|
301
333
|
resourceConfig,
|
|
302
334
|
adminJwt: ctx.authentication?.token,
|
|
303
335
|
remoteConfig: { ...this.globalConfig.remoteConfig },
|
|
304
|
-
globalConfig: this.globalConfig
|
|
336
|
+
globalConfig: this.globalConfig,
|
|
305
337
|
};
|
|
306
338
|
|
|
307
|
-
ctx.logger.info(
|
|
339
|
+
ctx.logger.info(
|
|
340
|
+
`${ctx.method} "${ctx.rawPath}" -> ${resource}/${action} | ${JSON.stringify(ctx.params)}`
|
|
341
|
+
);
|
|
308
342
|
|
|
309
343
|
const assigns = {
|
|
310
344
|
beforeFilterAssigns: {},
|
|
311
345
|
actionAssigns: {},
|
|
312
|
-
afterFilterAssigns: {}
|
|
346
|
+
afterFilterAssigns: {},
|
|
313
347
|
};
|
|
314
348
|
|
|
315
349
|
// Initialize global context
|
|
@@ -328,7 +362,7 @@ export default class HonoAdapter {
|
|
|
328
362
|
}
|
|
329
363
|
|
|
330
364
|
// Process action handler
|
|
331
|
-
const result = await handler(ctx, resourceConfig) || {};
|
|
365
|
+
const result = (await handler(ctx, resourceConfig)) || {};
|
|
332
366
|
assigns.actionAssigns = result.assigns || {};
|
|
333
367
|
|
|
334
368
|
if (result.headers) this.applyHeaders(result.headers, routerCtx);
|
|
@@ -340,10 +374,14 @@ export default class HonoAdapter {
|
|
|
340
374
|
if (pattern.test(ctx.path)) {
|
|
341
375
|
const afterResult = filter(ctx) || {};
|
|
342
376
|
|
|
343
|
-
if (afterResult.redirect)
|
|
377
|
+
if (afterResult.redirect)
|
|
378
|
+
return routerCtx.redirect(afterResult.redirect);
|
|
344
379
|
if (afterResult.json) return routerCtx.json(afterResult.json);
|
|
345
380
|
|
|
346
|
-
Object.assign(
|
|
381
|
+
Object.assign(
|
|
382
|
+
assigns.afterFilterAssigns,
|
|
383
|
+
afterResult.assigns || {}
|
|
384
|
+
);
|
|
347
385
|
}
|
|
348
386
|
}
|
|
349
387
|
|
|
@@ -353,16 +391,19 @@ export default class HonoAdapter {
|
|
|
353
391
|
...assigns.beforeFilterAssigns,
|
|
354
392
|
...assigns.actionAssigns,
|
|
355
393
|
...assigns.afterFilterAssigns,
|
|
356
|
-
_context
|
|
394
|
+
_context,
|
|
357
395
|
};
|
|
358
396
|
|
|
359
397
|
// Render the view
|
|
360
|
-
const rendered = this.renderAction(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
398
|
+
const rendered = this.renderAction(
|
|
399
|
+
{
|
|
400
|
+
views,
|
|
401
|
+
viewsFallback: resourceConfig.viewsFallback,
|
|
402
|
+
action,
|
|
403
|
+
assigns: fullAssigns,
|
|
404
|
+
},
|
|
405
|
+
this.globalConfig?.helpers || {}
|
|
406
|
+
);
|
|
366
407
|
|
|
367
408
|
return routerCtx.html(rendered);
|
|
368
409
|
});
|
|
@@ -371,7 +412,9 @@ export default class HonoAdapter {
|
|
|
371
412
|
|
|
372
413
|
// Single method to handle all route types
|
|
373
414
|
routeCollection(mapping, resourceConfig) {
|
|
374
|
-
mapping.forEach(handlerMap =>
|
|
415
|
+
mapping.forEach((handlerMap) =>
|
|
416
|
+
this.createAction(handlerMap, resourceConfig)
|
|
417
|
+
);
|
|
375
418
|
}
|
|
376
419
|
|
|
377
420
|
// Use the same implementation for all route types
|
|
@@ -391,7 +434,9 @@ export default class HonoAdapter {
|
|
|
391
434
|
|
|
392
435
|
applyHeaders(headers, router) {
|
|
393
436
|
if (!headers) return;
|
|
394
|
-
Object.entries(headers).forEach(([key, value]) =>
|
|
437
|
+
Object.entries(headers).forEach(([key, value]) =>
|
|
438
|
+
router.header(key, value)
|
|
439
|
+
);
|
|
395
440
|
}
|
|
396
441
|
|
|
397
442
|
formatQuery(queries) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import HonoAdapter from "../HonoAdapter";
|
|
2
|
+
import RaketaContext from "../RaketaContext";
|
|
2
3
|
|
|
3
4
|
class HonoMock {
|
|
4
5
|
constructor() {
|
|
5
6
|
this.router = {
|
|
6
|
-
routes: []
|
|
7
|
+
routes: [],
|
|
7
8
|
};
|
|
8
9
|
this.get = jest.fn();
|
|
9
10
|
this.post = jest.fn();
|
|
@@ -11,18 +12,19 @@ class HonoMock {
|
|
|
11
12
|
this.delete = jest.fn();
|
|
12
13
|
this.use = jest.fn();
|
|
13
14
|
this.header = jest.fn();
|
|
15
|
+
// this.routes = [];
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
describe("HonoAdapter", () => {
|
|
18
20
|
let app;
|
|
19
21
|
let adapter;
|
|
20
|
-
|
|
22
|
+
|
|
21
23
|
beforeEach(() => {
|
|
22
24
|
app = new HonoMock();
|
|
23
25
|
adapter = new HonoAdapter(app);
|
|
24
26
|
});
|
|
25
|
-
|
|
27
|
+
|
|
26
28
|
// Method tests
|
|
27
29
|
test("getMethod returns Hono's get for invalid verbs", () => {
|
|
28
30
|
expect(adapter.getMethod("not existing")).toBe(app.get);
|
|
@@ -78,58 +80,126 @@ describe("HonoAdapter", () => {
|
|
|
78
80
|
},
|
|
79
81
|
});
|
|
80
82
|
});
|
|
81
|
-
|
|
83
|
+
|
|
82
84
|
test("setGlobalContext sets the global context function", () => {
|
|
83
85
|
const newCtx = () => ({ test: true });
|
|
84
86
|
adapter.setGlobalContext(newCtx);
|
|
85
87
|
expect(adapter.globalCtx).toBe(newCtx);
|
|
86
88
|
});
|
|
87
|
-
|
|
89
|
+
|
|
88
90
|
test("applyHeaders applies headers to router context", () => {
|
|
89
91
|
const headers = {
|
|
90
92
|
"Content-Type": "application/json",
|
|
91
|
-
"X-Custom-Header": "test-value"
|
|
93
|
+
"X-Custom-Header": "test-value",
|
|
92
94
|
};
|
|
93
|
-
|
|
95
|
+
|
|
94
96
|
adapter.applyHeaders(headers, app);
|
|
95
|
-
|
|
97
|
+
|
|
96
98
|
expect(app.header).toHaveBeenCalledTimes(2);
|
|
97
99
|
expect(app.header).toHaveBeenCalledWith("Content-Type", "application/json");
|
|
98
100
|
expect(app.header).toHaveBeenCalledWith("X-Custom-Header", "test-value");
|
|
99
101
|
});
|
|
100
|
-
|
|
102
|
+
|
|
101
103
|
test("applyHeaders does nothing when headers is undefined", () => {
|
|
102
104
|
adapter.applyHeaders(undefined, app);
|
|
103
105
|
expect(app.header).not.toHaveBeenCalled();
|
|
104
106
|
});
|
|
105
|
-
|
|
107
|
+
|
|
106
108
|
test("formatQuery formats query parameters correctly", () => {
|
|
107
109
|
const queries = {
|
|
108
110
|
single: ["value"],
|
|
109
|
-
multiple: ["one", "two"]
|
|
111
|
+
multiple: ["one", "two"],
|
|
110
112
|
};
|
|
111
|
-
|
|
113
|
+
|
|
112
114
|
const result = adapter.formatQuery(queries);
|
|
113
|
-
|
|
115
|
+
|
|
114
116
|
expect(result).toEqual({
|
|
115
117
|
single: "value",
|
|
116
|
-
multiple: ["one", "two"]
|
|
118
|
+
multiple: ["one", "two"],
|
|
117
119
|
});
|
|
118
120
|
});
|
|
119
121
|
|
|
120
122
|
test("parseFormBody handles array values", () => {
|
|
121
123
|
expect(
|
|
122
124
|
adapter.parseFormBody({
|
|
123
|
-
"tags[]": ["javascript", "react", "node"]
|
|
125
|
+
"tags[]": ["javascript", "react", "node"],
|
|
124
126
|
})
|
|
125
127
|
).toEqual({
|
|
126
|
-
tags: ["javascript", "react", "node"]
|
|
128
|
+
tags: ["javascript", "react", "node"],
|
|
127
129
|
});
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
test("parseFormBody handles empty values", () => {
|
|
131
|
-
expect(
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
expect(adapter.parseFormBody({})).toEqual({});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("renderLayout renders an existing layout template", async () => {
|
|
137
|
+
const result = await adapter.renderLayout(
|
|
138
|
+
{
|
|
139
|
+
_context: {
|
|
140
|
+
layout: {
|
|
141
|
+
sidebar: [],
|
|
142
|
+
},
|
|
143
|
+
path: "/en/pages",
|
|
144
|
+
locale: "en",
|
|
145
|
+
locales: {
|
|
146
|
+
en: {
|
|
147
|
+
flag: "",
|
|
148
|
+
name: "English",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
globalConfig: {
|
|
152
|
+
cloud: {
|
|
153
|
+
images_api_key: "api-key",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
"<h1>Hi</h1>"
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(typeof result).toBe("string");
|
|
162
|
+
expect(result.length).toBeGreaterThan(0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("addRouote - adds a route to the local router", () => {
|
|
166
|
+
expect(adapter.localRouter).toEqual([]);
|
|
167
|
+
|
|
168
|
+
const handler = () => {};
|
|
169
|
+
|
|
170
|
+
adapter.addRoute("GET", "/en/pages", handler);
|
|
171
|
+
|
|
172
|
+
expect(adapter.localRouter).toEqual([
|
|
173
|
+
{ verb: "GET", path: "/en/pages", handler },
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("addRouote - overrides routes if they already exist in the local router", () => {
|
|
178
|
+
expect(adapter.localRouter).toEqual([]);
|
|
179
|
+
|
|
180
|
+
const handler = () => {};
|
|
181
|
+
const handler2 = () => {};
|
|
182
|
+
|
|
183
|
+
adapter.addRoute("GET", "/en/pages", handler);
|
|
184
|
+
|
|
185
|
+
expect(adapter.localRouter).toEqual([
|
|
186
|
+
{ verb: "GET", path: "/en/pages", handler },
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
adapter.addRoute("GET", "/en/pages", handler2);
|
|
190
|
+
|
|
191
|
+
expect(adapter.localRouter).toEqual([
|
|
192
|
+
{ verb: "GET", path: "/en/pages", handler: handler2 },
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("link links local routes with the app", () => {
|
|
197
|
+
const handler = () => {};
|
|
198
|
+
adapter.addRoute("GET", "/en/pages", handler);
|
|
199
|
+
|
|
200
|
+
adapter.link();
|
|
201
|
+
|
|
202
|
+
expect(app.get).toHaveBeenCalledTimes(1);
|
|
203
|
+
expect(app.get).toHaveBeenCalledWith("/en/pages", handler);
|
|
134
204
|
});
|
|
135
|
-
});
|
|
205
|
+
});
|