@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "1.6.2",
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.19",
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.1.2"
28
+ "graphql-request": "7.2.0"
29
29
  }
30
30
  }
@@ -16,14 +16,14 @@ import utils from "./helpers/utils";
16
16
 
17
17
  function escapeHtml(string) {
18
18
  const escape = {
19
- '"': '"',
20
- '&': '&',
21
- "'": ''',
22
- '<': '&lt;',
23
- '>': '&gt;'
19
+ '"': "&quot;",
20
+ "&": "&amp;",
21
+ "'": "&#39;",
22
+ "<": "&lt;",
23
+ ">": "&gt;",
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 = value => Array.isArray(value) ? value : [value].filter(Boolean);
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] = key.endsWith("[]") || Array.isArray(value)
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 => path.normalize(path.join("/", ...filePath.split("/")));
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 && sanitizePath(`${currentPath}/views/${viewsFallback}/${partialPath}`)
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(`Template not found: "${partialPath}"\nSearched in:\n${paths.join('\n')}`);
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
- async createAction([verb, path, resource, views, action, handler], resourceConfig) {
265
- const method = this.getMethod(verb);
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
- // Clean up existing routes with same path/verb
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(routerCtx, next, verb, resource, action);
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(`${ctx.method} "${ctx.rawPath}" -> ${resource}/${action} | ${JSON.stringify(ctx.params)}`);
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) return routerCtx.redirect(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(assigns.afterFilterAssigns, afterResult.assigns || {});
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
- views,
362
- viewsFallback: resourceConfig.viewsFallback,
363
- action,
364
- assigns: fullAssigns
365
- }, this.globalConfig?.helpers || {});
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 => this.createAction(handlerMap, resourceConfig));
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]) => router.header(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
- adapter.parseFormBody({})
133
- ).toEqual({});
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
+ });