opacacms 0.1.1 → 0.1.2

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 (212) hide show
  1. package/package.json +36 -1
  2. package/bun.lock +0 -34
  3. package/global.d.ts +0 -11
  4. package/src/admin/api-client.ts +0 -63
  5. package/src/admin/auth-client.ts +0 -40
  6. package/src/admin/custom-field.ts +0 -179
  7. package/src/admin/index.ts +0 -15
  8. package/src/admin/react.tsx +0 -72
  9. package/src/admin/router.ts +0 -9
  10. package/src/admin/stores/admin-queries.ts +0 -121
  11. package/src/admin/stores/auth.ts +0 -61
  12. package/src/admin/stores/column-visibility.ts +0 -67
  13. package/src/admin/stores/config.ts +0 -15
  14. package/src/admin/stores/media.ts +0 -95
  15. package/src/admin/stores/query.ts +0 -13
  16. package/src/admin/stores/ui.ts +0 -29
  17. package/src/admin/ui/admin-client.tsx +0 -283
  18. package/src/admin/ui/admin-layout.tsx +0 -276
  19. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +0 -141
  20. package/src/admin/ui/components/DataDetailSheet.tsx +0 -141
  21. package/src/admin/ui/components/DataDetailView.tsx +0 -175
  22. package/src/admin/ui/components/Table.tsx +0 -67
  23. package/src/admin/ui/components/fields/ArrayField.tsx +0 -166
  24. package/src/admin/ui/components/fields/BlocksField.tsx +0 -202
  25. package/src/admin/ui/components/fields/BooleanField.tsx +0 -50
  26. package/src/admin/ui/components/fields/CollapsibleField.tsx +0 -75
  27. package/src/admin/ui/components/fields/DateField.tsx +0 -45
  28. package/src/admin/ui/components/fields/FileField.tsx +0 -322
  29. package/src/admin/ui/components/fields/GroupField.tsx +0 -50
  30. package/src/admin/ui/components/fields/JoinField.tsx +0 -23
  31. package/src/admin/ui/components/fields/NumberField.tsx +0 -46
  32. package/src/admin/ui/components/fields/RadioField.tsx +0 -62
  33. package/src/admin/ui/components/fields/RelationshipField.tsx +0 -278
  34. package/src/admin/ui/components/fields/RowField.tsx +0 -40
  35. package/src/admin/ui/components/fields/SelectField.tsx +0 -59
  36. package/src/admin/ui/components/fields/TabsField.tsx +0 -101
  37. package/src/admin/ui/components/fields/TextAreaField.tsx +0 -54
  38. package/src/admin/ui/components/fields/TextField.tsx +0 -49
  39. package/src/admin/ui/components/fields/VirtualField.tsx +0 -53
  40. package/src/admin/ui/components/fields/index.tsx +0 -371
  41. package/src/admin/ui/components/fields/richtext-editor/index.tsx +0 -211
  42. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +0 -142
  43. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +0 -95
  44. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +0 -226
  45. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +0 -16
  46. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +0 -184
  47. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +0 -240
  48. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +0 -40
  49. package/src/admin/ui/components/fields/utils.ts +0 -1
  50. package/src/admin/ui/components/link.tsx +0 -41
  51. package/src/admin/ui/components/media/AssetManagerModal.tsx +0 -334
  52. package/src/admin/ui/components/toast.tsx +0 -72
  53. package/src/admin/ui/components/ui/accordion.tsx +0 -51
  54. package/src/admin/ui/components/ui/alert-dialog.tsx +0 -98
  55. package/src/admin/ui/components/ui/blocks.tsx +0 -32
  56. package/src/admin/ui/components/ui/breadcrumbs.tsx +0 -59
  57. package/src/admin/ui/components/ui/button.tsx +0 -26
  58. package/src/admin/ui/components/ui/collapsible.tsx +0 -124
  59. package/src/admin/ui/components/ui/dialog.tsx +0 -79
  60. package/src/admin/ui/components/ui/group.tsx +0 -20
  61. package/src/admin/ui/components/ui/index.ts +0 -17
  62. package/src/admin/ui/components/ui/input.tsx +0 -12
  63. package/src/admin/ui/components/ui/join.tsx +0 -53
  64. package/src/admin/ui/components/ui/label.tsx +0 -11
  65. package/src/admin/ui/components/ui/radio-group.tsx +0 -75
  66. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +0 -122
  67. package/src/admin/ui/components/ui/relationship.tsx +0 -58
  68. package/src/admin/ui/components/ui/scroll-area.tsx +0 -19
  69. package/src/admin/ui/components/ui/select.tsx +0 -187
  70. package/src/admin/ui/components/ui/separator.tsx +0 -21
  71. package/src/admin/ui/components/ui/sheet.tsx +0 -106
  72. package/src/admin/ui/components/ui/tabs.tsx +0 -116
  73. package/src/admin/ui/components/ui/utils.ts +0 -3
  74. package/src/admin/ui/hooks/use-debounce.ts +0 -15
  75. package/src/admin/ui/styles/_locale-switcher.scss +0 -33
  76. package/src/admin/ui/styles/accordion.scss +0 -60
  77. package/src/admin/ui/styles/animations.scss +0 -41
  78. package/src/admin/ui/styles/asset-manager.scss +0 -547
  79. package/src/admin/ui/styles/badge.scss +0 -13
  80. package/src/admin/ui/styles/base.scss +0 -22
  81. package/src/admin/ui/styles/button.scss +0 -161
  82. package/src/admin/ui/styles/card.scss +0 -13
  83. package/src/admin/ui/styles/collapsible.scss +0 -75
  84. package/src/admin/ui/styles/data-detail.scss +0 -92
  85. package/src/admin/ui/styles/dialog.scss +0 -102
  86. package/src/admin/ui/styles/empty-state.scss +0 -22
  87. package/src/admin/ui/styles/group.scss +0 -19
  88. package/src/admin/ui/styles/index.scss +0 -33
  89. package/src/admin/ui/styles/input.scss +0 -80
  90. package/src/admin/ui/styles/label.scss +0 -12
  91. package/src/admin/ui/styles/layout.scss +0 -56
  92. package/src/admin/ui/styles/lexical.scss +0 -469
  93. package/src/admin/ui/styles/loading.scss +0 -102
  94. package/src/admin/ui/styles/media-registry.scss +0 -597
  95. package/src/admin/ui/styles/pagination.scss +0 -20
  96. package/src/admin/ui/styles/radio-group.scss +0 -66
  97. package/src/admin/ui/styles/row.scss +0 -17
  98. package/src/admin/ui/styles/scrollbar.scss +0 -36
  99. package/src/admin/ui/styles/select.scss +0 -121
  100. package/src/admin/ui/styles/separator.scss +0 -14
  101. package/src/admin/ui/styles/sheet.scss +0 -152
  102. package/src/admin/ui/styles/sidebar.scss +0 -148
  103. package/src/admin/ui/styles/switch.scss +0 -59
  104. package/src/admin/ui/styles/table.scss +0 -207
  105. package/src/admin/ui/styles/tabs.scss +0 -62
  106. package/src/admin/ui/styles/toast.scss +0 -45
  107. package/src/admin/ui/styles/variables.scss +0 -24
  108. package/src/admin/ui/views/collection-list-view.tsx +0 -720
  109. package/src/admin/ui/views/dashboard-view.tsx +0 -263
  110. package/src/admin/ui/views/document-edit-view.tsx +0 -384
  111. package/src/admin/ui/views/global-edit-view.tsx +0 -226
  112. package/src/admin/ui/views/init-view.tsx +0 -182
  113. package/src/admin/ui/views/login-view.tsx +0 -123
  114. package/src/admin/ui/views/media-registry-view.tsx +0 -1104
  115. package/src/admin/ui/views/settings-view.tsx +0 -729
  116. package/src/admin/webcomponent.tsx +0 -15
  117. package/src/auth/index.ts +0 -194
  118. package/src/auth/migrations.ts +0 -87
  119. package/src/auth/premissions.ts +0 -46
  120. package/src/cli/commands/generate-types.ts +0 -116
  121. package/src/cli/commands/init.ts +0 -95
  122. package/src/cli/commands/migrate-commands.ts +0 -160
  123. package/src/cli/commands/seed-command.ts +0 -11
  124. package/src/cli/d1-mock.ts +0 -101
  125. package/src/cli/index.test.ts +0 -84
  126. package/src/cli/index.ts +0 -183
  127. package/src/cli/r2-mock.ts +0 -217
  128. package/src/cli/seeding.ts +0 -409
  129. package/src/client.ts +0 -181
  130. package/src/config-utils.ts +0 -102
  131. package/src/config.ts +0 -49
  132. package/src/db/adapter.ts +0 -53
  133. package/src/db/better-sqlite.ts +0 -630
  134. package/src/db/bun-sqlite.ts +0 -646
  135. package/src/db/d1.ts +0 -711
  136. package/src/db/index.ts +0 -2
  137. package/src/db/kysely/data-mapper.ts +0 -142
  138. package/src/db/kysely/field-mapper.ts +0 -148
  139. package/src/db/kysely/migration-generator.ts +0 -223
  140. package/src/db/kysely/query-builder.ts +0 -92
  141. package/src/db/kysely/schema-builder.ts +0 -439
  142. package/src/db/kysely/sql-utils.ts +0 -13
  143. package/src/db/migration.ts +0 -40
  144. package/src/db/postgres.ts +0 -621
  145. package/src/db/sqlite.ts +0 -658
  146. package/src/db/system-schema.ts +0 -121
  147. package/src/index.ts +0 -11
  148. package/src/runtimes/README.md +0 -59
  149. package/src/runtimes/bun.ts +0 -49
  150. package/src/runtimes/cloudflare-workers.ts +0 -38
  151. package/src/runtimes/next.ts +0 -26
  152. package/src/runtimes/node.ts +0 -52
  153. package/src/schema/collection.ts +0 -184
  154. package/src/schema/fields/base.ts +0 -164
  155. package/src/schema/fields/index.ts +0 -427
  156. package/src/schema/global.ts +0 -145
  157. package/src/schema/index.ts +0 -4
  158. package/src/schema/infer.ts +0 -72
  159. package/src/server/admin-router.ts +0 -20
  160. package/src/server/admin.ts +0 -142
  161. package/src/server/assets.ts +0 -306
  162. package/src/server/collection-router.ts +0 -55
  163. package/src/server/handlers.ts +0 -722
  164. package/src/server/middlewares/admin.ts +0 -27
  165. package/src/server/middlewares/auth.ts +0 -89
  166. package/src/server/middlewares/context.ts +0 -17
  167. package/src/server/middlewares/cors.ts +0 -24
  168. package/src/server/middlewares/database-init.ts +0 -74
  169. package/src/server/middlewares/rate-limit.ts +0 -71
  170. package/src/server/router.ts +0 -47
  171. package/src/server/setup-middlewares.ts +0 -58
  172. package/src/server/system-router.ts +0 -35
  173. package/src/server.ts +0 -9
  174. package/src/storage/adapters/cloudflare-r2.ts +0 -136
  175. package/src/storage/adapters/local.ts +0 -146
  176. package/src/storage/adapters/s3.ts +0 -186
  177. package/src/storage/errors.ts +0 -46
  178. package/src/storage/index.ts +0 -6
  179. package/src/storage/types.ts +0 -39
  180. package/src/types.ts +0 -605
  181. package/src/utils/lexical.ts +0 -37
  182. package/src/utils/logger.ts +0 -73
  183. package/src/validation.ts +0 -429
  184. package/src/validator.ts +0 -179
  185. package/test/admin-custom-field.test.ts +0 -162
  186. package/test/admin-react-field.test.tsx +0 -134
  187. package/test/api-features.test.ts +0 -78
  188. package/test/api.test.ts +0 -178
  189. package/test/auth.test.ts +0 -62
  190. package/test/cli-integration.test.ts +0 -148
  191. package/test/cli.test.ts +0 -25
  192. package/test/db/postgres.test.ts +0 -95
  193. package/test/db/sqlite-filter.test.ts +0 -53
  194. package/test/db/sqlite.test.ts +0 -82
  195. package/test/engine-features.test.ts +0 -79
  196. package/test/globals.test.ts +0 -74
  197. package/test/integration-tmp/db-app/opacacms.config.ts +0 -15
  198. package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +0 -25
  199. package/test/integration-tmp/my-test-app/index.ts +0 -8
  200. package/test/integration-tmp/my-test-app/opacacms.config.ts +0 -16
  201. package/test/integration-tmp/my-test-app/package.json +0 -12
  202. package/test/populate.test.ts +0 -79
  203. package/test/runtimes.test.ts +0 -43
  204. package/test/schema-builder.test.ts +0 -107
  205. package/test/schema-features.test.ts +0 -63
  206. package/test/seeding.test.ts +0 -68
  207. package/test/storage/local.test.ts +0 -72
  208. package/test/storage/s3.test.ts +0 -60
  209. package/test/structural-data.test.ts +0 -100
  210. package/test/test-setup.ts +0 -11
  211. package/test/validation.test.ts +0 -162
  212. package/tsconfig.json +0 -42
@@ -1,162 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
2
- import { GlobalWindow } from "happy-dom";
3
- import { defineCustomField } from "../src/admin/custom-field";
4
-
5
- describe("defineCustomField Adapter", () => {
6
- let window: GlobalWindow;
7
-
8
- beforeEach(() => {
9
- window = new GlobalWindow();
10
- // Simulate browser environment globally for the tests
11
- (globalThis as any).window = window;
12
- (globalThis as any).document = window.document;
13
- (globalThis as any).customElements = window.customElements;
14
- (globalThis as any).HTMLElement = window.HTMLElement;
15
- (globalThis as any).CustomEvent = window.CustomEvent;
16
- });
17
-
18
- afterEach(() => {
19
- // Clean up globals
20
- (globalThis as any).window = undefined;
21
- (globalThis as any).document = undefined;
22
- (globalThis as any).customElements = undefined;
23
- (globalThis as any).HTMLElement = undefined;
24
- (globalThis as any).CustomEvent = undefined;
25
- });
26
-
27
- it("should define a new custom element in the registry", () => {
28
- defineCustomField("my-test-field", { mount: () => { } });
29
- expect(customElements.get("my-test-field")).toBeDefined();
30
- });
31
-
32
- it("should call the mount hook when inserted into the DOM", () => {
33
- const mountMock = mock((container, props) => { });
34
- defineCustomField("my-mounted-field", { mount: mountMock });
35
-
36
- const el = document.createElement("my-mounted-field");
37
- document.body.appendChild(el);
38
-
39
- expect(mountMock).toHaveBeenCalledTimes(1);
40
- expect(mountMock.mock.calls[0]![0]).toBe(el); // Passes container
41
-
42
- // Checks props object structure
43
- const props = mountMock.mock.calls[0]![1];
44
- expect(props).toHaveProperty("value");
45
- expect(props).toHaveProperty("fieldConfig");
46
- expect(props).toHaveProperty("disabled", false);
47
- expect(props).toHaveProperty("readOnly", false);
48
- expect(typeof props.onChange).toBe("function");
49
-
50
- document.body.removeChild(el);
51
- });
52
-
53
- it("should parse boolean attributes correctly on mount", () => {
54
- const mountMock = mock((container, props) => { });
55
- defineCustomField("my-boolean-field", { mount: mountMock });
56
-
57
- const el = document.createElement("my-boolean-field");
58
- el.setAttribute("data-disabled", "true");
59
- el.setAttribute("data-readonly", "true");
60
- document.body.appendChild(el);
61
-
62
- const props = mountMock.mock.calls[0]![1];
63
- expect(props.disabled).toBe(true);
64
- expect(props.readOnly).toBe(true);
65
- });
66
-
67
- it("should call update hook when value property changes", () => {
68
- const updateMock = mock((container, props) => { });
69
- defineCustomField("my-update-field", {
70
- mount: () => { },
71
- update: updateMock,
72
- });
73
-
74
- const el = document.createElement("my-update-field");
75
- document.body.appendChild(el);
76
-
77
- // Initial value change
78
- (el as any).value = "new value";
79
-
80
- expect(updateMock).toHaveBeenCalledTimes(1);
81
- expect(updateMock.mock.calls[0]![1].value).toBe("new value");
82
- });
83
-
84
- it("should not call update hook if value property is identical", () => {
85
- const updateMock = mock((container, props) => { });
86
- defineCustomField("my-update-cache-field", {
87
- mount: () => { },
88
- update: updateMock,
89
- });
90
-
91
- const el = document.createElement("my-update-cache-field");
92
- (el as any).value = "initial"; // Set before mount
93
- document.body.appendChild(el);
94
-
95
- // Identical value change
96
- (el as any).value = "initial";
97
- expect(updateMock).toHaveBeenCalledTimes(0);
98
-
99
- // Real change
100
- (el as any).value = "new value";
101
- expect(updateMock).toHaveBeenCalledTimes(1);
102
- });
103
-
104
- it("should call update hook when fieldConfig property changes", () => {
105
- const updateMock = mock((container, props) => { });
106
- defineCustomField("my-config-field", {
107
- mount: () => { },
108
- update: updateMock,
109
- });
110
-
111
- const el = document.createElement("my-config-field");
112
- document.body.appendChild(el);
113
-
114
- (el as any).fieldConfig = { slug: "test" };
115
-
116
- expect(updateMock).toHaveBeenCalledTimes(1);
117
- expect(updateMock.mock.calls[0]![1].fieldConfig).toEqual({ slug: "test" });
118
- });
119
-
120
- it("should dispatch opacachange event when onChange is called", () => {
121
- let internalOnChange: (v: any) => void = () => { };
122
-
123
- defineCustomField("my-event-field", {
124
- mount: (container, props) => {
125
- internalOnChange = props.onChange;
126
- },
127
- });
128
-
129
- const el = document.createElement("my-event-field");
130
- document.body.appendChild(el);
131
-
132
- const eventListener = mock((e: CustomEvent) => {
133
- expect(e.detail.value).toBe("hello world");
134
- });
135
-
136
- // Listen to the document, because bubbles and composed should be true
137
- document.addEventListener("opacachange", eventListener as unknown as EventListener);
138
-
139
- // Trigger the abstraction
140
- internalOnChange("hello world");
141
-
142
- expect(eventListener).toHaveBeenCalledTimes(1);
143
-
144
- document.removeEventListener("opacachange", eventListener as unknown as EventListener);
145
- });
146
-
147
- it("should call the unmount hook when removed from the DOM", () => {
148
- const unmountMock = mock((container) => { });
149
- defineCustomField("my-unmount-field", {
150
- mount: () => { },
151
- unmount: unmountMock,
152
- });
153
-
154
- const el = document.createElement("my-unmount-field");
155
- document.body.appendChild(el);
156
- expect(unmountMock).toHaveBeenCalledTimes(0);
157
-
158
- // Remove from DOM
159
- document.body.removeChild(el);
160
- expect(unmountMock).toHaveBeenCalledTimes(1);
161
- });
162
- });
@@ -1,134 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
2
- import { GlobalWindow } from 'happy-dom';
3
- import React from 'react';
4
- import { defineReactField } from '../src/admin/react';
5
-
6
- describe('defineReactField React Adapter', () => {
7
- let window: GlobalWindow;
8
-
9
- beforeEach(() => {
10
- window = new GlobalWindow();
11
- // Simulate browser environment globally for the tests
12
- (globalThis as any).window = window;
13
- (globalThis as any).document = window.document;
14
- (globalThis as any).customElements = window.customElements;
15
- (globalThis as any).HTMLElement = window.HTMLElement;
16
- (globalThis as any).CustomEvent = window.CustomEvent;
17
- });
18
-
19
- afterEach(() => {
20
- // Clean up globals
21
- (globalThis as any).window = undefined;
22
- (globalThis as any).document = undefined;
23
- (globalThis as any).customElements = undefined;
24
- (globalThis as any).HTMLElement = undefined;
25
- (globalThis as any).CustomEvent = undefined;
26
- });
27
-
28
- it('should define a React custom field successfully', async () => {
29
- const renderSpy = mock((props) => {
30
- return React.createElement(
31
- 'div',
32
- { id: 'test-react-field' },
33
- props.value,
34
- );
35
- });
36
-
37
- defineReactField('my-test-react', renderSpy);
38
-
39
- const el = document.createElement('my-test-react');
40
- (el as any).value = 'Hello React';
41
- document.body.appendChild(el);
42
-
43
- // Wait for React to asynchronously flush renders to DOM
44
- await new Promise((resolve) => setTimeout(resolve, 50));
45
-
46
- expect(renderSpy).toHaveBeenCalled();
47
- expect(renderSpy.mock.calls[0]![0].value).toBe('Hello React');
48
-
49
- // Check if React actually created the child node
50
- const child = el.querySelector('#test-react-field');
51
- expect(child).not.toBeNull();
52
- expect(child?.textContent).toBe('Hello React');
53
- });
54
-
55
- it('should re-render React component when custom element props change', async () => {
56
- const renderSpy = mock((props) => {
57
- return React.createElement('div', null, `error: ${props.error}`);
58
- });
59
-
60
- defineReactField('my-error-react', renderSpy);
61
-
62
- const el = document.createElement('my-error-react');
63
- document.body.appendChild(el);
64
-
65
- await new Promise((resolve) => setTimeout(resolve, 20));
66
-
67
- // Update error prop externally, mimicking CMS behavior
68
- (el as any).error = 'Invalid slug format';
69
-
70
- await new Promise((resolve) => setTimeout(resolve, 50));
71
-
72
- expect(renderSpy).toHaveBeenCalledTimes(2);
73
- expect(renderSpy.mock.calls[1]![0].error).toBe('Invalid slug format');
74
- expect(el.textContent).toBe('error: Invalid slug format');
75
- });
76
-
77
- it('should correctly receive parentData prop', async () => {
78
- const renderSpy = mock((props) => {
79
- return React.createElement(
80
- 'div',
81
- null,
82
- props.parentData?.country || 'none',
83
- );
84
- });
85
-
86
- defineReactField('my-parent-react', renderSpy);
87
-
88
- const el = document.createElement('my-parent-react');
89
- (el as any).parentData = { country: 'Brazil', taxCode: '123' };
90
- document.body.appendChild(el);
91
-
92
- await new Promise((resolve) => setTimeout(resolve, 50));
93
-
94
- expect(renderSpy.mock.calls[0]![0].parentData).toEqual({
95
- country: 'Brazil',
96
- taxCode: '123',
97
- });
98
- expect(el.textContent).toBe('Brazil');
99
- });
100
-
101
- it('should unmount React root to prevent memory leaks when element is removed', async () => {
102
- let effectRan = false;
103
- let effectCleanedUp = false;
104
-
105
- // A real React component utilizing Hooks
106
- const MyComponent = () => {
107
- React.useEffect(() => {
108
- effectRan = true;
109
- return () => {
110
- effectCleanedUp = true;
111
- };
112
- }, []);
113
- return React.createElement('span', null, 'Mounting');
114
- };
115
-
116
- defineReactField('my-unmount-react', MyComponent);
117
-
118
- const el = document.createElement('my-unmount-react');
119
- document.body.appendChild(el);
120
-
121
- // Wait for React render + useEffect
122
- await new Promise((resolve) => setTimeout(resolve, 50));
123
- expect(effectRan).toBe(true);
124
-
125
- // Remove the custom element
126
- document.body.removeChild(el);
127
-
128
- // Wait for the asynchronous unmount timeout (from our helper implementation)
129
- await new Promise((resolve) => setTimeout(resolve, 50));
130
-
131
- expect(effectCleanedUp).toBe(true);
132
- expect((el as any)._opacaReactRoot).toBeUndefined(); // Check root reference deleted
133
- });
134
- });
@@ -1,78 +0,0 @@
1
- import { describe, expect, mock, test } from "bun:test";
2
- import { Hono } from "hono";
3
- import { createBunSQLiteAdapter } from "../src/db/bun-sqlite";
4
- import { Collection, Field } from "../src/schema";
5
- import { createAPIRouter } from "../src/server/router";
6
- import type { OpacaConfig } from "../src/types";
7
-
8
- describe("API Schema Features Integration", () => {
9
- const dbPath = ":memory:";
10
- const db = createBunSQLiteAdapter(dbPath);
11
-
12
- const postsCollection = Collection.create("posts")
13
- .fields([Field.text("title").required(), Field.slug("slug").from("title")])
14
- .webhooks([
15
- {
16
- events: ["afterCreate"],
17
- url: "https://example.com/api/webhook",
18
- },
19
- ])
20
- .build();
21
-
22
- const config = {
23
- db,
24
- collections: [postsCollection],
25
- } as unknown as OpacaConfig;
26
-
27
- const apiRouter = createAPIRouter(config);
28
- const app = new Hono();
29
- app.route("/", apiRouter);
30
-
31
- test("Should auto-generate slug on POST if not provided", async () => {
32
- // Wait for the migration logic or use db.migrate directly.
33
- await db.migrate([postsCollection]);
34
-
35
- const req = new Request("http://localhost/api/posts", {
36
- method: "POST",
37
- headers: { "Content-Type": "application/json" },
38
- body: JSON.stringify({
39
- title: "Hello World 2024!",
40
- }),
41
- });
42
-
43
- const res = await app.fetch(req);
44
- expect(res.status).toBe(201);
45
-
46
- const json = (await res.json()) as any;
47
- expect(json.title).toBe("Hello World 2024!");
48
- expect(json.slug).toBe("hello-world-2024");
49
- });
50
-
51
- test("Should call webhook on afterCreate event", async () => {
52
- const globalFetch = globalThis.fetch;
53
- const fetchMock = mock(async () => new Response());
54
- globalThis.fetch = fetchMock as any;
55
-
56
- const req = new Request("http://localhost/api/posts", {
57
- method: "POST",
58
- headers: { "Content-Type": "application/json" },
59
- body: JSON.stringify({
60
- title: "Webhook Test",
61
- }),
62
- });
63
-
64
- const res = await app.fetch(req);
65
- expect(res.status).toBe(201);
66
-
67
- // Wait a brief moment for the asynchronous fetch to execute
68
- await new Promise((resolve) => setTimeout(resolve, 50));
69
-
70
- expect(fetchMock).toHaveBeenCalled();
71
- const mockCallArgs = fetchMock.mock.calls[0] as any;
72
- expect(mockCallArgs[0]).toBe("https://example.com/api/webhook");
73
- expect(mockCallArgs[1].method).toBe("POST");
74
- expect(JSON.parse(mockCallArgs[1].body).title).toBe("Webhook Test");
75
-
76
- globalThis.fetch = globalFetch;
77
- });
78
- });
package/test/api.test.ts DELETED
@@ -1,178 +0,0 @@
1
- import { beforeEach, describe, expect, it } from "bun:test";
2
- import { BunSQLiteAdapter } from "../src/db/bun-sqlite";
3
- import { createAPIRouter } from "../src/server/router";
4
- import type { Collection, OpacaConfig } from "../src/types";
5
-
6
- describe("API Router", () => {
7
- let db: BunSQLiteAdapter;
8
- let app: any;
9
-
10
- const testCollection: Collection = {
11
- slug: "posts",
12
- fields: [
13
- { name: "title", type: "text" },
14
- { name: "views", type: "number" },
15
- ],
16
- };
17
-
18
- const config: OpacaConfig = {
19
- db: null as any, // Will set later
20
- collections: [testCollection],
21
- };
22
-
23
- beforeEach(async () => {
24
- db = new BunSQLiteAdapter(":memory:");
25
- await db.migrate([testCollection]);
26
- config.db = db;
27
- process.env.BETTER_AUTH_URL = "http://localhost:3000";
28
- process.env.BETTER_AUTH_SECRET = "secret_for_test";
29
- app = createAPIRouter(config);
30
- });
31
-
32
- it("GET /:slug should return empty list initially", async () => {
33
- const res = await app.request("/api/posts");
34
- expect(res.status).toBe(200);
35
- expect(res.headers.get("X-Powered-By")).toBe("OpacaCMS");
36
- const body = await res.json();
37
- expect(body.docs).toEqual([]);
38
- expect(body.totalDocs).toBe(0);
39
- });
40
-
41
- it("POST /:slug should create a document", async () => {
42
- const res = await app.request("/api/posts", {
43
- method: "POST",
44
- headers: { "Content-Type": "application/json" },
45
- body: JSON.stringify({ title: "New API Post", views: 100 }),
46
- });
47
-
48
- expect(res.status).toBe(201);
49
- const doc = await res.json();
50
- expect(doc.title).toBe("New API Post");
51
- expect(doc.id).toBeDefined();
52
-
53
- // Verify persistence
54
- const saved = await db.findOne("posts", { id: (doc as any).id });
55
- expect(saved).toBeDefined();
56
- });
57
-
58
- it("GET /:slug/:id should return a document", async () => {
59
- const doc = await db.create("posts", { title: "Find Me" });
60
-
61
- const res = await app.request(`/api/posts/${(doc as any).id}`);
62
- expect(res.status).toBe(200);
63
- const body = await res.json();
64
- expect(body.title).toBe("Find Me");
65
- });
66
-
67
- it("PATCH /:slug/:id should update a document", async () => {
68
- const doc = await db.create("posts", { title: "Update Me" });
69
-
70
- const res = await app.request(`/api/posts/${(doc as any).id}`, {
71
- method: "PATCH",
72
- headers: { "Content-Type": "application/json" },
73
- body: JSON.stringify({ title: "Updated" }),
74
- });
75
-
76
- expect(res.status).toBe(200);
77
- const body = (await res.json()) as any;
78
- expect(body.title).toBe("Updated");
79
-
80
- const check = await db.findOne("posts", { id: (doc as any).id });
81
- expect((check as any)?.title).toBe("Updated");
82
- });
83
-
84
- it("DELETE /:slug/:id should delete a document", async () => {
85
- const doc = await db.create("posts", { title: "Delete Me" });
86
-
87
- const res = await app.request(`/api/posts/${(doc as any).id}`, {
88
- method: "DELETE",
89
- });
90
-
91
- expect(res.status).toBe(200);
92
-
93
- const check = await db.findOne("posts", { id: (doc as any).id });
94
- expect(check).toBeNull();
95
- });
96
-
97
- it("POST /:slug should fail validation if required field missing", async () => {
98
- const requiredCollection: Collection = {
99
- slug: "reqposts",
100
- fields: [{ name: "title", type: "text", required: true }],
101
- };
102
-
103
- await db.migrate([requiredCollection]);
104
- const newConfig = { ...config, collections: [...config.collections, requiredCollection] };
105
- const newApp = createAPIRouter(newConfig);
106
-
107
- const res = await newApp.request("/api/reqposts", {
108
- method: "POST",
109
- headers: { "Content-Type": "application/json" },
110
- body: JSON.stringify({}), // Missing title
111
- });
112
-
113
- expect(res.status).toBe(400);
114
- const body = (await res.json()) as any;
115
- expect(body.errors).toBeDefined();
116
- });
117
-
118
- it("POST /:slug should fail validation if wrong type", async () => {
119
- const res = await app.request("/api/posts", {
120
- method: "POST",
121
- headers: { "Content-Type": "application/json" },
122
- body: JSON.stringify({ title: 123 }), // title is text, expected string
123
- });
124
-
125
- expect(res.status).toBe(400);
126
- });
127
-
128
- it("GET /:slug should support pagination", async () => {
129
- // Create 15 items
130
- for (let i = 1; i <= 15; i++) {
131
- await db.create("posts", { title: `Post ${i}`, views: i });
132
- }
133
-
134
- // Page 1
135
- const res1 = await app.request("/api/posts?limit=10&page=1");
136
- const body1 = await res1.json();
137
- expect(body1.docs.length).toBe(10);
138
- expect(body1.totalDocs).toBe(15);
139
- expect(body1.totalPages).toBe(2);
140
- expect(body1.page).toBe(1);
141
- expect(body1.hasNextPage).toBe(true);
142
-
143
- // Page 2
144
- const res2 = await app.request("/api/posts?limit=10&page=2");
145
- const body2 = await res2.json();
146
- expect(body2.docs.length).toBe(5);
147
- expect(body2.page).toBe(2);
148
- expect(body2.hasNextPage).toBe(false);
149
- });
150
-
151
- it("GET /:slug should support sorting", async () => {
152
- await db.create("posts", { title: "A", views: 10 });
153
- await db.create("posts", { title: "B", views: 20 });
154
-
155
- // Descending
156
- const resDesc = await app.request("/api/posts?sort=-views");
157
- const bodyDesc = await resDesc.json();
158
- expect(bodyDesc.docs[0].views).toBe(20);
159
-
160
- // Ascending
161
- const resAsc = await app.request("/api/posts?sort=views");
162
- const bodyAsc = await resAsc.json();
163
- expect(bodyAsc.docs[0].views).toBe(10);
164
- });
165
-
166
- it("GET /:slug should support advanced filters", async () => {
167
- await db.create("posts", { title: "Low", views: 5 });
168
- await db.create("posts", { title: "Medium", views: 15 });
169
- await db.create("posts", { title: "High", views: 25 });
170
-
171
- // views > 10
172
- // URL encoded: views[gt]=10
173
- const res = await app.request("/api/posts?views[gt]=10");
174
- const body = await res.json();
175
- expect(body.docs.length).toBe(2);
176
- expect(body.docs.find((d: any) => d.views === 5)).toBeUndefined();
177
- });
178
- });
package/test/auth.test.ts DELETED
@@ -1,62 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { Hono } from "hono";
3
- import { createBunSQLiteAdapter } from "../src/db/bun-sqlite";
4
- import { createAPIRouter } from "../src/server/router";
5
- import type { OpacaConfig } from "../src/types";
6
-
7
- // Set Env for better-auth defaults
8
- process.env.BETTER_AUTH_URL = "http://localhost/api/auth";
9
- process.env.BETTER_AUTH_SECRET = "test-secret-12345678901234567890"; // Must be long enough
10
-
11
- describe("Better Auth Integration", () => {
12
- const dbPath = ":memory:";
13
- const db = createBunSQLiteAdapter(dbPath);
14
-
15
- const config = {
16
- db,
17
- collections: [{ slug: "posts", fields: [] }],
18
- access: {
19
- roles: {
20
- editor: {
21
- posts: ["create", "read"],
22
- },
23
- },
24
- },
25
- trustedOrigins: ["http://test.local"],
26
- auth: {
27
- // Empty auth object will use all defaults
28
- },
29
- };
30
-
31
- // We need to wait for migrations?
32
- // Since runAuthMigrations is fire-and-forget in createAPIRouter, we might need a small delay or manual trigger for strict testing.
33
- // But for route existence check, it shouldn't matter.
34
-
35
- const apiRouter = createAPIRouter(config as any);
36
- const app = new Hono();
37
- app.route("/", apiRouter);
38
-
39
- // TODO: Investigate why this returns 404 in bun test environment despite handler being hit.
40
- // Admin protection check confirms auth.api is working.
41
- test.skip("Auth routes should be mounted", async () => {
42
- const req = new Request("http://localhost/api/auth/session");
43
- const res = await app.fetch(req);
44
-
45
- // Even if session is null, it should return 200 OK with null body or similar,
46
- // OR 404 if route not found.
47
- expect(res.status).not.toBe(404);
48
-
49
- // better-auth /session usually returns 200 with null if no session
50
- // or 401 depending on config, but definitely not 404.
51
- });
52
-
53
- test("Admin routes should be protected", async () => {
54
- const req = new Request("http://localhost/api/__admin/collections");
55
- const res = await app.fetch(req);
56
-
57
- // Should be 401 Unauthorized
58
- expect(res.status).toBe(401);
59
- const body = await res.json();
60
- expect(body).toEqual({ message: "Unauthorized" });
61
- });
62
- });