opacacms 0.1.11 → 0.1.13

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 (246) hide show
  1. package/dist/admin/index.js +9464 -21
  2. package/dist/admin/webcomponent.d.ts +1 -1
  3. package/dist/admin/webcomponent.js +9620 -6
  4. package/dist/admin.css +1 -0
  5. package/dist/{chunk-6dhs73zq.js → chunk-0am1m47g.js} +1 -1
  6. package/dist/{chunk-0nf7fe26.js → chunk-0d9aqz6z.js} +1 -1
  7. package/dist/{chunk-cvdd4eqh.js → chunk-2kyhqvhc.js} +5 -1
  8. package/dist/{chunk-gjjcc4hm.js → chunk-2z8wxx9g.js} +21 -6
  9. package/dist/{chunk-xg35h5a3.js → chunk-7fyepksb.js} +1 -1
  10. package/dist/{chunk-njytmdb4.js → chunk-pxh5encs.js} +34 -24
  11. package/dist/{chunk-n8aekdnr.js → chunk-qkn1ykrj.js} +33 -23
  12. package/dist/{chunk-kwp83w8b.js → chunk-wmvjvn7b.js} +4 -4
  13. package/dist/{chunk-qrt22f6e.js → chunk-wq314kkx.js} +35 -25
  14. package/dist/{chunk-eqtsfyjf.js → chunk-x2ejaftz.js} +52 -28
  15. package/dist/{chunk-6ew02s0c.js → chunk-xtwc125q.js} +18 -18
  16. package/dist/cli/index.js +5 -5
  17. package/dist/db/better-sqlite.d.ts +1 -0
  18. package/dist/db/better-sqlite.js +3 -3
  19. package/dist/db/bun-sqlite.d.ts +1 -0
  20. package/dist/db/bun-sqlite.js +3 -3
  21. package/dist/db/d1.js +3 -3
  22. package/dist/db/index.d.ts +3 -0
  23. package/dist/db/index.js +17 -13
  24. package/dist/db/postgres.js +3 -3
  25. package/dist/db/sqlite.js +3 -3
  26. package/dist/runtimes/bun.js +2 -2
  27. package/dist/runtimes/cloudflare-workers.js +2 -2
  28. package/dist/runtimes/next.js +2 -2
  29. package/dist/runtimes/node.js +2 -2
  30. package/dist/server.js +2 -2
  31. package/package.json +8 -2
  32. package/bun.lock +0 -34
  33. package/dist/admin/index.css +0 -47
  34. package/dist/api.d.ts +0 -6
  35. package/dist/api.js +0 -27
  36. package/dist/chunk-2zm8cy1w.js +0 -9482
  37. package/global.d.ts +0 -11
  38. package/src/admin/api-client.ts +0 -63
  39. package/src/admin/auth-client.ts +0 -40
  40. package/src/admin/custom-field.ts +0 -179
  41. package/src/admin/index.ts +0 -15
  42. package/src/admin/react.tsx +0 -72
  43. package/src/admin/router.ts +0 -9
  44. package/src/admin/stores/admin-queries.ts +0 -121
  45. package/src/admin/stores/auth.ts +0 -61
  46. package/src/admin/stores/column-visibility.ts +0 -67
  47. package/src/admin/stores/config.ts +0 -15
  48. package/src/admin/stores/media.ts +0 -95
  49. package/src/admin/stores/query.ts +0 -13
  50. package/src/admin/stores/ui.ts +0 -29
  51. package/src/admin/ui/admin-client.tsx +0 -283
  52. package/src/admin/ui/admin-layout.tsx +0 -276
  53. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +0 -141
  54. package/src/admin/ui/components/DataDetailSheet.tsx +0 -141
  55. package/src/admin/ui/components/DataDetailView.tsx +0 -175
  56. package/src/admin/ui/components/Table.tsx +0 -67
  57. package/src/admin/ui/components/fields/ArrayField.tsx +0 -166
  58. package/src/admin/ui/components/fields/BlocksField.tsx +0 -202
  59. package/src/admin/ui/components/fields/BooleanField.tsx +0 -50
  60. package/src/admin/ui/components/fields/CollapsibleField.tsx +0 -75
  61. package/src/admin/ui/components/fields/DateField.tsx +0 -45
  62. package/src/admin/ui/components/fields/FileField.tsx +0 -322
  63. package/src/admin/ui/components/fields/GroupField.tsx +0 -50
  64. package/src/admin/ui/components/fields/JoinField.tsx +0 -23
  65. package/src/admin/ui/components/fields/NumberField.tsx +0 -46
  66. package/src/admin/ui/components/fields/RadioField.tsx +0 -62
  67. package/src/admin/ui/components/fields/RelationshipField.tsx +0 -278
  68. package/src/admin/ui/components/fields/RowField.tsx +0 -40
  69. package/src/admin/ui/components/fields/SelectField.tsx +0 -59
  70. package/src/admin/ui/components/fields/TabsField.tsx +0 -101
  71. package/src/admin/ui/components/fields/TextAreaField.tsx +0 -54
  72. package/src/admin/ui/components/fields/TextField.tsx +0 -49
  73. package/src/admin/ui/components/fields/VirtualField.tsx +0 -53
  74. package/src/admin/ui/components/fields/index.tsx +0 -371
  75. package/src/admin/ui/components/fields/richtext-editor/index.tsx +0 -211
  76. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +0 -142
  77. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +0 -95
  78. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +0 -226
  79. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +0 -16
  80. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +0 -184
  81. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +0 -240
  82. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +0 -40
  83. package/src/admin/ui/components/fields/utils.ts +0 -1
  84. package/src/admin/ui/components/link.tsx +0 -41
  85. package/src/admin/ui/components/media/AssetManagerModal.tsx +0 -334
  86. package/src/admin/ui/components/toast.tsx +0 -72
  87. package/src/admin/ui/components/ui/accordion.tsx +0 -51
  88. package/src/admin/ui/components/ui/alert-dialog.tsx +0 -98
  89. package/src/admin/ui/components/ui/blocks.tsx +0 -32
  90. package/src/admin/ui/components/ui/breadcrumbs.tsx +0 -59
  91. package/src/admin/ui/components/ui/button.tsx +0 -26
  92. package/src/admin/ui/components/ui/collapsible.tsx +0 -124
  93. package/src/admin/ui/components/ui/dialog.tsx +0 -79
  94. package/src/admin/ui/components/ui/group.tsx +0 -20
  95. package/src/admin/ui/components/ui/index.ts +0 -17
  96. package/src/admin/ui/components/ui/input.tsx +0 -12
  97. package/src/admin/ui/components/ui/join.tsx +0 -53
  98. package/src/admin/ui/components/ui/label.tsx +0 -11
  99. package/src/admin/ui/components/ui/radio-group.tsx +0 -75
  100. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +0 -122
  101. package/src/admin/ui/components/ui/relationship.tsx +0 -58
  102. package/src/admin/ui/components/ui/scroll-area.tsx +0 -19
  103. package/src/admin/ui/components/ui/select.tsx +0 -187
  104. package/src/admin/ui/components/ui/separator.tsx +0 -21
  105. package/src/admin/ui/components/ui/sheet.tsx +0 -106
  106. package/src/admin/ui/components/ui/tabs.tsx +0 -116
  107. package/src/admin/ui/components/ui/utils.ts +0 -3
  108. package/src/admin/ui/hooks/use-debounce.ts +0 -15
  109. package/src/admin/ui/styles/_locale-switcher.scss +0 -33
  110. package/src/admin/ui/styles/accordion.scss +0 -60
  111. package/src/admin/ui/styles/animations.scss +0 -41
  112. package/src/admin/ui/styles/asset-manager.scss +0 -547
  113. package/src/admin/ui/styles/badge.scss +0 -13
  114. package/src/admin/ui/styles/base.scss +0 -22
  115. package/src/admin/ui/styles/button.scss +0 -161
  116. package/src/admin/ui/styles/card.scss +0 -13
  117. package/src/admin/ui/styles/collapsible.scss +0 -75
  118. package/src/admin/ui/styles/data-detail.scss +0 -92
  119. package/src/admin/ui/styles/dialog.scss +0 -102
  120. package/src/admin/ui/styles/empty-state.scss +0 -22
  121. package/src/admin/ui/styles/group.scss +0 -19
  122. package/src/admin/ui/styles/index.scss +0 -33
  123. package/src/admin/ui/styles/input.scss +0 -80
  124. package/src/admin/ui/styles/label.scss +0 -12
  125. package/src/admin/ui/styles/layout.scss +0 -56
  126. package/src/admin/ui/styles/lexical.scss +0 -469
  127. package/src/admin/ui/styles/loading.scss +0 -102
  128. package/src/admin/ui/styles/media-registry.scss +0 -597
  129. package/src/admin/ui/styles/pagination.scss +0 -20
  130. package/src/admin/ui/styles/radio-group.scss +0 -66
  131. package/src/admin/ui/styles/row.scss +0 -17
  132. package/src/admin/ui/styles/scrollbar.scss +0 -36
  133. package/src/admin/ui/styles/select.scss +0 -121
  134. package/src/admin/ui/styles/separator.scss +0 -14
  135. package/src/admin/ui/styles/sheet.scss +0 -152
  136. package/src/admin/ui/styles/sidebar.scss +0 -148
  137. package/src/admin/ui/styles/switch.scss +0 -59
  138. package/src/admin/ui/styles/table.scss +0 -207
  139. package/src/admin/ui/styles/tabs.scss +0 -62
  140. package/src/admin/ui/styles/toast.scss +0 -45
  141. package/src/admin/ui/styles/variables.scss +0 -24
  142. package/src/admin/ui/views/collection-list-view.tsx +0 -720
  143. package/src/admin/ui/views/dashboard-view.tsx +0 -263
  144. package/src/admin/ui/views/document-edit-view.tsx +0 -384
  145. package/src/admin/ui/views/global-edit-view.tsx +0 -226
  146. package/src/admin/ui/views/init-view.tsx +0 -182
  147. package/src/admin/ui/views/login-view.tsx +0 -123
  148. package/src/admin/ui/views/media-registry-view.tsx +0 -1104
  149. package/src/admin/ui/views/settings-view.tsx +0 -729
  150. package/src/admin/webcomponent.tsx +0 -15
  151. package/src/api.ts +0 -9
  152. package/src/auth/index.ts +0 -194
  153. package/src/auth/migrations.ts +0 -87
  154. package/src/auth/premissions.ts +0 -46
  155. package/src/cli/commands/generate-types.ts +0 -116
  156. package/src/cli/commands/init.ts +0 -95
  157. package/src/cli/commands/migrate-commands.ts +0 -160
  158. package/src/cli/commands/seed-command.ts +0 -11
  159. package/src/cli/d1-mock.ts +0 -101
  160. package/src/cli/index.test.ts +0 -84
  161. package/src/cli/index.ts +0 -183
  162. package/src/cli/r2-mock.ts +0 -217
  163. package/src/cli/seeding.ts +0 -409
  164. package/src/client.ts +0 -181
  165. package/src/config-utils.ts +0 -102
  166. package/src/config.ts +0 -49
  167. package/src/db/adapter.ts +0 -53
  168. package/src/db/better-sqlite.ts +0 -632
  169. package/src/db/bun-sqlite.ts +0 -646
  170. package/src/db/d1.ts +0 -711
  171. package/src/db/index.ts +0 -6
  172. package/src/db/kysely/data-mapper.ts +0 -142
  173. package/src/db/kysely/field-mapper.ts +0 -148
  174. package/src/db/kysely/migration-generator.ts +0 -223
  175. package/src/db/kysely/query-builder.ts +0 -92
  176. package/src/db/kysely/schema-builder.ts +0 -439
  177. package/src/db/kysely/sql-utils.ts +0 -13
  178. package/src/db/postgres.ts +0 -621
  179. package/src/db/sqlite.ts +0 -660
  180. package/src/db/system-schema.ts +0 -121
  181. package/src/index.ts +0 -13
  182. package/src/runtimes/README.md +0 -59
  183. package/src/runtimes/bun.ts +0 -49
  184. package/src/runtimes/cloudflare-workers.ts +0 -38
  185. package/src/runtimes/next.ts +0 -26
  186. package/src/runtimes/node.ts +0 -52
  187. package/src/schema/collection.ts +0 -184
  188. package/src/schema/fields/base.ts +0 -164
  189. package/src/schema/fields/index.ts +0 -427
  190. package/src/schema/global.ts +0 -145
  191. package/src/schema/index.ts +0 -4
  192. package/src/schema/infer.ts +0 -72
  193. package/src/server/admin-router.ts +0 -20
  194. package/src/server/admin.ts +0 -142
  195. package/src/server/assets.ts +0 -306
  196. package/src/server/collection-router.ts +0 -55
  197. package/src/server/handlers.ts +0 -722
  198. package/src/server/middlewares/admin.ts +0 -27
  199. package/src/server/middlewares/auth.ts +0 -89
  200. package/src/server/middlewares/context.ts +0 -17
  201. package/src/server/middlewares/cors.ts +0 -24
  202. package/src/server/middlewares/database-init.ts +0 -74
  203. package/src/server/middlewares/rate-limit.ts +0 -77
  204. package/src/server/router.ts +0 -47
  205. package/src/server/setup-middlewares.ts +0 -58
  206. package/src/server/system-router.ts +0 -35
  207. package/src/server.ts +0 -9
  208. package/src/storage/adapters/cloudflare-r2.ts +0 -136
  209. package/src/storage/adapters/local.ts +0 -146
  210. package/src/storage/adapters/s3.ts +0 -186
  211. package/src/storage/errors.ts +0 -46
  212. package/src/storage/index.ts +0 -5
  213. package/src/storage/types.ts +0 -39
  214. package/src/types.ts +0 -577
  215. package/src/utils/lexical.ts +0 -37
  216. package/src/utils/logger.ts +0 -73
  217. package/src/validation.ts +0 -429
  218. package/src/validator.ts +0 -179
  219. package/test/admin-custom-field.test.ts +0 -162
  220. package/test/admin-react-field.test.tsx +0 -134
  221. package/test/api-features.test.ts +0 -78
  222. package/test/api.test.ts +0 -178
  223. package/test/auth.test.ts +0 -62
  224. package/test/cli-integration.test.ts +0 -148
  225. package/test/cli.test.ts +0 -25
  226. package/test/db/postgres.test.ts +0 -95
  227. package/test/db/sqlite-filter.test.ts +0 -53
  228. package/test/db/sqlite.test.ts +0 -82
  229. package/test/engine-features.test.ts +0 -79
  230. package/test/globals.test.ts +0 -74
  231. package/test/integration-tmp/db-app/opacacms.config.ts +0 -15
  232. package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +0 -25
  233. package/test/integration-tmp/my-test-app/index.ts +0 -8
  234. package/test/integration-tmp/my-test-app/opacacms.config.ts +0 -16
  235. package/test/integration-tmp/my-test-app/package.json +0 -12
  236. package/test/populate.test.ts +0 -79
  237. package/test/runtimes.test.ts +0 -43
  238. package/test/schema-builder.test.ts +0 -107
  239. package/test/schema-features.test.ts +0 -63
  240. package/test/seeding.test.ts +0 -68
  241. package/test/storage/local.test.ts +0 -72
  242. package/test/storage/s3.test.ts +0 -60
  243. package/test/structural-data.test.ts +0 -100
  244. package/test/test-setup.ts +0 -11
  245. package/test/validation.test.ts +0 -162
  246. package/tsconfig.json +0 -42
@@ -1,145 +0,0 @@
1
- import type { ApiKey } from "@better-auth/api-key";
2
- import type { Session, User } from "better-auth";
3
- import { icons } from "lucide-react";
4
- import type { AccessConfig, Global as GlobalType, IconName } from "../types";
5
- import { type FieldBuilder, VirtualFieldBuilder } from "./fields";
6
- import type { InferFields } from "./infer";
7
-
8
- export type Global = GlobalType;
9
-
10
- /**
11
- * A Fluent API Builder for defining OpacaCMS Globals.
12
- */
13
- export class GlobalBuilder<TFields extends Record<string, any> = {}> {
14
- protected config: Partial<Global> = {};
15
- protected _fields: FieldBuilder<any, any>[] = [];
16
-
17
- constructor(slug: string) {
18
- this.config.slug = slug;
19
- this.config.fields = [];
20
- this.config.timestamps = true;
21
- }
22
-
23
- /**
24
- * Enables or disables timestamps (createdAt, updatedAt) for this global.
25
- * Defaults to true.
26
- */
27
- public timestamps(enabled: boolean | { createdAt?: string; updatedAt?: string } = true): this {
28
- this.config.timestamps = enabled;
29
- return this;
30
- }
31
-
32
- /**
33
- * Sets the label for this global in the Admin UI.
34
- */
35
- public label(label: string): this {
36
- this.config.label = label;
37
- return this;
38
- }
39
-
40
- /**
41
- * Sets the Lucide icon for this global.
42
- */
43
- public icon(iconName: IconName): this {
44
- this.config.icon = iconName;
45
- return this;
46
- }
47
-
48
- /**
49
- * Defines the fields for this global schema.
50
- */
51
- public fields<T extends readonly any[]>(fields: [...T]): GlobalBuilder<InferFields<T>> {
52
- const nextBuilder = new GlobalBuilder<InferFields<T>>(this.config.slug!);
53
- nextBuilder.config = { ...this.config };
54
- nextBuilder._fields = [...fields];
55
- nextBuilder.config.fields = fields.map((f) => f.build());
56
- return nextBuilder as any; // Cast safely due to generic transformation
57
- }
58
-
59
- /**
60
- * Configuration for who can read and update this global.
61
- */
62
- public access(rules: AccessConfig): this {
63
- this.config.access = rules;
64
- return this;
65
- }
66
-
67
- /**
68
- * Defines a virtual/computed field with full type inference of the current global data.
69
- */
70
- public virtual<TName extends string, TReturn = any>(
71
- name: TName,
72
- options: {
73
- label?: string;
74
- resolve: (args: {
75
- data: TFields;
76
- req: any;
77
- user: any;
78
- session: any;
79
- apiKey?: any;
80
- }) => TReturn | Promise<TReturn>;
81
- returnType?: "string" | "number" | "boolean" | "json";
82
- },
83
- ): GlobalBuilder<TFields & { [K in TName]: TReturn }> {
84
- const builder = new VirtualFieldBuilder<TName, TFields>(name);
85
- if (options.label) builder.label(options.label);
86
- if (options.resolve) builder.resolve(options.resolve as any);
87
- if (options.returnType) builder.returnType(options.returnType);
88
-
89
- const nextBuilder = new GlobalBuilder<TFields & { [K in TName]: TReturn }>(this.config.slug!);
90
- nextBuilder.config = { ...this.config };
91
- nextBuilder._fields = [...this._fields, builder];
92
- // Update raw fields array
93
- nextBuilder.config.fields = nextBuilder._fields.map((f) => f.build());
94
- return nextBuilder;
95
- }
96
-
97
- /**
98
- * Alias for .virtual()
99
- */
100
- public computed<TName extends string, TReturn = any>(
101
- name: TName,
102
- options: {
103
- label?: string;
104
- resolve: (args: {
105
- data: TFields;
106
- req: any;
107
- user: any;
108
- session: any;
109
- apiKey?: any;
110
- }) => TReturn | Promise<TReturn>;
111
- returnType?: "string" | "number" | "boolean" | "json";
112
- },
113
- ): GlobalBuilder<TFields & { [K in TName]: TReturn }> {
114
- return this.virtual<TName, TReturn>(name, options);
115
- }
116
-
117
- /**
118
- * Extends the global with arbitrary configuration.
119
- */
120
- public extend(opts: Record<string, any>): this {
121
- this.config = { ...this.config, ...opts };
122
- return this;
123
- }
124
-
125
- /**
126
- * Compiles the builder down into the raw Global object expected by OpacaCMS.
127
- */
128
- public build(): Global {
129
- if (!this.config.slug) {
130
- throw new Error("Globals must have a slug.");
131
- }
132
- if (!this.config.fields || this.config.fields.length === 0) {
133
- throw new Error(`Global "${this.config.slug}" must have at least one field.`);
134
- }
135
-
136
- return this.config as Global;
137
- }
138
- }
139
-
140
- /**
141
- * Singleton factory for creating Globals.
142
- */
143
- export const Global = {
144
- create: (slug: string) => new GlobalBuilder(slug),
145
- };
@@ -1,4 +0,0 @@
1
- export { Collection, CollectionBuilder } from "./collection";
2
- export { Field, FieldBuilder } from "./fields/index";
3
- export { Global, GlobalBuilder } from "./global";
4
- export type { AccessRules, InferFields } from "./infer";
@@ -1,72 +0,0 @@
1
- import type { FieldBuilder } from "./fields/base";
2
-
3
- /**
4
- * Helper to convert a union of types into an intersection.
5
- */
6
- type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
7
- ? I
8
- : never;
9
-
10
- type Flatten<T> = { [K in keyof T]: T[K] } & {};
11
-
12
- /**
13
- * Extracts the TName and TValue from a single FieldBuilder.
14
- */
15
- export type InferField<T extends FieldBuilder<any, any, any, any>> =
16
- T extends FieldBuilder<any, any, infer TValue, any> ? TValue : any;
17
-
18
- /**
19
- * Internal helper to distribute over a union of FieldBuilders.
20
- * Matches against the class generics for maximum reliability.
21
- */
22
- type ProcessField<F> =
23
- F extends FieldBuilder<"group", infer N, any, infer Sub>
24
- ? N extends string
25
- ? { [K in N]: Sub extends readonly any[] ? InferFields<Sub> : unknown }
26
- : {}
27
- : F extends FieldBuilder<"array", infer N, any, infer Sub>
28
- ? N extends string
29
- ? { [K in N]: Sub extends readonly any[] ? InferFields<Sub>[] : unknown[] }
30
- : {}
31
- : F extends FieldBuilder<"row" | "collapsible", any, any, infer Sub>
32
- ? Sub extends readonly any[]
33
- ? InferFields<Sub>
34
- : {}
35
- : F extends FieldBuilder<"tabs", any, any, infer Sub>
36
- ? Sub extends readonly any[]
37
- ? UnionToIntersection<
38
- Sub[number] extends { fields: infer S }
39
- ? S extends readonly any[]
40
- ? InferFields<S>
41
- : {}
42
- : {}
43
- >
44
- : {}
45
- : F extends FieldBuilder<any, infer N, infer V, any>
46
- ? N extends string
47
- ? N extends ""
48
- ? {}
49
- : { [K in N]: V }
50
- : {}
51
- : {};
52
-
53
- /**
54
- * Magically maps an array of FieldBuilders into a recursive object type representation.
55
- *
56
- * Performance Optimized:
57
- * - Uses ProcessField helper for guaranteed union distribution.
58
- * - UnionToIntersection merges individual field objects into a clean intersection.
59
- */
60
- export type InferFields<T extends readonly any[]> = Flatten<
61
- UnionToIntersection<ProcessField<T[number]>>
62
- >;
63
-
64
- /**
65
- * Access rules with strongly typed hooks.
66
- */
67
- export interface AccessRules<TFields extends Record<string, any>> {
68
- read?: (args: { user: any; data?: TFields }) => boolean | Promise<boolean>;
69
- create?: (args: { user: any; data: TFields }) => boolean | Promise<boolean>;
70
- update?: (args: { user: any; data: TFields }) => boolean | Promise<boolean>;
71
- delete?: (args: { user: any; data?: TFields }) => boolean | Promise<boolean>;
72
- }
@@ -1,20 +0,0 @@
1
- import { Hono } from "hono";
2
- import type { Auth } from "../auth";
3
- import { getSystemCollections } from "../db/system-schema";
4
- import type { OpacaConfig } from "../types";
5
- import { createAdminHandlers } from "./admin";
6
- import { adminMiddleware } from "./middlewares/admin";
7
- import type { ApiContextVariables } from "./router";
8
-
9
- export function createAdminRouter(config: OpacaConfig, state: { auth: Auth | undefined }) {
10
- const adminRouter = new Hono<{ Variables: ApiContextVariables }>();
11
- const adminHandlers = createAdminHandlers(config, () => state.auth);
12
-
13
- adminRouter.get("/collections", adminMiddleware, adminHandlers.getCollections);
14
- adminRouter.get("/metadata", adminHandlers.getMetadata);
15
- adminRouter.get("/config", adminMiddleware, adminHandlers.getConfig);
16
- adminRouter.get("/setup", adminHandlers.getSetupStatus);
17
- adminRouter.post("/api-key/create", adminMiddleware, adminHandlers.createApiKey);
18
-
19
- return adminRouter;
20
- }
@@ -1,142 +0,0 @@
1
- import type { Context } from "hono";
2
- import type { Auth } from "../auth";
3
- import { sanitizeConfig } from "../config-utils";
4
- import type { OpacaConfig } from "../types";
5
-
6
- type AdminHandlers = {
7
- getMetadata: (c: Context) => Response;
8
- getCollections: (c: Context) => Response;
9
- getConfig: (c: Context) => Promise<Response>;
10
- getSetupStatus: (c: Context) => Promise<Response>;
11
- createApiKey: (c: Context) => Promise<Response>;
12
- };
13
-
14
- /**
15
- * Creates the admin handlers for the OpacaCMS.
16
- * @param config The OpacaCMS configuration.
17
- * @param getAuth A function that returns the authentication instance.
18
- * @returns An object containing the admin handlers for hono integration.
19
- */
20
- export function createAdminHandlers(
21
- config: OpacaConfig,
22
- getAuth: () => Auth | undefined,
23
- ): AdminHandlers {
24
- const getMetadata = (c: Context) => {
25
- return c.json(sanitizeConfig(config));
26
- };
27
-
28
- const getCollections = (c: Context) => {
29
- // Return collections with field details simplified if needed, or full config
30
- // For now returning config.collections directly.
31
- // Ideally we might strip internal server-only properties if any.
32
-
33
- const collections = [...config.collections];
34
-
35
- // Auto-inject system collections (auth + assets) if relevant features are enabled
36
- const supportsAuth =
37
- config.db.name === "sqlite" || config.db.name === "postgres" || config.db.name === "d1";
38
-
39
- const { getSystemCollections } = require("../db/system-schema");
40
- const systemCollections = getSystemCollections();
41
-
42
- for (const systemCol of systemCollections) {
43
- const isAsset = systemCol.slug === "_opaca_assets";
44
- const isAuth = ["_users", "_sessions", "_accounts", "_verifications", "_api_keys"].includes(
45
- systemCol.slug,
46
- );
47
-
48
- if ((isAsset && config.storages) || (isAuth && supportsAuth)) {
49
- if (!collections.find((col) => col.slug === systemCol.slug)) {
50
- collections.push({
51
- ...systemCol,
52
- admin: true, // Mark as system/admin collection
53
- });
54
- }
55
- }
56
- }
57
-
58
- // Filter collections to returned only what's allowed in the UI
59
- const filteredCollections = collections.filter((c) => !c.hidden);
60
-
61
- return c.json({
62
- collections: filteredCollections,
63
- globals: config.globals,
64
- });
65
- };
66
-
67
- const getConfig = async (c: Context) => {
68
- return c.json({
69
- serverURL: config.serverURL,
70
- admin: config.admin,
71
- });
72
- };
73
-
74
- const getSetupStatus = async (c: Context) => {
75
- try {
76
- let userCount = 0;
77
- try {
78
- userCount = await config.db.count("_users");
79
- } catch (_e) {
80
- // Fallback: the "_users" collection might not be in the OpacaCMS schema
81
- const result = (await config.db.unsafe("SELECT COUNT(*) as count FROM _users")) as any;
82
- const rows = result?.results || result || [];
83
- userCount = Number(rows[0]?.count || rows[0]?.["count(*)"] || 0);
84
- }
85
-
86
- return c.json({
87
- initialized: userCount > 0,
88
- });
89
- } catch (e) {
90
- console.error("[OpacaCMS] Failed to check setup status:", e);
91
-
92
- return c.json({
93
- initialized: false,
94
- });
95
- }
96
- };
97
-
98
- const createApiKey = async (c: Context) => {
99
- const auth = getAuth();
100
- if (!auth) {
101
- return c.json({ message: "Auth not initialized" }, 503);
102
- }
103
-
104
- const user = c.get("user");
105
- if (!user) {
106
- return c.json({ message: "Unauthorized" }, 401);
107
- }
108
-
109
- try {
110
- const { name, expiresIn, permissions } = await c.req.json();
111
-
112
- if (!name || typeof name !== "string") {
113
- return c.json({ message: "Invalid or missing 'name'" }, 400);
114
- }
115
-
116
- const res = await auth.api.createApiKey({
117
- body: {
118
- name,
119
- expiresIn: expiresIn ? Number(expiresIn) : undefined,
120
- permissions,
121
- userId: user.id,
122
- },
123
- });
124
-
125
- return c.json(res);
126
- // biome-ignore lint/suspicious/noExplicitAny: error shoulb be typed as any
127
- } catch (err: any) {
128
- console.error("[OpacaCMS] Failed to create API key:", err);
129
- // Return full error details if possible
130
- const message = err?.message || (typeof err === "string" ? err : JSON.stringify(err));
131
- return c.json({ message, detail: err }, 400);
132
- }
133
- };
134
-
135
- return {
136
- getMetadata,
137
- getCollections,
138
- getConfig,
139
- getSetupStatus,
140
- createApiKey,
141
- };
142
- }
@@ -1,306 +0,0 @@
1
- import type { Context } from "hono";
2
- import type { FileRecord } from "../storage/types";
3
- import type { OpacaConfig } from "../types";
4
-
5
- export function createAssetsHandlers(config: OpacaConfig) {
6
- return {
7
- async upload(c: Context) {
8
- const user = c.get("user");
9
- // Security: Only allow authenticated users to upload to the global registry
10
- if (!user) return c.json({ error: "Unauthorized" }, 401);
11
-
12
- const bucket = c.req.query("bucket") || "default";
13
- if (!config.storages) return c.json({ error: "Storage not configured" }, 500);
14
-
15
- const storageAdapter = config.storages[bucket];
16
- if (!storageAdapter) {
17
- return c.json({ error: `Bucket '${bucket}' not found` }, 404);
18
- }
19
-
20
- try {
21
- // Auto-patch missing columns for DX
22
- try {
23
- if (config.db.name === "sqlite" || config.db.name === "d1") {
24
- const tableInfo = (await config.db.unsafe(`PRAGMA table_info(_opaca_assets)`)) as any[];
25
- const columns = tableInfo.map((c) => c.name);
26
- if (!columns.includes("folder"))
27
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN folder TEXT`);
28
- if (!columns.includes("alt_text"))
29
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN alt_text TEXT`);
30
- if (!columns.includes("caption"))
31
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN caption TEXT`);
32
- } else if (config.db.name === "postgres") {
33
- const checkCols = (await config.db.unsafe(`
34
- SELECT column_name FROM information_schema.columns
35
- WHERE table_name = '_opaca_assets' AND column_name IN ('folder', 'alt_text', 'caption')
36
- `)) as any[];
37
- const existing = checkCols.map((c) => c.column_name);
38
- if (!existing.includes("folder"))
39
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN folder TEXT`);
40
- if (!existing.includes("alt_text"))
41
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN "alt_text" TEXT`);
42
- if (!existing.includes("caption"))
43
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN caption TEXT`);
44
- }
45
- } catch (e) {
46
- console.error("Auto-patch columns failed", e);
47
- }
48
-
49
- // Read the folder before upload so we can prefix the storage key
50
- const folder = c.req.query("folder") || null;
51
- const keyPrefix = folder ? `${folder}/` : "";
52
- const now = new Date().toISOString();
53
-
54
- const formData = await c.req.parseBody({ all: true });
55
- const fileRaw = formData["file"];
56
- const file = (Array.isArray(fileRaw) ? fileRaw[0] : fileRaw) as any;
57
-
58
- if (!file || (typeof file !== "object" && typeof file !== "string")) {
59
- return c.json({ error: "No file provided" }, 400);
60
- }
61
-
62
- const fileName = file.name || "unnamed";
63
- const fileType = file.type || "application/octet-stream";
64
- const fileSize = file.size || 0;
65
-
66
- const fileRecord: FileRecord = {
67
- filename: fileName,
68
- original_filename: fileName,
69
- mime_type: fileType,
70
- filesize: fileSize,
71
- stream: typeof file.stream === "function" ? file.stream() : new Response(file).body!,
72
- };
73
-
74
- // Pipe stream directly to adapter, passing the folder as a key prefix
75
- const uploadedFileData = await storageAdapter.upload(fileRecord, {
76
- generateUniqueName: true,
77
- keyPrefix,
78
- });
79
-
80
- // Ensure the stored key in the DB reflects the folder prefix
81
- const storedKey = keyPrefix + uploadedFileData.filename;
82
-
83
- try {
84
- // Insert into hidden registry table
85
- const assetId = (
86
- globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2)
87
- ).replace(/-/g, "");
88
-
89
- await config.db.create("_opaca_assets", {
90
- id: assetId,
91
- key: storedKey,
92
- filename: fileName,
93
- originalFilename: fileName,
94
- mimeType: uploadedFileData.mime_type,
95
- filesize: uploadedFileData.filesize,
96
- bucket,
97
- folder,
98
- altText: null,
99
- caption: null,
100
- uploadedBy: user.id || null,
101
- });
102
-
103
- // Return the standardized JSON payload for embedded file fields
104
- return c.json(
105
- {
106
- assetId,
107
- ...uploadedFileData,
108
- key: storedKey,
109
- },
110
- 201,
111
- );
112
- } catch (dbError: any) {
113
- // Rollback mechanism
114
- console.error(
115
- `[OpacaCMS] Registry insert failed, rolling back physical file upload: ${storedKey}`,
116
- );
117
- storageAdapter.delete(storedKey).catch((cleanupError: any) => {
118
- console.error(
119
- `[OpacaCMS] CRITICAL: Failed to clean up orphaned file ${storedKey}!`,
120
- cleanupError,
121
- );
122
- });
123
- throw dbError;
124
- }
125
- } catch (error: any) {
126
- return c.json({ error: error.message }, 400);
127
- }
128
- },
129
-
130
- async list(c: Context) {
131
- const user = c.get("user");
132
- if (!user || (user.role !== "admin" && !user.role?.includes("admin"))) {
133
- return c.json({ error: "Unauthorized" }, 401);
134
- }
135
-
136
- const bucket = c.req.query("bucket") || "all";
137
- const page = parseInt(c.req.query("page") || "1", 10);
138
- const limit = parseInt(c.req.query("limit") || "20", 10);
139
- const offset = (page - 1) * limit;
140
- const folder = c.req.query("folder") || null;
141
-
142
- try {
143
- let query: any = {};
144
- if (bucket !== "all") query.bucket = bucket;
145
-
146
- if (folder !== null && folder !== "") {
147
- query.folder = folder;
148
- } else {
149
- // Handle root folder (NULL or empty string)
150
- // If we have a bucket filter, we should combine it
151
- if (bucket !== "all") {
152
- query = {
153
- and: [{ bucket: bucket }, { or: [{ folder: null }, { folder: "" }] }],
154
- };
155
- } else {
156
- query = { or: [{ folder: null }, { folder: "" }] };
157
- }
158
- }
159
-
160
- const result = await config.db.find<any>("_opaca_assets", query, {
161
- page,
162
- limit,
163
- sort: "created_at:desc",
164
- });
165
-
166
- const rows = result.docs;
167
- const total = result.totalDocs;
168
-
169
- // Discover subfolders (isolated by bucket)
170
- let folderRows: any[] = [];
171
- const bucketFilter = bucket !== "all" ? `AND bucket = ?` : "";
172
- const bucketParam = bucket !== "all" ? [bucket] : [];
173
-
174
- if (config.db.name === "postgres") {
175
- const pgBucketFilter = bucketFilter.replace("?", "$1");
176
- if (folder === null || folder === "") {
177
- folderRows = (await config.db.unsafe(
178
- `SELECT DISTINCT split_part(folder, '/', 1) as subfolder, bucket FROM _opaca_assets WHERE folder IS NOT NULL AND folder != '' ${pgBucketFilter}`,
179
- bucketParam,
180
- )) as any[];
181
- } else {
182
- folderRows = (await config.db.unsafe(
183
- `SELECT DISTINCT split_part(substring(folder from length($1) + 2), '/', 1) as subfolder, bucket FROM _opaca_assets WHERE folder LIKE $2 ${bucket !== "all" ? "AND bucket = $3" : ""}`,
184
- [folder, `${folder}/%`, ...bucketParam],
185
- )) as any[];
186
- }
187
- } else {
188
- if (folder === null || folder === "") {
189
- folderRows = (await config.db.unsafe(
190
- `
191
- SELECT DISTINCT
192
- CASE
193
- WHEN INSTR(folder, '/') > 0 THEN SUBSTR(folder, 1, INSTR(folder, '/') - 1)
194
- ELSE folder
195
- END as subfolder,
196
- bucket
197
- FROM _opaca_assets WHERE folder IS NOT NULL AND folder != '' ${bucketFilter}
198
- `,
199
- bucketParam,
200
- )) as any[];
201
- } else {
202
- const skipLen = folder.length + 2;
203
- folderRows = (await config.db.unsafe(
204
- `
205
- SELECT DISTINCT
206
- CASE
207
- WHEN INSTR(SUBSTR(folder, ?), '/') > 0 THEN SUBSTR(SUBSTR(folder, ?), 1, INSTR(SUBSTR(folder, ?), '/') - 1)
208
- ELSE SUBSTR(folder, ?)
209
- END as subfolder,
210
- bucket
211
- FROM _opaca_assets WHERE folder LIKE ? ${bucketFilter}
212
- `,
213
- [skipLen, skipLen, skipLen, skipLen, `${folder}/%`, ...bucketParam],
214
- )) as any[];
215
- }
216
- }
217
-
218
- // Group shared folders by name
219
- const folderMap: Record<string, string[]> = {};
220
- for (const row of folderRows) {
221
- if (!row.subfolder) continue;
222
- if (!folderMap[row.subfolder]) folderMap[row.subfolder] = [];
223
- if (!folderMap[row.subfolder]?.includes(row.bucket)) {
224
- folderMap[row.subfolder]?.push(row.bucket);
225
- }
226
- }
227
-
228
- const folders = Object.entries(folderMap).map(([name, buckets]) => ({
229
- name,
230
- buckets,
231
- }));
232
-
233
- return c.json({
234
- docs: rows,
235
- folders,
236
- totalDocs: total,
237
- limit,
238
- page,
239
- totalPages: Math.ceil(total / limit),
240
- });
241
- } catch (e: any) {
242
- return c.json({ error: e.message }, 500);
243
- }
244
- },
245
-
246
- async presign(c: Context) {
247
- const user = c.get("user");
248
- if (!user) return c.json({ error: "Unauthorized" }, 401);
249
-
250
- const { filename, bucket = "default", operation = "write" } = await c.req.json();
251
- if (!config.storages || !config.storages[bucket]) {
252
- return c.json({ error: "Bucket not found" }, 404);
253
- }
254
-
255
- const adapter = config.storages[bucket];
256
- if (!adapter.generatePresignedUrl) {
257
- return c.json({ error: "Adapter does not support presigned URLs" }, 400);
258
- }
259
-
260
- try {
261
- const url = await adapter.generatePresignedUrl(
262
- filename,
263
- operation as "read" | "write",
264
- 3600,
265
- );
266
- return c.json({ uploadUrl: url, filename });
267
- } catch (e: any) {
268
- return c.json({ error: e.message }, 500);
269
- }
270
- },
271
-
272
- async serve(c: Context) {
273
- const id = c.req.param("id");
274
-
275
- try {
276
- const asset = await config.db.findOne<any>("_opaca_assets", { id });
277
-
278
- if (!asset) {
279
- return c.json({ error: "Asset not found" }, 404);
280
- }
281
-
282
- const bucket = asset.bucket || "default";
283
- if (!config.storages || !config.storages[bucket]) {
284
- return c.json({ error: "Storage bucket not configured" }, 500);
285
- }
286
-
287
- const adapter = config.storages[bucket];
288
- if (!adapter.download) {
289
- return c.json({ error: "Storage adapter does not support direct downloads" }, 400);
290
- }
291
-
292
- const stream = await adapter.download(asset.key || asset.filename);
293
-
294
- c.header("Content-Type", asset.mimeType || "application/octet-stream");
295
- c.header("Content-Length", asset.filesize.toString());
296
- // Cache for 1 day
297
- c.header("Cache-Control", "public, max-age=86400");
298
-
299
- return c.body(stream as any);
300
- } catch (e: any) {
301
- console.error(`[OpacaCMS] Failed to serve asset ${id}:`, e);
302
- return c.json({ error: e.message }, 500);
303
- }
304
- },
305
- };
306
- }