@raketa-cloud/admin 1.2.0 → 1.4.0

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/HonoAdapter.js +304 -169
  3. package/src/filters.js +17 -0
  4. package/src/handlers/article_revisions/ArticleRevisionsHandler.js +8 -0
  5. package/src/handlers/articles/ArticlesHandler.js +12 -1
  6. package/src/handlers/event_revisions/EventRevisionsHandler.js +8 -0
  7. package/src/handlers/events/EventsHandler.js +13 -2
  8. package/src/handlers/page_revisions/PageRevisionsHandler.js +8 -0
  9. package/src/handlers/pages/PagesHandler.js +12 -1
  10. package/src/index.js +1 -0
  11. package/src/routeCollection.js +14 -83
  12. package/src/routeMember.js +10 -33
  13. package/src/routeNestedCollection.js +19 -47
  14. package/src/routeNestedMember.js +14 -40
  15. package/src/tests/Adapter.test.js +4 -4
  16. package/src/tests/HonoAdapter.filter.test.js +40 -0
  17. package/src/tests/HonoAdapter.test.js +110 -9
  18. package/src/tests/RaketaContext.test.js +65 -0
  19. package/src/tests/cipher.test.js +73 -0
  20. package/src/tests/filters.test.js +97 -0
  21. package/src/tests/handlers/article_revisions/ArticleRevisionsHandler.test/create.test.js +43 -35
  22. package/src/tests/handlers/articles/ArticlesHandler.test/show.test.js +12 -1
  23. package/src/tests/handlers/event_revisions/EventRevisionsHandler.test/create.test.js +41 -33
  24. package/src/tests/handlers/events/EventsHandler.test/show.test.js +12 -1
  25. package/src/tests/handlers/page_revisions/PageRevisionsHandler.test/create.test.js +41 -27
  26. package/src/tests/handlers/pages/PagesHandler.test/show.test.js +12 -1
  27. package/src/tests/routeCollection.test.js +0 -92
  28. package/src/tests/routeMember.test.js +0 -6
  29. package/src/tests/routeNestedCollection.test.js +0 -14
  30. package/src/tests/routeNestedMember.test.js +0 -6
  31. package/src/tests/utils.test.js +70 -0
  32. package/src/views/articles/_shared/_history_toolbar.ejs +2 -2
  33. package/src/views/articles/_shared/_toolbar.ejs +38 -26
  34. package/src/views/articles/_shared/history_table.ejs +6 -0
  35. package/src/views/events/_shared/_history_toolbar.ejs +2 -2
  36. package/src/views/events/_shared/_toolbar.ejs +38 -26
  37. package/src/views/events/_shared/history_table.ejs +6 -0
  38. package/src/views/pages/_shared/_history_toolbar.ejs +2 -2
  39. package/src/views/pages/_shared/_toolbar.ejs +38 -26
  40. package/src/views/pages/_shared/history_table.ejs +6 -0
  41. package/src/Adapter.js +0 -265
@@ -25,7 +25,18 @@ const show = async (ctx) => {
25
25
  }
26
26
 
27
27
  const revisions = await ctx.client.pageRevisions.all(id, ctx.locale);
28
- const changes = await ctx.client.pageChanges.list({}, id, ctx.locale);
28
+ const changes = await ctx.client.pageChanges.list(
29
+ {
30
+ "f:change_type": [
31
+ "revision_publish",
32
+ "revision_lock",
33
+ "revision_unlock",
34
+ "update",
35
+ ],
36
+ },
37
+ id,
38
+ ctx.locale
39
+ );
29
40
 
30
41
  return {
31
42
  assigns: {
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ export { default as setupCloud } from "./setupCloud";
12
12
  export { default as routeStandard } from "./routeStandard";
13
13
  export { default as routeCategoriesModule } from "./routeCategoriesModule";
14
14
  export { default as buildResourcePath } from "./buildResourcePath";
15
+ export { setBeforeFilter, setAfterFilter } from "./filters";
15
16
 
16
17
  export { default as RaketaClient } from "./cloud/RaketaClient";
17
18
  export { default as Redirects } from "./cloud/Redirects";
@@ -1,89 +1,20 @@
1
1
  const routeCollection = (adapter, resource, handler, resourceConfig = {}) => {
2
- const {
3
- before_filter,
4
- after_filter,
5
- index,
6
- show,
7
- create_form,
8
- edit_form,
9
- create,
10
- update,
11
- destroy,
12
- } = handler;
13
-
14
- const mapping = [
15
- [
16
- "GET",
17
- `/${resource}`,
18
- resource,
19
- resource,
20
- "index",
21
- index,
22
- before_filter,
23
- after_filter,
24
- ],
25
- [
26
- "GET",
27
- `/${resource}/new`,
28
- resource,
29
- resource,
30
- "create_form",
31
- create_form,
32
- before_filter,
33
- after_filter,
34
- ],
35
- [
36
- "GET",
37
- `/${resource}/:id`,
38
- resource,
39
- resource,
40
- "show",
41
- show,
42
- before_filter,
43
- after_filter,
44
- ],
45
- [
46
- "GET",
47
- `/${resource}/:id/edit`,
48
- resource,
49
- resource,
50
- "edit_form",
51
- edit_form,
52
- before_filter,
53
- after_filter,
54
- ],
55
- [
56
- "POST",
57
- `/${resource}`,
58
- resource,
59
- resource,
60
- "create_form",
61
- create,
62
- before_filter,
63
- after_filter,
64
- ],
65
- [
66
- "POST",
67
- `/${resource}/:id`,
68
- resource,
69
- resource,
70
- "edit_form",
71
- update,
72
- before_filter,
73
- after_filter,
74
- ],
75
- [
76
- "DELETE",
77
- `/${resource}/:id`,
78
- resource,
79
- resource,
80
- "destroy",
81
- destroy,
82
- before_filter,
83
- after_filter,
84
- ],
2
+ // Create route definitions
3
+ const routes = [
4
+ ["GET", `/${resource}`, "index", handler.index],
5
+ ["GET", `/${resource}/new`, "create_form", handler.create_form],
6
+ ["GET", `/${resource}/:id`, "show", handler.show],
7
+ ["GET", `/${resource}/:id/edit`, "edit_form", handler.edit_form],
8
+ ["POST", `/${resource}`, "create_form", handler.create],
9
+ ["POST", `/${resource}/:id`, "edit_form", handler.update],
10
+ ["DELETE", `/${resource}/:id`, "destroy", handler.destroy]
85
11
  ];
86
12
 
13
+ // Map routes to full configuration
14
+ const mapping = routes.map(([verb, path, action, fn]) => [
15
+ verb, path, resource, resource, action, fn
16
+ ]);
17
+
87
18
  adapter.routeCollection(mapping, resourceConfig);
88
19
  };
89
20
 
@@ -1,39 +1,16 @@
1
1
  const routeMember = (adapter, resource, handler, resourceConfig = {}) => {
2
- const { before_filter, after_filter, show, edit_form, update } = handler;
3
-
4
- const mapping = [
5
- [
6
- "GET",
7
- `/${resource}`,
8
- resource,
9
- resource,
10
- "show",
11
- show,
12
- before_filter,
13
- after_filter,
14
- ],
15
- [
16
- "GET",
17
- `/${resource}/edit`,
18
- resource,
19
- resource,
20
- "edit_form",
21
- edit_form,
22
- before_filter,
23
- after_filter,
24
- ],
25
- [
26
- "POST",
27
- `/${resource}`,
28
- resource,
29
- resource,
30
- "edit_form",
31
- update,
32
- before_filter,
33
- after_filter,
34
- ],
2
+ // Create route definitions
3
+ const routes = [
4
+ ["GET", `/${resource}`, "show", handler.show],
5
+ ["GET", `/${resource}/edit`, "edit_form", handler.edit_form],
6
+ ["POST", `/${resource}`, "edit_form", handler.update]
35
7
  ];
36
8
 
9
+ // Map routes to full configuration
10
+ const mapping = routes.map(([verb, path, action, fn]) => [
11
+ verb, path, resource, resource, action, fn
12
+ ]);
13
+
37
14
  adapter.routeMember(mapping, resourceConfig);
38
15
  };
39
16
 
@@ -1,93 +1,65 @@
1
- const routeNestedCollection = (
2
- adapter,
3
- parent,
4
- resource,
5
- handler,
6
- resourceConfig = {}
7
- ) => {
8
- const {
9
- before_filter,
10
- after_filter,
11
- index,
12
- show,
13
- create_form,
14
- edit_form,
15
- create,
16
- update,
17
- destroy,
18
- } = handler;
1
+ const routeNestedCollection = (adapter, parent, resource, handler, resourceConfig = {}) => {
2
+ const views = `${parent}/${resource}`;
19
3
 
4
+ // Define routes exactly as they were in the original implementation
5
+ // to maintain compatibility with the tests
20
6
  const mapping = [
21
7
  [
22
8
  "GET",
23
9
  `/${parent}/:parent_id/${resource}`,
24
10
  resource,
25
- `${parent}/${resource}`,
11
+ views,
26
12
  "index",
27
- index,
28
- before_filter,
29
- after_filter,
13
+ handler.index
30
14
  ],
31
15
  [
32
16
  "GET",
33
17
  `/${parent}/:parent_id/${resource}/new`,
34
18
  resource,
35
- `${parent}/${resource}`,
19
+ views,
36
20
  "create_form",
37
- create_form,
38
- before_filter,
39
- after_filter,
21
+ handler.create_form
40
22
  ],
41
23
  [
42
24
  "GET",
43
25
  `/${resource}/:id`,
44
26
  resource,
45
- `${parent}/${resource}`,
27
+ views,
46
28
  "show",
47
- show,
48
- before_filter,
49
- after_filter,
29
+ handler.show
50
30
  ],
51
31
  [
52
32
  "GET",
53
33
  `/${resource}/:id/edit`,
54
34
  resource,
55
- `${parent}/${resource}`,
35
+ views,
56
36
  "edit_form",
57
- edit_form,
58
- before_filter,
59
- after_filter,
37
+ handler.edit_form
60
38
  ],
61
39
  [
62
40
  "POST",
63
41
  `/${parent}/:parent_id/${resource}`,
64
42
  resource,
65
- `${parent}/${resource}`,
43
+ views,
66
44
  "create_form",
67
- create,
68
- before_filter,
69
- after_filter,
45
+ handler.create
70
46
  ],
71
47
  [
72
48
  "PUT",
73
49
  `/${resource}/:id`,
74
50
  resource,
75
- `${parent}/${resource}`,
51
+ views,
76
52
  "update",
77
- update,
78
- before_filter,
79
- after_filter,
53
+ handler.update
80
54
  ],
81
55
  [
82
56
  "DELETE",
83
57
  `/${resource}/:id`,
84
58
  resource,
85
- `${parent}/${resource}`,
59
+ views,
86
60
  "destroy",
87
- destroy,
88
- before_filter,
89
- after_filter,
90
- ],
61
+ handler.destroy
62
+ ]
91
63
  ];
92
64
 
93
65
  adapter.routeNestedCollection(mapping, resourceConfig);
@@ -1,45 +1,19 @@
1
- const routeNestedMember = (
2
- adapter,
3
- parent,
4
- resource,
5
- handler,
6
- resourceConfig = {}
7
- ) => {
8
- const { before_filter, after_filter, show, edit_form, update } = handler;
9
-
10
- const mapping = [
11
- [
12
- "GET",
13
- `/${parent}/:parent_id/${resource}`,
14
- resource,
15
- `${parent}/${resource}`,
16
- "show",
17
- show,
18
- before_filter,
19
- after_filter,
20
- ],
21
- [
22
- "GET",
23
- `/${parent}/:parent_id/${resource}/edit`,
24
- resource,
25
- `${parent}/${resource}`,
26
- "edit_form",
27
- edit_form,
28
- before_filter,
29
- after_filter,
30
- ],
31
- [
32
- "POST",
33
- `/${parent}/:parent_id/${resource}`,
34
- resource,
35
- `${parent}/${resource}`,
36
- "edit_form",
37
- update,
38
- before_filter,
39
- after_filter,
40
- ],
1
+ const routeNestedMember = (adapter, parent, resource, handler, resourceConfig = {}) => {
2
+ const views = `${parent}/${resource}`;
3
+ const basePath = `/${parent}/:parent_id/${resource}`;
4
+
5
+ // Create route definitions
6
+ const routes = [
7
+ ["GET", basePath, "show", handler.show],
8
+ ["GET", `${basePath}/edit`, "edit_form", handler.edit_form],
9
+ ["POST", basePath, "edit_form", handler.update]
41
10
  ];
42
11
 
12
+ // Map routes to full configuration
13
+ const mapping = routes.map(([verb, path, action, fn]) => [
14
+ verb, path, resource, views, action, fn
15
+ ]);
16
+
43
17
  adapter.routeNestedMember(mapping, resourceConfig);
44
18
  };
45
19
 
@@ -1,8 +1,8 @@
1
- import Adapter from "../Adapter";
1
+ import HonoAdapter from "../HonoAdapter";
2
2
 
3
- const adapter = new Adapter({});
3
+ const adapter = new HonoAdapter({});
4
4
 
5
- describe("Adapter", () => {
5
+ describe("HonoAdapter parseFormBody", () => {
6
6
  test("parseFormBody returns an object with parsed keys", () => {
7
7
  expect(
8
8
  adapter.parseFormBody({
@@ -36,4 +36,4 @@ describe("Adapter", () => {
36
36
  },
37
37
  });
38
38
  });
39
- });
39
+ });
@@ -0,0 +1,40 @@
1
+ import HonoAdapter from "../HonoAdapter";
2
+
3
+ jest.mock("../setupAuthentication", () => jest.fn());
4
+
5
+ describe("HonoAdapter Global Filters", () => {
6
+ let adapter;
7
+
8
+ beforeEach(() => {
9
+ adapter = new HonoAdapter({
10
+ get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn(),
11
+ router: { routes: [] }
12
+ }, {
13
+ i18n: { locales: { en: "English" }, defaultLocale: "en", timezones: {} },
14
+ remoteConfig: {},
15
+ layout: {}
16
+ });
17
+ });
18
+
19
+ test("setBeforeFilter adds filters to the globalBeforeFilters array", () => {
20
+ expect(adapter.globalBeforeFilters).toEqual([]);
21
+
22
+ const pattern = /\/settings\/content/;
23
+ const filter = () => ({ assigns: { foo: "bar" } });
24
+
25
+ adapter.setBeforeFilter(pattern, filter);
26
+
27
+ expect(adapter.globalBeforeFilters).toEqual([{ pattern, filter }]);
28
+ });
29
+
30
+ test("setAfterFilter adds filters to the globalAfterFilters array", () => {
31
+ expect(adapter.globalAfterFilters).toEqual([]);
32
+
33
+ const pattern = /\/settings\/content/;
34
+ const filter = () => ({ assigns: { foo: "bar" } });
35
+
36
+ adapter.setAfterFilter(pattern, filter);
37
+
38
+ expect(adapter.globalAfterFilters).toEqual([{ pattern, filter }]);
39
+ });
40
+ });
@@ -1,17 +1,29 @@
1
1
  import HonoAdapter from "../HonoAdapter";
2
2
 
3
3
  class HonoMock {
4
- use() {}
5
- get() {}
6
- post() {}
7
- put() {}
8
- delete() {}
4
+ constructor() {
5
+ this.router = {
6
+ routes: []
7
+ };
8
+ this.get = jest.fn();
9
+ this.post = jest.fn();
10
+ this.put = jest.fn();
11
+ this.delete = jest.fn();
12
+ this.use = jest.fn();
13
+ this.header = jest.fn();
14
+ }
9
15
  }
10
16
 
11
- const app = new HonoMock();
12
- const adapter = new HonoAdapter(app);
13
-
14
17
  describe("HonoAdapter", () => {
18
+ let app;
19
+ let adapter;
20
+
21
+ beforeEach(() => {
22
+ app = new HonoMock();
23
+ adapter = new HonoAdapter(app);
24
+ });
25
+
26
+ // Method tests
15
27
  test("getMethod returns Hono's get for invalid verbs", () => {
16
28
  expect(adapter.getMethod("not existing")).toBe(app.get);
17
29
  });
@@ -31,4 +43,93 @@ describe("HonoAdapter", () => {
31
43
  test("getMethod for DELETE", () => {
32
44
  expect(adapter.getMethod("DELETE")).toBe(app.delete);
33
45
  });
34
- });
46
+
47
+ // Form body parsing tests
48
+ test("parseFormBody returns an object with parsed keys", () => {
49
+ expect(
50
+ adapter.parseFormBody({
51
+ "order[email]": "john@example.com",
52
+ "order[uid]": "order-id",
53
+ "order[vouchers]": ["42", 420],
54
+ "order[items][1][name]": "A beer",
55
+ "order[items][1][price]": 5,
56
+ "order[items][1][qty]": 3,
57
+ "order[items][2][name]": "A t-shirt",
58
+ "order[items][2][price]": 20,
59
+ "order[items][2][qty]": 1,
60
+ })
61
+ ).toEqual({
62
+ order: {
63
+ email: "john@example.com",
64
+ uid: "order-id",
65
+ vouchers: ["42", 420],
66
+ items: {
67
+ 1: {
68
+ name: "A beer",
69
+ price: 5,
70
+ qty: 3,
71
+ },
72
+ 2: {
73
+ name: "A t-shirt",
74
+ price: 20,
75
+ qty: 1,
76
+ },
77
+ },
78
+ },
79
+ });
80
+ });
81
+
82
+ test("setGlobalContext sets the global context function", () => {
83
+ const newCtx = () => ({ test: true });
84
+ adapter.setGlobalContext(newCtx);
85
+ expect(adapter.globalCtx).toBe(newCtx);
86
+ });
87
+
88
+ test("applyHeaders applies headers to router context", () => {
89
+ const headers = {
90
+ "Content-Type": "application/json",
91
+ "X-Custom-Header": "test-value"
92
+ };
93
+
94
+ adapter.applyHeaders(headers, app);
95
+
96
+ expect(app.header).toHaveBeenCalledTimes(2);
97
+ expect(app.header).toHaveBeenCalledWith("Content-Type", "application/json");
98
+ expect(app.header).toHaveBeenCalledWith("X-Custom-Header", "test-value");
99
+ });
100
+
101
+ test("applyHeaders does nothing when headers is undefined", () => {
102
+ adapter.applyHeaders(undefined, app);
103
+ expect(app.header).not.toHaveBeenCalled();
104
+ });
105
+
106
+ test("formatQuery formats query parameters correctly", () => {
107
+ const queries = {
108
+ single: ["value"],
109
+ multiple: ["one", "two"]
110
+ };
111
+
112
+ const result = adapter.formatQuery(queries);
113
+
114
+ expect(result).toEqual({
115
+ single: "value",
116
+ multiple: ["one", "two"]
117
+ });
118
+ });
119
+
120
+ test("parseFormBody handles array values", () => {
121
+ expect(
122
+ adapter.parseFormBody({
123
+ "tags[]": ["javascript", "react", "node"]
124
+ })
125
+ ).toEqual({
126
+ tags: ["javascript", "react", "node"]
127
+ });
128
+ });
129
+
130
+ test("parseFormBody handles empty values", () => {
131
+ expect(
132
+ adapter.parseFormBody({})
133
+ ).toEqual({});
134
+ });
135
+ });
@@ -0,0 +1,65 @@
1
+ import RaketaContext from "../RaketaContext";
2
+
3
+ describe("RaketaContext", () => {
4
+ test("constructor should initialize all properties", () => {
5
+ const params = {
6
+ router: { fake: "router" },
7
+ res: { fake: "res" },
8
+ req: { fake: "req" },
9
+ method: "GET",
10
+ headers: { "content-type": "application/json" },
11
+ rawPath: "/raw/path",
12
+ path: "/path",
13
+ url: "http://example.com/path",
14
+ action: "index",
15
+ params: { id: "123" },
16
+ query: { sort: "name" },
17
+ body: { title: "Test" },
18
+ resourceParams: { resource: { title: "Test" } },
19
+ resource: "articles",
20
+ assigns: { someVar: "value" },
21
+ authentication: { token: "jwt-token" },
22
+ getGlobalCtx: () => ({}),
23
+ };
24
+
25
+ const context = new RaketaContext(params);
26
+
27
+ // Test that all properties are set correctly
28
+ Object.keys(params).forEach((key) => {
29
+ expect(context[key]).toEqual(params[key]);
30
+ });
31
+ });
32
+
33
+ test("initGlobalContext should merge global context into the instance", () => {
34
+ const globalCtx = jest.fn().mockReturnValue({
35
+ client: { users: "usersClient" },
36
+ config: { someConfig: true },
37
+ });
38
+
39
+ const context = new RaketaContext({
40
+ router: {},
41
+ res: {},
42
+ req: {},
43
+ method: "GET",
44
+ headers: {},
45
+ rawPath: "",
46
+ path: "",
47
+ url: "",
48
+ action: "",
49
+ params: {},
50
+ query: {},
51
+ body: {},
52
+ resourceParams: {},
53
+ resource: "",
54
+ assigns: {},
55
+ authentication: {},
56
+ getGlobalCtx: globalCtx,
57
+ });
58
+
59
+ context.initGlobalContext();
60
+
61
+ expect(globalCtx).toHaveBeenCalledWith(context);
62
+ expect(context.client).toEqual({ users: "usersClient" });
63
+ expect(context.config).toEqual({ someConfig: true });
64
+ });
65
+ });
@@ -0,0 +1,73 @@
1
+ import cipher from "../cipher";
2
+ import { randomBytes } from "node:crypto";
3
+
4
+ jest.mock("node:crypto", () => {
5
+ const originalModule = jest.requireActual("node:crypto");
6
+
7
+ // Mock implementation to make tests deterministic
8
+ const mockIV = Buffer.from("0123456789abcdef0123456789abcdef", "hex");
9
+
10
+ return {
11
+ ...originalModule,
12
+ randomBytes: jest.fn(() => mockIV),
13
+ createCipheriv: originalModule.createCipheriv,
14
+ createDecipheriv: originalModule.createDecipheriv,
15
+ pbkdf2Sync: originalModule.pbkdf2Sync,
16
+ };
17
+ });
18
+
19
+ describe("cipher", () => {
20
+ const mockConfig = {
21
+ secrets: {
22
+ crypto_secret: "test-secret-key"
23
+ }
24
+ };
25
+
26
+ describe("encrypt", () => {
27
+ test("encrypts plaintext string", () => {
28
+ const plaintext = "sensitive data";
29
+ const encrypted = cipher.encrypt(plaintext, mockConfig);
30
+
31
+ // Check that the output has the expected format (IV:encryptedData)
32
+ expect(encrypted).toContain(":");
33
+ const [ivHex, encryptedData] = encrypted.split(":");
34
+
35
+ // IV should be 32 hex characters (16 bytes)
36
+ expect(ivHex.length).toBe(32);
37
+ // Encrypted data should be present
38
+ expect(encryptedData.length).toBeGreaterThan(0);
39
+
40
+ // Verify randomBytes was called to generate an IV
41
+ expect(randomBytes).toHaveBeenCalledWith(16);
42
+ });
43
+ });
44
+
45
+ describe("decrypt", () => {
46
+ test("decrypts encrypted string back to original plaintext", () => {
47
+ const plaintext = "sensitive data to encrypt and decrypt";
48
+ const encrypted = cipher.encrypt(plaintext, mockConfig);
49
+ const decrypted = cipher.decrypt(encrypted, mockConfig);
50
+
51
+ expect(decrypted).toBe(plaintext);
52
+ });
53
+ });
54
+
55
+ test("encrypt and decrypt cycle works for complex data", () => {
56
+ const complexData = JSON.stringify({
57
+ id: 12345,
58
+ name: "Test User",
59
+ email: "test@example.com",
60
+ permissions: ["admin", "editor"],
61
+ metadata: {
62
+ lastLogin: "2024-05-15T12:30:00Z",
63
+ browser: "Chrome"
64
+ }
65
+ });
66
+
67
+ const encrypted = cipher.encrypt(complexData, mockConfig);
68
+ const decrypted = cipher.decrypt(encrypted, mockConfig);
69
+
70
+ expect(decrypted).toBe(complexData);
71
+ expect(JSON.parse(decrypted)).toEqual(JSON.parse(complexData));
72
+ });
73
+ });