@raketa-cloud/admin 1.6.1 → 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.
Files changed (30) hide show
  1. package/package.json +7 -7
  2. package/src/HonoAdapter.js +84 -39
  3. package/src/cloud/Articles.js +20 -2
  4. package/src/cloud/Events.js +21 -3
  5. package/src/cloud/Pages.js +15 -0
  6. package/src/cloud/documents/articlesDocuments.js +29 -0
  7. package/src/cloud/documents/eventsDocuments.js +36 -0
  8. package/src/cloud/documents/pagesDocuments.js +28 -0
  9. package/src/handlers/TagsAPIHandler.js +11 -0
  10. package/src/handlers/articles/ArticlesAPIHandler.js +15 -0
  11. package/src/handlers/articles/ChangeHandler.js +22 -0
  12. package/src/handlers/events/ChangeHandler.js +22 -0
  13. package/src/handlers/events/EventsAPIHandler.js +15 -0
  14. package/src/handlers/pages/ChangeHandler.js +22 -0
  15. package/src/handlers/pages/PagesAPIHandler.js +15 -0
  16. package/src/helpers/AuthHelper.js +7 -1
  17. package/src/index.js +1 -0
  18. package/src/routeArticlesModule.js +11 -0
  19. package/src/routeEventsModule.js +11 -0
  20. package/src/routePagesModule.js +11 -0
  21. package/src/routeStandard.js +8 -6
  22. package/src/tests/HonoAdapter.test.js +90 -20
  23. package/src/tests/handlers/TagsHandlerAPI.test/index.test.js +38 -0
  24. package/src/tests/handlers/articles/ArticlesAPIHandler.test/index.test.js +43 -0
  25. package/src/tests/handlers/articles/ChangeHandler.test/show.test.js +59 -0
  26. package/src/tests/handlers/events/ChangeHandler.test/show.test.js +59 -0
  27. package/src/tests/handlers/events/EventsAPIHandler.test/index.test.js +39 -0
  28. package/src/tests/handlers/pages/ChangeHandler.test/show.test.js +59 -0
  29. package/src/tests/handlers/pages/PagesAPIHandler.test/index.test.js +39 -0
  30. package/src/tests/helpers/AuthHelper.test.js +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "main": "src/index.js",
5
5
  "files": [
6
6
  "src/",
@@ -12,19 +12,19 @@
12
12
  "prepare": "npx tailwindcss -i ./src/styles/application.css -o ./dist/application.css"
13
13
  },
14
14
  "devDependencies": {
15
- "@babel/core": "7.26.10",
16
- "@babel/preset-env": "7.26.9",
17
- "@babel/preset-typescript": "7.26.0",
15
+ "@babel/core": "7.27.1",
16
+ "@babel/preset-env": "7.27.2",
17
+ "@babel/preset-typescript": "7.27.1",
18
18
  "@repo/typescript-config": "*",
19
19
  "@tailwindcss/forms": "0.5.7",
20
20
  "@tailwindcss/typography": "0.5.13",
21
- "@types/node": "22.13.10",
21
+ "@types/node": "22.15.21",
22
22
  "jest": "29.7.0",
23
23
  "tailwindcss": "3.4.4",
24
- "typescript": "5.8.2"
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,15 +1,16 @@
1
- import { toDateInput, dateToUTCISO8601String } from "../utils";
1
+ import { dateToUTCISO8601String, toDateInput } from "../utils";
2
2
  import RaketaClient from "./RaketaClient";
3
3
  import {
4
4
  ArticleByIdDocument,
5
+ ArticleChangePreviewDocument,
5
6
  ArticlesDocument,
6
7
  ChangeArticleSlugDocument,
7
8
  CloneArticleDocument,
8
9
  CreateArticleDocument,
9
- buildBatchTranslateArticleDocument,
10
10
  DeleteArticleDocument,
11
11
  MiniArticlesDocument,
12
12
  TranslateArticleDocument,
13
+ buildBatchTranslateArticleDocument,
13
14
  } from "./documents/articlesDocuments";
14
15
 
15
16
  export default class Articles extends RaketaClient {
@@ -191,4 +192,21 @@ export default class Articles extends RaketaClient {
191
192
 
192
193
  return { article, errors };
193
194
  }
195
+
196
+ async getChangePreview(articleId, version, locale) {
197
+ const input = {
198
+ articleId,
199
+ version: Number.parseInt(version, 10),
200
+ locale,
201
+ };
202
+
203
+ const { articleChangePreview } = await this.query(
204
+ ArticleChangePreviewDocument,
205
+ {
206
+ input,
207
+ }
208
+ );
209
+
210
+ return articleChangePreview;
211
+ }
194
212
  }
@@ -1,20 +1,21 @@
1
1
  import {
2
+ dateToUTCISO8601String,
3
+ toDateInput,
2
4
  toDateTimeInput,
3
5
  toUTCISO8601String,
4
- toDateInput,
5
- dateToUTCISO8601String,
6
6
  } from "../utils";
7
7
  import RaketaClient from "./RaketaClient";
8
8
  import {
9
9
  ChangeEventSlugDocument,
10
10
  CloneEventDocument,
11
- buildBatchTranslateEventDocument,
12
11
  CreateEventDocument,
13
12
  DeleteEventDocument,
14
13
  EventByIdDocument,
14
+ EventChangePreviewDocument,
15
15
  EventsDocument,
16
16
  MiniEventsDocument,
17
17
  TranslateEventDocument,
18
+ buildBatchTranslateEventDocument,
18
19
  } from "./documents/eventsDocuments";
19
20
 
20
21
  export default class Events extends RaketaClient {
@@ -206,4 +207,21 @@ export default class Events extends RaketaClient {
206
207
 
207
208
  return { event, errors };
208
209
  }
210
+
211
+ async getChangePreview(eventId, version, locale) {
212
+ const input = {
213
+ eventId,
214
+ version: Number.parseInt(version, 10),
215
+ locale,
216
+ };
217
+
218
+ const { eventChangePreview } = await this.query(
219
+ EventChangePreviewDocument,
220
+ {
221
+ input,
222
+ }
223
+ );
224
+
225
+ return eventChangePreview;
226
+ }
209
227
  }
@@ -6,6 +6,7 @@ import {
6
6
  DeletePageDocument,
7
7
  MiniPagesDocument,
8
8
  PageByIdDocument,
9
+ PageChangePreviewDocument,
9
10
  PagesDocument,
10
11
  TranslatePageDocument,
11
12
  buildBatchTranslatePageDocument,
@@ -182,4 +183,18 @@ export default class Pages extends RaketaClient {
182
183
 
183
184
  return { page, errors };
184
185
  }
186
+
187
+ async getChangePreview(pageId, version, locale) {
188
+ const input = {
189
+ pageId,
190
+ version: Number.parseInt(version, 10),
191
+ locale,
192
+ };
193
+
194
+ const { pageChangePreview } = await this.query(PageChangePreviewDocument, {
195
+ input,
196
+ });
197
+
198
+ return pageChangePreview;
199
+ }
185
200
  }
@@ -315,6 +315,34 @@ const UnlockArticleRevisionDocument = gql`
315
315
  }
316
316
  `;
317
317
 
318
+ const ArticleChangePreviewDocument = gql`
319
+ query ($input: ArticleChangePreviewInput!) {
320
+ articleChangePreview(input: $input) {
321
+ article {
322
+ id
323
+ slug
324
+ title
325
+ widgets
326
+ settings
327
+ visibility
328
+ publishAt
329
+ translations
330
+ tags ${tagFragment()}
331
+ websiteSettings
332
+ }
333
+ articleChange {
334
+ version
335
+ title
336
+ description
337
+ operator
338
+ operatorMeta
339
+ source
340
+ insertedAt
341
+ }
342
+ }
343
+ }
344
+ `;
345
+
318
346
  export {
319
347
  ArticleByIdDocument,
320
348
  ArticlesDocument,
@@ -340,4 +368,5 @@ export {
340
368
  buildBatchTranslateArticleDocument,
341
369
  buildBatchStartArticleRevisionDocument,
342
370
  ArticleEditorsDocument,
371
+ ArticleChangePreviewDocument,
343
372
  };
@@ -323,6 +323,41 @@ const UnlockEventRevisionDocument = gql`
323
323
  }
324
324
  `;
325
325
 
326
+ const EventChangePreviewDocument = gql`
327
+ query ($input: EventChangePreviewInput!) {
328
+ eventChangePreview(input: $input) {
329
+ event {
330
+ id
331
+ slug
332
+ title
333
+ widgets
334
+ settings
335
+ visibility
336
+ publishAt
337
+ startsAt
338
+ endsAt
339
+ eventType {
340
+ id
341
+ slug
342
+ title
343
+ }
344
+ translations
345
+ tags ${tagFragment()}
346
+ websiteSettings
347
+ }
348
+ eventChange {
349
+ version
350
+ title
351
+ description
352
+ operator
353
+ operatorMeta
354
+ source
355
+ insertedAt
356
+ }
357
+ }
358
+ }
359
+ `;
360
+
326
361
  export {
327
362
  EventByIdDocument,
328
363
  EventsDocument,
@@ -348,4 +383,5 @@ export {
348
383
  buildBatchTranslateEventDocument,
349
384
  EventHistoryDocument,
350
385
  EventEditorsDocument,
386
+ EventChangePreviewDocument,
351
387
  };
@@ -311,6 +311,33 @@ const UnlockPageRevisionDocument = gql`
311
311
  }
312
312
  `;
313
313
 
314
+ const PageChangePreviewDocument = gql`
315
+ query ($input: PageChangePreviewInput!) {
316
+ pageChangePreview(input: $input) {
317
+ page {
318
+ id
319
+ slug
320
+ title
321
+ widgets
322
+ settings
323
+ visibility
324
+ translations
325
+ tags ${tagFragment()}
326
+ websiteSettings
327
+ }
328
+ pageChange {
329
+ version
330
+ title
331
+ description
332
+ operator
333
+ operatorMeta
334
+ source
335
+ insertedAt
336
+ }
337
+ }
338
+ }
339
+ `;
340
+
314
341
  export {
315
342
  PageByIdDocument,
316
343
  PagesDocument,
@@ -336,4 +363,5 @@ export {
336
363
  PageHistoryDocument,
337
364
  buildBatchStartPageRevisionDocument,
338
365
  PageEditorsDocument,
366
+ PageChangePreviewDocument,
339
367
  };
@@ -0,0 +1,11 @@
1
+ const index = async (ctx) => {
2
+ const items = await ctx.client.tags.list(ctx.query, ctx.locale);
3
+
4
+ return {
5
+ json: {
6
+ ...items,
7
+ },
8
+ };
9
+ };
10
+
11
+ export default { index };
@@ -0,0 +1,15 @@
1
+ const index = async (ctx) => {
2
+ const items = await ctx.client.articles.listAll(
3
+ ctx.query,
4
+ ctx.locale,
5
+ ctx.resourceConfig.articleType
6
+ );
7
+
8
+ return {
9
+ json: {
10
+ ...items,
11
+ },
12
+ };
13
+ };
14
+
15
+ export default { index };
@@ -0,0 +1,22 @@
1
+ const show = async (ctx) => {
2
+ const articleId = ctx.params.parent_id;
3
+ const version = ctx.query.version;
4
+
5
+ const item = await ctx.client.articles.getChangePreview(
6
+ articleId,
7
+ version,
8
+ ctx.locale
9
+ );
10
+
11
+ if (!item) {
12
+ throw new Error(`Missing ${ctx.resourceConfig.articleType} #${articleId}`);
13
+ }
14
+
15
+ return {
16
+ json: {
17
+ ...item,
18
+ },
19
+ };
20
+ };
21
+
22
+ export default { show };
@@ -0,0 +1,22 @@
1
+ const show = async (ctx) => {
2
+ const eventId = ctx.params.parent_id;
3
+ const version = ctx.query.version;
4
+
5
+ const item = await ctx.client.events.getChangePreview(
6
+ eventId,
7
+ version,
8
+ ctx.locale
9
+ );
10
+
11
+ if (!item) {
12
+ throw new Error(`Missing ${ctx.resourceConfig.eventType} #${eventId}`);
13
+ }
14
+
15
+ return {
16
+ json: {
17
+ ...item,
18
+ },
19
+ };
20
+ };
21
+
22
+ export default { show };
@@ -0,0 +1,15 @@
1
+ const index = async (ctx) => {
2
+ const items = await ctx.client.events.listAll(
3
+ ctx.query,
4
+ ctx.locale,
5
+ ctx.resourceConfig.eventType
6
+ );
7
+
8
+ return {
9
+ json: {
10
+ ...items,
11
+ },
12
+ };
13
+ };
14
+
15
+ export default { index };
@@ -0,0 +1,22 @@
1
+ const show = async (ctx) => {
2
+ const pageId = ctx.params.parent_id;
3
+ const version = ctx.query.version;
4
+
5
+ const item = await ctx.client.pages.getChangePreview(
6
+ pageId,
7
+ version,
8
+ ctx.locale
9
+ );
10
+
11
+ if (!item) {
12
+ throw new Error(`Missing ${ctx.resourceConfig.pageType} #${pageId}`);
13
+ }
14
+
15
+ return {
16
+ json: {
17
+ ...item,
18
+ },
19
+ };
20
+ };
21
+
22
+ export default { show };
@@ -0,0 +1,15 @@
1
+ const index = async (ctx) => {
2
+ const items = await ctx.client.pages.listAll(
3
+ ctx.query,
4
+ ctx.locale,
5
+ ctx.resourceConfig.pageType
6
+ );
7
+
8
+ return {
9
+ json: {
10
+ ...items,
11
+ },
12
+ };
13
+ };
14
+
15
+ export default { index };
@@ -2,7 +2,13 @@ const AuthHelper = {
2
2
  cookie_key: "auth_cookie",
3
3
 
4
4
  parseJWT: (token) => {
5
- return JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
5
+ const tokenPayload = token?.split(".")?.[1];
6
+
7
+ if(!tokenPayload){
8
+ return "";
9
+ }
10
+
11
+ return JSON.parse(Buffer.from(tokenPayload, "base64").toString());
6
12
  },
7
13
 
8
14
  verifyJWT: (token) => {
package/src/index.js CHANGED
@@ -46,6 +46,7 @@ export { default as RedirectsHandler } from "./handlers/RedirectsHandler";
46
46
  export { default as TagsHandler } from "./handlers/TagsHandler";
47
47
 
48
48
  export { default as PageClonesHandler } from "./handlers/pages/ClonesHandler";
49
+ export { default as ChangeHandler } from "./handlers/pages/ChangeHandler";
49
50
  export { default as PageDeletesHandler } from "./handlers/pages/DeletesHandler";
50
51
  export { default as PageTranslationsHandler } from "./handlers/pages/TranslationsHandler";
51
52
  export { default as PageURLsHandler } from "./handlers/pages/URLsHandler";
@@ -10,7 +10,9 @@ import TagHandler from "./handlers/article_revisions/TagHandler";
10
10
  import UnlocksHandler from "./handlers/article_revisions/UnlocksHandler";
11
11
  import VisualHandler from "./handlers/article_revisions/VisualHandler";
12
12
  import WidgetHandler from "./handlers/article_revisions/WidgetHandler";
13
+ import ArticlesAPIHandler from "./handlers/articles/ArticlesAPIHandler";
13
14
  import ArticlesHandler from "./handlers/articles/ArticlesHandler";
15
+ import ChangeHandler from "./handlers/articles/ChangeHandler";
14
16
  import ClonesHandler from "./handlers/articles/ClonesHandler";
15
17
  import DeletesHandler from "./handlers/articles/DeletesHandler";
16
18
  import HistoryHandler from "./handlers/articles/HistoryHandler";
@@ -146,6 +148,15 @@ function routeArticlesModule(
146
148
  WidgetHandler,
147
149
  resourceConfig
148
150
  );
151
+
152
+ routeCollection(adapter, "api/articles", ArticlesAPIHandler, resourceConfig);
153
+ routeNestedMember(
154
+ adapter,
155
+ "api/article",
156
+ "change",
157
+ ChangeHandler,
158
+ resourceConfig
159
+ );
149
160
  }
150
161
 
151
162
  export default routeArticlesModule;
@@ -10,8 +10,10 @@ import TagHandler from "./handlers/event_revisions/TagHandler";
10
10
  import UnlocksHandler from "./handlers/event_revisions/UnlocksHandler";
11
11
  import VisualHandler from "./handlers/event_revisions/VisualHandler";
12
12
  import WidgetHandler from "./handlers/event_revisions/WidgetHandler";
13
+ import ChangeHandler from "./handlers/events/ChangeHandler";
13
14
  import ClonesHandler from "./handlers/events/ClonesHandler";
14
15
  import DeletesHandler from "./handlers/events/DeletesHandler";
16
+ import EventsAPIHandler from "./handlers/events/EventsAPIHandler";
15
17
  import EventsHandler from "./handlers/events/EventsHandler";
16
18
  import HistoryHandler from "./handlers/events/HistoryHandler";
17
19
  import TranslationsHandler from "./handlers/events/TranslationsHandler";
@@ -161,6 +163,15 @@ function routeEventsModule(
161
163
  WidgetHandler,
162
164
  resourceConfig
163
165
  );
166
+
167
+ routeCollection(adapter, "api/events", EventsAPIHandler, resourceConfig);
168
+ routeNestedMember(
169
+ adapter,
170
+ "api/event",
171
+ "change",
172
+ ChangeHandler,
173
+ resourceConfig
174
+ );
164
175
  }
165
176
 
166
177
  export default routeEventsModule;
@@ -10,9 +10,11 @@ import TagHandler from "./handlers/page_revisions/TagHandler";
10
10
  import UnlocksHandler from "./handlers/page_revisions/UnlocksHandler";
11
11
  import VisualHandler from "./handlers/page_revisions/VisualHandler";
12
12
  import WidgetHandler from "./handlers/page_revisions/WidgetHandler";
13
+ import ChangeHandler from "./handlers/pages/ChangeHandler";
13
14
  import ClonesHandler from "./handlers/pages/ClonesHandler";
14
15
  import DeletesHandler from "./handlers/pages/DeletesHandler";
15
16
  import HistoryHandler from "./handlers/pages/HistoryHandler";
17
+ import PagesAPIHandler from "./handlers/pages/PagesAPIHandler";
16
18
  import PagesHandler from "./handlers/pages/PagesHandler";
17
19
  import TranslationsHandler from "./handlers/pages/TranslationsHandler";
18
20
  import URLsHandler from "./handlers/pages/URLsHandler";
@@ -158,6 +160,15 @@ function routePagesModule(
158
160
  WidgetHandler,
159
161
  resourceConfig
160
162
  );
163
+
164
+ routeCollection(adapter, "api/pages", PagesAPIHandler, resourceConfig);
165
+ routeNestedMember(
166
+ adapter,
167
+ "api/page",
168
+ "change",
169
+ ChangeHandler,
170
+ resourceConfig
171
+ );
161
172
  }
162
173
 
163
174
  export default routePagesModule;
@@ -1,11 +1,12 @@
1
- import routeCollection from "./routeCollection";
2
- import routeSettings from "./routeSettings";
3
- import routePagesModule from "./routePagesModule";
4
- import routeArticlesModule from "./routeArticlesModule";
5
- import routeEventsModule from "./routeEventsModule";
1
+ import DashboardsHandler from "./handlers/DashboardsHandler";
6
2
  import RedirectsHandler from "./handlers/RedirectsHandler";
3
+ import TagsAPIHandler from "./handlers/TagsAPIHandler";
7
4
  import TagsHandler from "./handlers/TagsHandler";
8
- import DashboardsHandler from "./handlers/DashboardsHandler";
5
+ import routeArticlesModule from "./routeArticlesModule";
6
+ import routeCollection from "./routeCollection";
7
+ import routeEventsModule from "./routeEventsModule";
8
+ import routePagesModule from "./routePagesModule";
9
+ import routeSettings from "./routeSettings";
9
10
 
10
11
  function routeStandard(adapter) {
11
12
  adapter.routeCollection(
@@ -26,6 +27,7 @@ function routeStandard(adapter) {
26
27
 
27
28
  routeCollection(adapter, "redirects", RedirectsHandler);
28
29
  routeCollection(adapter, "tags", TagsHandler);
30
+ routeCollection(adapter, "api/tags", TagsAPIHandler);
29
31
  routeSettings(adapter);
30
32
  routePagesModule(adapter);
31
33
  routeArticlesModule(adapter);
@@ -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
+ });
@@ -0,0 +1,38 @@
1
+ import Tags from "../../../cloud/Tags";
2
+ import TagsAPIHandler from "../../../handlers/TagsAPIHandler";
3
+
4
+ import Factory from "../../support/factory";
5
+
6
+ describe("Pages API Handler", () => {
7
+ function buildCtx() {
8
+ return {
9
+ client: {
10
+ tags: new Tags(() => ({}), "token"),
11
+ },
12
+ query: { foo: "bar" },
13
+ locale: "en",
14
+ };
15
+ }
16
+
17
+ test("index", async () => {
18
+ const tags = [
19
+ Factory.build("tag", { id: "1", name: "First Tag" }),
20
+ Factory.build("tag", { id: "2", name: "Second Tag" }),
21
+ ];
22
+
23
+ const ctx = buildCtx();
24
+
25
+ const mockedList = jest
26
+ .spyOn(ctx.client.tags, "list")
27
+ .mockImplementation(() => ({ tags: tags }));
28
+
29
+ const result = await TagsAPIHandler.index(ctx);
30
+
31
+ expect(mockedList).toHaveBeenCalledWith({ foo: "bar" }, "en");
32
+ expect(result).toEqual({
33
+ json: {
34
+ tags: tags,
35
+ },
36
+ });
37
+ });
38
+ });
@@ -0,0 +1,43 @@
1
+ import Articles from "../../../../cloud/Articles";
2
+ import ArticlesAPIHandler from "../../../../handlers/articles/ArticlesAPIHandler";
3
+
4
+ import Factory from "../../../support/factory";
5
+
6
+ describe("Articles API Handler", () => {
7
+ function buildCtx() {
8
+ return {
9
+ client: {
10
+ articles: new Articles(() => ({}), "token"),
11
+ },
12
+ query: { foo: "bar" },
13
+ locale: "en",
14
+ resourceConfig: {
15
+ articleType: "article",
16
+ articleTypePlural: "articles",
17
+ },
18
+ };
19
+ }
20
+
21
+ test("index", async () => {
22
+ const article = Factory.build("article");
23
+ const ctx = buildCtx();
24
+
25
+ const mockedArticleList = jest
26
+ .spyOn(ctx.client.articles, "listAll")
27
+ .mockImplementation(() => ({ list: [article] }));
28
+
29
+ const result = await ArticlesAPIHandler.index(ctx);
30
+
31
+ expect(mockedArticleList).toHaveBeenCalledWith(
32
+ { foo: "bar" },
33
+ "en",
34
+ "article"
35
+ );
36
+
37
+ expect(result).toEqual({
38
+ json: {
39
+ list: [article],
40
+ },
41
+ });
42
+ });
43
+ });
@@ -0,0 +1,59 @@
1
+ import ArticleChanges from "../../../../cloud/ArticleChanges";
2
+ import ArticleRevisions from "../../../../cloud/ArticleRevisions";
3
+ import Articles from "../../../../cloud/Articles";
4
+ import ChangeHandler from "../../../../handlers/articles/ChangeHandler";
5
+
6
+ import Factory from "../../../support/factory";
7
+
8
+ describe("ChangeHandler", () => {
9
+ function buildCtx(article) {
10
+ return {
11
+ client: {
12
+ articles: new Articles(() => ({}), "token"),
13
+ articleChanges: new ArticleChanges(() => ({}), "token"),
14
+ articleRevisions: new ArticleRevisions(() => ({}), "token"),
15
+ },
16
+ params: { parent_id: article.id },
17
+ query: { version: 49 },
18
+ locale: "en",
19
+ resourceConfig: {
20
+ articleType: "article",
21
+ articleTypePlural: "articles",
22
+ },
23
+ };
24
+ }
25
+
26
+ test("show for existing article", async () => {
27
+ const article = Factory.build("article");
28
+
29
+ const ctx = buildCtx(article);
30
+
31
+ const mockedArticleFind = jest
32
+ .spyOn(ctx.client.articles, "getChangePreview")
33
+ .mockImplementation(() => [article]);
34
+
35
+ const result = await ChangeHandler.show(ctx);
36
+
37
+ expect(mockedArticleFind).toHaveBeenCalledWith(article.id, 49, "en");
38
+
39
+ expect(result).toEqual({
40
+ json: {
41
+ ...[article],
42
+ },
43
+ });
44
+ });
45
+
46
+ test("show for non existing article", async () => {
47
+ const ctx = buildCtx({ id: "42" });
48
+
49
+ const mockedArticleFind = jest
50
+ .spyOn(ctx.client.articles, "getChangePreview")
51
+ .mockImplementation(() => null);
52
+
53
+ expect(async () => {
54
+ await ChangeHandler.show(ctx);
55
+ }).rejects.toThrow("Missing article #42");
56
+
57
+ expect(mockedArticleFind).toHaveBeenCalledWith("42", 49, "en");
58
+ });
59
+ });
@@ -0,0 +1,59 @@
1
+ import EventChanges from "../../../../cloud/EventChanges";
2
+ import EventRevisions from "../../../../cloud/EventRevisions";
3
+ import Events from "../../../../cloud/Events";
4
+ import ChangeHandler from "../../../../handlers/events/ChangeHandler";
5
+
6
+ import Factory from "../../../support/factory";
7
+
8
+ describe("ChangeHandler", () => {
9
+ function buildCtx(event) {
10
+ return {
11
+ client: {
12
+ events: new Events(() => ({}), "token"),
13
+ eventChanges: new EventChanges(() => ({}), "token"),
14
+ eventRevisions: new EventRevisions(() => ({}), "token"),
15
+ },
16
+ params: { parent_id: event.id },
17
+ query: { version: 49 },
18
+ locale: "en",
19
+ resourceConfig: {
20
+ eventType: "event",
21
+ eventTypePlural: "events",
22
+ },
23
+ };
24
+ }
25
+
26
+ test("show for existing event", async () => {
27
+ const event = Factory.build("event");
28
+
29
+ const ctx = buildCtx(event);
30
+
31
+ const mockedEventFind = jest
32
+ .spyOn(ctx.client.events, "getChangePreview")
33
+ .mockImplementation(() => [event]);
34
+
35
+ const result = await ChangeHandler.show(ctx);
36
+
37
+ expect(mockedEventFind).toHaveBeenCalledWith(event.id, 49, "en");
38
+
39
+ expect(result).toEqual({
40
+ json: {
41
+ ...[event],
42
+ },
43
+ });
44
+ });
45
+
46
+ test("show for non existing event", async () => {
47
+ const ctx = buildCtx({ id: "42" });
48
+
49
+ const mockedEventFind = jest
50
+ .spyOn(ctx.client.events, "getChangePreview")
51
+ .mockImplementation(() => null);
52
+
53
+ expect(async () => {
54
+ await ChangeHandler.show(ctx);
55
+ }).rejects.toThrow("Missing event #42");
56
+
57
+ expect(mockedEventFind).toHaveBeenCalledWith("42", 49, "en");
58
+ });
59
+ });
@@ -0,0 +1,39 @@
1
+ import Events from "../../../../cloud/Events";
2
+ import EventsAPIHandler from "../../../../handlers/events/EventsAPIHandler";
3
+
4
+ import Factory from "../../../support/factory";
5
+
6
+ describe("Events API Handler", () => {
7
+ function buildCtx() {
8
+ return {
9
+ client: {
10
+ events: new Events(() => ({}), "token"),
11
+ },
12
+ query: { foo: "bar" },
13
+ locale: "en",
14
+ resourceConfig: {
15
+ eventType: "event",
16
+ eventTypePlural: "events",
17
+ },
18
+ };
19
+ }
20
+
21
+ test("index", async () => {
22
+ const event = Factory.build("event");
23
+ const ctx = buildCtx();
24
+
25
+ const mockedEventList = jest
26
+ .spyOn(ctx.client.events, "listAll")
27
+ .mockImplementation(() => ({ list: [event] }));
28
+
29
+ const result = await EventsAPIHandler.index(ctx);
30
+
31
+ expect(mockedEventList).toHaveBeenCalledWith({ foo: "bar" }, "en", "event");
32
+
33
+ expect(result).toEqual({
34
+ json: {
35
+ list: [event],
36
+ },
37
+ });
38
+ });
39
+ });
@@ -0,0 +1,59 @@
1
+ import PageChanges from "../../../../cloud/PageChanges";
2
+ import PageRevisions from "../../../../cloud/PageRevisions";
3
+ import Pages from "../../../../cloud/Pages";
4
+ import ChangeHandler from "../../../../handlers/pages/ChangeHandler";
5
+
6
+ import Factory from "../../../support/factory";
7
+
8
+ describe("ChangeHandler", () => {
9
+ function buildCtx(page) {
10
+ return {
11
+ client: {
12
+ pages: new Pages(() => ({}), "token"),
13
+ pageChanges: new PageChanges(() => ({}), "token"),
14
+ pageRevisions: new PageRevisions(() => ({}), "token"),
15
+ },
16
+ params: { parent_id: page.id },
17
+ query: { version: 49 },
18
+ locale: "en",
19
+ resourceConfig: {
20
+ pageType: "page",
21
+ pageTypePlural: "pages",
22
+ },
23
+ };
24
+ }
25
+
26
+ test("show for existing page", async () => {
27
+ const page = Factory.build("page");
28
+
29
+ const ctx = buildCtx(page);
30
+
31
+ const mockedPageFind = jest
32
+ .spyOn(ctx.client.pages, "getChangePreview")
33
+ .mockImplementation(() => [page]);
34
+
35
+ const result = await ChangeHandler.show(ctx);
36
+
37
+ expect(mockedPageFind).toHaveBeenCalledWith(page.id, 49, "en");
38
+
39
+ expect(result).toEqual({
40
+ json: {
41
+ ...[page],
42
+ },
43
+ });
44
+ });
45
+
46
+ test("show for non existing page", async () => {
47
+ const ctx = buildCtx({ id: "42" });
48
+
49
+ const mockedPageFind = jest
50
+ .spyOn(ctx.client.pages, "getChangePreview")
51
+ .mockImplementation(() => null);
52
+
53
+ expect(async () => {
54
+ await ChangeHandler.show(ctx);
55
+ }).rejects.toThrow("Missing page #42");
56
+
57
+ expect(mockedPageFind).toHaveBeenCalledWith("42", 49, "en");
58
+ });
59
+ });
@@ -0,0 +1,39 @@
1
+ import Pages from "../../../../cloud/Pages";
2
+ import PagesAPIHandler from "../../../../handlers/pages/PagesAPIHandler";
3
+
4
+ import Factory from "../../../support/factory";
5
+
6
+ describe("Pages API Handler", () => {
7
+ function buildCtx() {
8
+ return {
9
+ client: {
10
+ pages: new Pages(() => ({}), "token"),
11
+ },
12
+ query: { foo: "bar" },
13
+ locale: "en",
14
+ resourceConfig: {
15
+ pageType: "page",
16
+ pageTypePlural: "pages",
17
+ },
18
+ };
19
+ }
20
+
21
+ test("index", async () => {
22
+ const page = Factory.build("page");
23
+ const ctx = buildCtx();
24
+
25
+ const mockedPageList = jest
26
+ .spyOn(ctx.client.pages, "listAll")
27
+ .mockImplementation(() => ({ list: [page] }));
28
+
29
+ const result = await PagesAPIHandler.index(ctx);
30
+
31
+ expect(mockedPageList).toHaveBeenCalledWith({ foo: "bar" }, "en", "page");
32
+
33
+ expect(result).toEqual({
34
+ json: {
35
+ list: [page],
36
+ },
37
+ });
38
+ });
39
+ });
@@ -49,6 +49,18 @@ describe("AuthHelper", () => {
49
49
  const result = AuthHelper.verifyJWT(mockToken);
50
50
  expect(result).toBe(false);
51
51
  });
52
+
53
+ test("returns false for invalid token", () => {
54
+ const invalidToken = "header";
55
+ const invalidToken2 = "header.";
56
+
57
+ const result = AuthHelper.verifyJWT(invalidToken);
58
+ expect(result).toBe(false);
59
+
60
+ const result2 = AuthHelper.verifyJWT(invalidToken2);
61
+ expect(result2).toBe(false);
62
+ })
63
+
52
64
  });
53
65
 
54
66
  describe("isAuthenticated", () => {