opacacms 0.2.1 → 0.3.1

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 (185) hide show
  1. package/README.md +31 -22
  2. package/dist/admin/auth-client.d.ts +39 -39
  3. package/dist/admin/index.d.ts +2 -2
  4. package/dist/admin/index.js +15 -10520
  5. package/dist/admin/plugin-client.d.ts +65 -0
  6. package/dist/admin/react.d.ts +2 -2
  7. package/dist/admin/react.js +34 -4
  8. package/dist/admin/stores/ui.d.ts +19 -4
  9. package/dist/admin/ui/components/PluginSettingsForm.d.ts +2 -2
  10. package/dist/admin/ui/components/custom-alert.d.ts +7 -0
  11. package/dist/admin/ui/components/{DetailSheet.d.ts → detail-sheet.d.ts} +1 -2
  12. package/dist/admin/ui/components/fields/FieldLabel.d.ts +1 -1
  13. package/dist/admin/ui/components/fields/RelationshipField.d.ts +1 -1
  14. package/dist/admin/ui/components/media/AssetManagerModal.d.ts +2 -2
  15. package/dist/admin/ui/components/plugin-iframe.d.ts +7 -0
  16. package/dist/admin/ui/components/ui/accordion.d.ts +17 -7
  17. package/dist/admin/ui/components/ui/alert-dialog.d.ts +16 -12
  18. package/dist/admin/ui/components/ui/button.d.ts +11 -7
  19. package/dist/admin/ui/components/ui/relationship.d.ts +1 -1
  20. package/dist/admin/ui/components/ui/sheet.d.ts +14 -27
  21. package/dist/admin/ui/components/ui/tooltip.d.ts +7 -0
  22. package/dist/admin/ui/components/versions-sheet.d.ts +4 -5
  23. package/dist/admin/ui/views/collection-list-view.d.ts +1 -1
  24. package/dist/admin/ui/views/dashboard-view.d.ts +1 -1
  25. package/dist/admin/ui/views/media-registry-view.d.ts +3 -3
  26. package/dist/admin/ui/views/settings-view.d.ts +2 -2
  27. package/dist/admin/vue.js +27 -4
  28. package/dist/admin/webcomponent.js +66 -16
  29. package/dist/admin.css +1 -1
  30. package/dist/auth/index.d.ts +43 -43
  31. package/dist/{chunk-7y1nbmw6.js → chunk-1bd7fz7n.js} +32 -2
  32. package/dist/chunk-1qm0m8r8.js +413 -0
  33. package/dist/chunk-2k3ysje3.js +31 -0
  34. package/dist/{chunk-jdfw4v3r.js → chunk-3j9zjfmn.js} +95 -30
  35. package/dist/{chunk-byq8g0rd.js → chunk-48ywpd0a.js} +16 -22
  36. package/dist/{chunk-tfnaf41w.js → chunk-5422w4eq.js} +41 -25
  37. package/dist/chunk-56n342hs.js +95 -0
  38. package/dist/chunk-5b8r0v8c.js +47 -0
  39. package/dist/chunk-63yg00vx.js +263 -0
  40. package/dist/{chunk-8sqjbsgt.js → chunk-6bywt602.js} +26 -1
  41. package/dist/{chunk-v9z61v3g.js → chunk-6qs0g65f.js} +43 -3
  42. package/dist/chunk-7rr5p01g.js +581 -0
  43. package/dist/{chunk-2es275xs.js → chunk-941zxavt.js} +867 -322
  44. package/dist/{chunk-51z3x7kq.js → chunk-a3qae86h.js} +1 -1
  45. package/dist/{chunk-3rdhbedb.js → chunk-adq2b75c.js} +2 -2
  46. package/dist/chunk-d0tb1xjw.js +93 -0
  47. package/dist/chunk-d7cgd6vn.js +318 -0
  48. package/dist/{chunk-6d1vdfwa.js → chunk-e0g6gn7n.js} +54 -75
  49. package/dist/chunk-ec4jhybj.js +1137 -0
  50. package/dist/chunk-fatyf6f7.js +221 -0
  51. package/dist/{chunk-526a3gqx.js → chunk-fnsf1dfm.js} +1 -1
  52. package/dist/chunk-g9bxb6h0.js +205 -0
  53. package/dist/chunk-gyaf5kgf.js +10 -0
  54. package/dist/{chunk-9kxpbcb1.js → chunk-h6dhexzr.js} +16 -7
  55. package/dist/{chunk-dykn5hr6.js → chunk-j8js1y0h.js} +31 -74
  56. package/dist/{chunk-t0zg026p.js → chunk-jq1drsen.js} +12 -1
  57. package/dist/{chunk-b3kr8w41.js → chunk-m24yqkeq.js} +38 -26
  58. package/dist/chunk-m5ems3hh.js +410 -0
  59. package/dist/{chunk-8scgdznr.js → chunk-m83ybzf8.js} +15 -18
  60. package/dist/chunk-majsbncm.js +98 -0
  61. package/dist/chunk-mp2gt9yh.js +237 -0
  62. package/dist/chunk-n1twhqmf.js +54 -0
  63. package/dist/{chunk-bygjkgrx.js → chunk-naqcqj8n.js} +57 -80
  64. package/dist/chunk-q5sb5dcr.js +15 -0
  65. package/dist/{chunk-06ks4ggh.js → chunk-qhdsjek6.js} +49 -89
  66. package/dist/{chunk-n133qpsm.js → chunk-qsh2nqz3.js} +50 -81
  67. package/dist/chunk-r0ms5tk1.js +76 -0
  68. package/dist/chunk-rwqwsanx.js +75 -0
  69. package/dist/chunk-sqsfk9p4.js +700 -0
  70. package/dist/{chunk-5gvbp2qa.js → chunk-x7bnzswh.js} +25 -18
  71. package/dist/cli/commands/dev.d.ts +8 -0
  72. package/dist/cli/commands/doctor.d.ts +8 -0
  73. package/dist/cli/commands/generate.d.ts +26 -0
  74. package/dist/cli/commands/init.d.ts +13 -1
  75. package/dist/cli/commands/migrate.d.ts +33 -0
  76. package/dist/cli/commands/plugin.d.ts +13 -0
  77. package/dist/cli/commands/seed.d.ts +21 -0
  78. package/dist/cli/{commands/migrate-commands.d.ts → core/migrations/migrate-logic.d.ts} +2 -2
  79. package/dist/cli/core/migrations/schema-diff-engine.d.ts +12 -0
  80. package/dist/cli/core/migrations/schema-diff.d.ts +11 -0
  81. package/dist/cli/{seeding.d.ts → core/seeding/auto-seed.d.ts} +7 -4
  82. package/dist/cli/core/seeding/seed-logic.d.ts +2 -0
  83. package/dist/cli/index.d.ts +4 -0
  84. package/dist/cli/index.js +6 -170
  85. package/dist/client/RichText.d.ts +5 -0
  86. package/dist/client/rich-text-utils.d.ts +5 -0
  87. package/dist/client.js +3 -2
  88. package/dist/config.d.ts +3 -3
  89. package/dist/db/better-sqlite.d.ts +2 -3
  90. package/dist/db/better-sqlite.js +6 -5
  91. package/dist/db/bun-sqlite.d.ts +2 -3
  92. package/dist/db/bun-sqlite.js +6 -5
  93. package/dist/db/d1.d.ts +13 -7
  94. package/dist/db/d1.js +6 -5
  95. package/dist/db/index.d.ts +2 -2
  96. package/dist/db/index.js +10 -12
  97. package/dist/db/kysely/factory.d.ts +29 -0
  98. package/dist/db/kysely/plugins/audit-logging.d.ts +48 -0
  99. package/dist/db/kysely/plugins/auto-timestamps.d.ts +38 -0
  100. package/dist/db/kysely/plugins/cursor-pagination.d.ts +42 -0
  101. package/dist/db/kysely/plugins/deadlock-handler.d.ts +47 -0
  102. package/dist/db/kysely/plugins/draft-swapper.d.ts +33 -0
  103. package/dist/db/kysely/plugins/field-masking.d.ts +45 -0
  104. package/dist/db/kysely/plugins/fts-normalizer.d.ts +38 -0
  105. package/dist/db/kysely/plugins/i18n-fallback.d.ts +48 -0
  106. package/dist/db/kysely/plugins/id-generation.d.ts +42 -0
  107. package/dist/db/kysely/plugins/index.d.ts +16 -0
  108. package/dist/db/kysely/plugins/json-flattener.d.ts +38 -0
  109. package/dist/db/kysely/plugins/relationship-preloading.d.ts +39 -0
  110. package/dist/db/kysely/plugins/slug-generation.d.ts +37 -0
  111. package/dist/db/kysely/plugins/soft-delete.d.ts +42 -0
  112. package/dist/db/kysely/plugins/tree-resolver.d.ts +39 -0
  113. package/dist/db/kysely/plugins/virtual-field-resolver.d.ts +54 -0
  114. package/dist/db/kysely/plugins/zod-coercion.d.ts +34 -0
  115. package/dist/db/kysely/snapshot/snapshot-manager.d.ts +18 -0
  116. package/dist/db/postgres.d.ts +2 -2
  117. package/dist/db/postgres.js +6 -5
  118. package/dist/db/sqlite.d.ts +2 -3
  119. package/dist/db/sqlite.js +6 -5
  120. package/dist/index.d.ts +3 -0
  121. package/dist/index.js +161 -7
  122. package/dist/runtimes/bun.js +9 -6
  123. package/dist/runtimes/cloudflare-workers.d.ts +3 -1
  124. package/dist/runtimes/cloudflare-workers.js +36 -7
  125. package/dist/runtimes/next.js +8 -5
  126. package/dist/runtimes/node.js +9 -6
  127. package/dist/schema/collection.d.ts +116 -70
  128. package/dist/schema/compiler.d.ts +6 -0
  129. package/dist/schema/global.d.ts +38 -71
  130. package/dist/schema/index.d.ts +5 -4
  131. package/dist/schema/index.js +35 -550
  132. package/dist/schema/zod.d.ts +564 -0
  133. package/dist/server/admin-router.d.ts +1 -1
  134. package/dist/server/collection-router.d.ts +1 -1
  135. package/dist/server/graphql.d.ts +6 -0
  136. package/dist/server/handlers.d.ts +25 -7
  137. package/dist/server/middlewares/auth.d.ts +1 -1
  138. package/dist/server/plugins-loader.d.ts +1 -1
  139. package/dist/server/router.d.ts +2 -2
  140. package/dist/server/routers/admin.d.ts +1 -1
  141. package/dist/server/routers/auth.d.ts +1 -1
  142. package/dist/server/routers/collections.d.ts +4 -1
  143. package/dist/server/routers/plugins.d.ts +2 -2
  144. package/dist/server/setup-middlewares.d.ts +1 -1
  145. package/dist/server/system-router.d.ts +1 -1
  146. package/dist/server.js +11 -6
  147. package/dist/storage/adapters/cloudflare-r2.d.ts +11 -2
  148. package/dist/storage/index.js +5 -5
  149. package/dist/types.d.ts +253 -42
  150. package/dist/utils/context.d.ts +14 -0
  151. package/dist/utils/logger.d.ts +2 -0
  152. package/dist/utils/string.d.ts +10 -0
  153. package/dist/utils/webhooks-engine.d.ts +24 -0
  154. package/dist/validation.d.ts +67 -1
  155. package/dist/validator.d.ts +1 -0
  156. package/package.json +50 -11
  157. package/src/cli/index.ts +117 -0
  158. package/dist/chunk-6qq3ne6b.js +0 -288
  159. package/dist/chunk-6v1fw7q7.js +0 -126
  160. package/dist/chunk-7a9kn0np.js +0 -116
  161. package/dist/chunk-bexcv7xe.js +0 -36
  162. package/dist/chunk-d3ffeqp9.js +0 -87
  163. package/dist/chunk-fj19qccp.js +0 -78
  164. package/dist/chunk-g1jb60xd.js +0 -17
  165. package/dist/chunk-j53pz21t.js +0 -20
  166. package/dist/chunk-mkn49zmy.js +0 -102
  167. package/dist/chunk-r39em4yj.js +0 -29
  168. package/dist/chunk-rsf0tpy1.js +0 -8
  169. package/dist/chunk-srsac177.js +0 -85
  170. package/dist/chunk-twpvxfce.js +0 -64
  171. package/dist/chunk-ywm4t2gm.js +0 -19
  172. package/dist/cli/commands/plugin-sync.d.ts +0 -1
  173. package/dist/cli/commands/seed-command.d.ts +0 -2
  174. package/dist/plugins/ui-bridge.d.ts +0 -12
  175. package/dist/schema/fields/base.d.ts +0 -84
  176. package/dist/schema/fields/index.d.ts +0 -147
  177. package/dist/schema/infer.d.ts +0 -55
  178. /package/dist/admin/ui/components/{ColumnVisibilityToggle.d.ts → column-visibility-toggle.d.ts} +0 -0
  179. /package/dist/admin/ui/components/{DataDetailView.d.ts → data-detail-view.d.ts} +0 -0
  180. /package/dist/cli/{d1-mock.d.ts → core/mocks/d1-mock.d.ts} +0 -0
  181. /package/dist/cli/{r2-mock.d.ts → core/mocks/r2-mock.d.ts} +0 -0
  182. /package/dist/cli/{commands → core/plugins}/plugin-build.d.ts +0 -0
  183. /package/dist/cli/{commands → core/plugins}/plugin-init.d.ts +0 -0
  184. /package/dist/cli/{commands → core/types}/generate-types.d.ts +0 -0
  185. /package/dist/{schema/fields/validation.test.d.ts → cli/seeding.test.d.ts} +0 -0
@@ -1,23 +1,29 @@
1
1
  import {
2
2
  createAuth,
3
3
  sanitizeConfig
4
- } from "./chunk-b3kr8w41.js";
4
+ } from "./chunk-m24yqkeq.js";
5
+ import {
6
+ sanitizeGraphQLName
7
+ } from "./chunk-5b8r0v8c.js";
8
+ import {
9
+ requestContext
10
+ } from "./chunk-q5sb5dcr.js";
5
11
  import {
6
12
  toSnakeCase
7
13
  } from "./chunk-qxt9vge8.js";
14
+ import {
15
+ OpacaLogger,
16
+ logger
17
+ } from "./chunk-jq1drsen.js";
8
18
  import {
9
19
  exports_system_schema,
10
20
  getSystemCollections,
11
21
  init_system_schema
12
- } from "./chunk-v9z61v3g.js";
13
- import {
14
- OpacaLogger,
15
- logger
16
- } from "./chunk-t0zg026p.js";
22
+ } from "./chunk-6qs0g65f.js";
17
23
  import {
18
24
  __require,
19
25
  __toCommonJS
20
- } from "./chunk-8sqjbsgt.js";
26
+ } from "./chunk-6bywt602.js";
21
27
 
22
28
  // src/server/admin.ts
23
29
  function createAdminHandlers(config, settings, getAuth) {
@@ -30,7 +36,7 @@ function createAdminHandlers(config, settings, getAuth) {
30
36
  const { getSystemCollections: getSystemCollections2 } = (init_system_schema(), __toCommonJS(exports_system_schema));
31
37
  const systemCollections = getSystemCollections2();
32
38
  for (const systemCol of systemCollections) {
33
- const isAsset = systemCol.slug === "_opaca_assets";
39
+ const isAsset = systemCol.slug === "_assets";
34
40
  const isAuth = ["_users", "_sessions", "_accounts", "_verifications", "_api_keys"].includes(systemCol.slug);
35
41
  if (isAsset && config.storages || isAuth && supportsAuth) {
36
42
  if (!collections.find((col) => col.slug === systemCol.slug)) {
@@ -64,7 +70,8 @@ function createAdminHandlers(config, settings, getAuth) {
64
70
  userCount = Number(rows[0]?.count || rows[0]?.["count(*)"] || 0);
65
71
  }
66
72
  return c.json({
67
- initialized: userCount > 0
73
+ initialized: userCount > 0,
74
+ api: sanitizeConfig(config, settings).api
68
75
  });
69
76
  } catch (e) {
70
77
  console.error("[OpacaCMS] Failed to check setup status:", e);
@@ -119,10 +126,111 @@ function createAdminHandlers(config, settings, getAuth) {
119
126
  };
120
127
  }
121
128
 
129
+ // src/server/handlers.ts
130
+ init_system_schema();
131
+
132
+ // src/utils/webhooks-engine.ts
133
+ async function verifyWebhookSignature(req, secretOrFn, headerName = "x-opaca-signature") {
134
+ if (typeof secretOrFn === "function") {
135
+ return secretOrFn(req);
136
+ }
137
+ const signature = req.headers.get(headerName);
138
+ if (!signature || !secretOrFn)
139
+ return false;
140
+ try {
141
+ const body = await req.clone().text();
142
+ const encoder = new TextEncoder;
143
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secretOrFn).buffer, { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
144
+ const sigArray = hexToUint8Array(signature);
145
+ return await crypto.subtle.verify("HMAC", key, sigArray.buffer, encoder.encode(body).buffer);
146
+ } catch (err) {
147
+ logger.error("[Webhooks] Signature verification failed:", err);
148
+ return false;
149
+ }
150
+ }
151
+ async function dispatchWebhook(args) {
152
+ const { url, data, event, collection, headers, retries = 3, timeoutMs = 30000, transform } = args;
153
+ let payload = data;
154
+ if (transform) {
155
+ try {
156
+ payload = await transform(data);
157
+ } catch (err) {
158
+ logger.error(`[Webhooks] Transformation failed for ${collection} [${event}]`, err);
159
+ }
160
+ }
161
+ const fullPayload = {
162
+ event,
163
+ collection,
164
+ data: payload,
165
+ timestamp: new Date().toISOString()
166
+ };
167
+ let lastError = null;
168
+ const controller = new AbortController;
169
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
170
+ const attempt = async (retryCount) => {
171
+ try {
172
+ const response = await fetch(url, {
173
+ method: "POST",
174
+ headers: {
175
+ "Content-Type": "application/json",
176
+ "X-Opaca-Event": event,
177
+ "X-Opaca-Collection": collection,
178
+ ...headers
179
+ },
180
+ body: JSON.stringify(fullPayload),
181
+ signal: controller.signal
182
+ });
183
+ if (!response.ok) {
184
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
185
+ }
186
+ logger.info(`[Webhooks] Sent to ${url} [${collection}:${event}]`);
187
+ return;
188
+ } catch (err) {
189
+ lastError = err;
190
+ logger.warn(`[Webhooks] Attempt ${retryCount + 1}/${retries} failed for ${url} [${collection}:${event}]: ${err.message}`);
191
+ if (retryCount < retries) {
192
+ const delay = 2 ** retryCount * 1000;
193
+ logger.warn(`[Webhooks] Delivery failed to ${url}. Retrying in ${delay}ms... (Attempt ${retryCount + 1}/${retries})`);
194
+ await new Promise((resolve) => setTimeout(resolve, delay));
195
+ return attempt(retryCount + 1);
196
+ }
197
+ }
198
+ };
199
+ try {
200
+ await attempt(0);
201
+ } catch {} finally {
202
+ clearTimeout(timeout);
203
+ if (lastError) {
204
+ const msg = lastError.message || String(lastError);
205
+ logger.error(`[Webhooks] Final delivery failure to ${url} after ${retries} attempts. Last error: ${msg}`);
206
+ }
207
+ }
208
+ }
209
+ function hexToUint8Array(hex) {
210
+ const matches = hex.match(/.{1,2}/g);
211
+ if (!matches)
212
+ return new Uint8Array;
213
+ return new Uint8Array(matches.map((byte) => parseInt(byte, 16)));
214
+ }
215
+
122
216
  // src/validator.ts
123
217
  import { z } from "zod";
124
218
  function generateSchemaForCollection(collection, isUpdate = false, forDocs = false) {
125
- const shape = mapFieldsToShape(collection.fields, isUpdate, forDocs);
219
+ let shape;
220
+ if (collection.schema && collection.schema._def.type === "object") {
221
+ const originalShape = collection.schema.shape;
222
+ shape = { ...originalShape };
223
+ if (isUpdate) {
224
+ for (const key in shape) {
225
+ const field = shape[key];
226
+ if (field) {
227
+ shape[key] = field.optional().nullable();
228
+ }
229
+ }
230
+ }
231
+ } else {
232
+ shape = mapFieldsToShape(collection.fields, isUpdate, forDocs);
233
+ }
126
234
  if (collection.versions) {
127
235
  shape._status = z.enum(["draft", "published"]).optional().nullable();
128
236
  }
@@ -135,7 +243,38 @@ function generateSchemaForCollection(collection, isUpdate = false, forDocs = fal
135
243
  shape[createdField] = dateSchema.optional().nullable();
136
244
  shape[updatedField] = dateSchema.optional().nullable();
137
245
  }
138
- return z.object(shape);
246
+ let baseSchema = z.object(shape);
247
+ if (!isUpdate) {
248
+ baseSchema = baseSchema.superRefine((data, ctx) => {
249
+ const checkFields = (fields, currentData, path = []) => {
250
+ if (!fields)
251
+ return;
252
+ fields.forEach((field) => {
253
+ if (field.requiredCondition && typeof field.requiredCondition === "function") {
254
+ const isRequired = field.requiredCondition(data);
255
+ const value = currentData?.[field.name];
256
+ if (isRequired && (value === undefined || value === null || value === "")) {
257
+ ctx.addIssue({
258
+ code: z.ZodIssueCode.custom,
259
+ message: `${field.label || field.name} is required`,
260
+ path: [...path, field.name]
261
+ });
262
+ }
263
+ }
264
+ if (field.fields && Array.isArray(field.fields)) {
265
+ checkFields(field.fields, currentData?.[field.name], [...path, field.name]);
266
+ }
267
+ if (field.tabs && Array.isArray(field.tabs)) {
268
+ field.tabs.forEach((tab) => {
269
+ checkFields(tab.fields, currentData, path);
270
+ });
271
+ }
272
+ });
273
+ };
274
+ checkFields(collection.fields, data);
275
+ });
276
+ }
277
+ return baseSchema;
139
278
  }
140
279
  function mapFieldsToShape(fields, isUpdate = false, forDocs = false) {
141
280
  const shape = {};
@@ -201,17 +340,8 @@ function mapFieldsToShape(fields, isUpdate = false, forDocs = false) {
201
340
  });
202
341
  }
203
342
  });
204
- } else if (typeof field.validate === "object" && field.validate !== null && "safeParse" in field.validate) {
205
- schema = schema.superRefine((val, ctx) => {
206
- if (val === undefined || val === null)
207
- return;
208
- const result = field.validate.safeParse(val);
209
- if (!result.success) {
210
- for (const issue of result.error.issues) {
211
- ctx.addIssue(issue);
212
- }
213
- }
214
- });
343
+ } else if (field.validate instanceof z.ZodType) {
344
+ schema = z.intersection(schema, field.validate);
215
345
  }
216
346
  }
217
347
  shape[fieldName] = schema;
@@ -222,24 +352,43 @@ function mapFieldToZod(field, forDocs = false) {
222
352
  switch (field.type) {
223
353
  case "text":
224
354
  case "slug":
225
- case "richtext":
226
355
  case "textarea":
227
356
  return z.string();
228
- case "relationship": {
229
- const isHasMany = "hasMany" in field && field.hasMany;
230
- const base = forDocs ? z.union([z.string(), z.number(), z.null()]) : z.union([z.string(), z.number(), z.undefined(), z.null()]);
231
- const schema = isHasMany ? z.array(base.optional().nullable()) : base;
232
- if (isHasMany) {
233
- return z.preprocess((val) => {
234
- if (val === undefined || val === null || val === "")
235
- return [];
236
- if (Array.isArray(val))
237
- return val;
238
- return [val];
239
- }, schema);
240
- }
241
- return schema;
357
+ case "richtext":
358
+ return z.any();
359
+ case "relationship":
360
+ return field.hasMany ? z.array(z.string()) : z.string();
361
+ case "select":
362
+ case "radio": {
363
+ const choices = field.options?.choices || [];
364
+ const values = choices.map((c) => typeof c === "string" ? c : c.value);
365
+ if (values.length > 0) {
366
+ return z.enum(values);
367
+ }
368
+ return z.string();
242
369
  }
370
+ case "date":
371
+ return forDocs ? z.string() : z.union([z.string(), z.date()]);
372
+ case "boolean":
373
+ return z.preprocess((val) => {
374
+ if (val === "true" || val === 1)
375
+ return true;
376
+ if (val === "false" || val === 0)
377
+ return false;
378
+ return val;
379
+ }, z.boolean());
380
+ case "number":
381
+ return z.preprocess((val) => {
382
+ if (val === "" || val === undefined || val === null)
383
+ return;
384
+ if (typeof val === "string") {
385
+ const num = Number(val);
386
+ return Number.isNaN(num) ? undefined : num;
387
+ }
388
+ return val;
389
+ }, z.number());
390
+ case "json":
391
+ return z.any();
243
392
  case "file":
244
393
  return z.object({
245
394
  id: z.string(),
@@ -252,64 +401,14 @@ function mapFieldToZod(field, forDocs = false) {
252
401
  focal_x: z.number().optional().nullable(),
253
402
  focal_y: z.number().optional().nullable()
254
403
  });
255
- case "number":
256
- return z.preprocess((val) => {
257
- if (val === "" || val === undefined || val === null)
258
- return;
259
- if (typeof val === "string") {
260
- const num = Number(val);
261
- return Number.isNaN(num) ? undefined : num;
262
- }
263
- return val;
264
- }, z.number().optional().nullable());
265
- case "boolean":
266
- return z.preprocess((val) => {
267
- if (typeof val === "string")
268
- return val === "true";
269
- return val;
270
- }, z.boolean());
271
- case "date": {
272
- if (forDocs)
273
- return z.string().describe("ISO 8601 Date");
274
- return z.preprocess((val) => {
275
- if (val === "" || val === undefined || val === null)
276
- return;
277
- return val;
278
- }, z.union([
279
- z.string().regex(/^\d{4}-\d{2}-\d{2}/, "Invalid date format"),
280
- z.date(),
281
- z.undefined(),
282
- z.null()
283
- ]));
284
- }
285
- case "select":
286
- case "radio": {
287
- const choices = field.options?.choices;
288
- if (choices && Array.isArray(choices)) {
289
- const values = choices.map((c) => typeof c === "string" ? c : c.value);
290
- if (values.length > 0) {
291
- return z.enum(values);
292
- }
293
- }
294
- return z.string();
295
- }
296
- case "json":
297
- return z.any();
298
404
  case "array":
299
- return z.preprocess((val) => {
300
- if (val === undefined || val === null || val === "")
301
- return [];
302
- if (Array.isArray(val))
303
- return val;
304
- return [val];
305
- }, z.array(z.any()));
405
+ return z.array(z.any());
306
406
  default:
307
407
  return z.any();
308
408
  }
309
409
  }
310
410
 
311
411
  // src/server/handlers.ts
312
- init_system_schema();
313
412
  var hydrateDoc = async (doc, fields, c, config) => {
314
413
  if (!doc)
315
414
  return doc;
@@ -484,7 +583,7 @@ var populateDoc = async (db, fields, doc, populate, config) => {
484
583
  }
485
584
  return doc;
486
585
  };
487
- function createHandlers(config, collection, getAuth) {
586
+ function createHandlers(collection, config) {
488
587
  const { db } = config;
489
588
  const checkAccess = async (c, action, data) => {
490
589
  const access = collection.access?.[action];
@@ -517,7 +616,8 @@ function createHandlers(config, collection, getAuth) {
517
616
  user,
518
617
  session,
519
618
  apiKey,
520
- data
619
+ data,
620
+ operation: action
521
621
  });
522
622
  };
523
623
  const getFieldAccessPermissions = async (c, operation, fields, data) => {
@@ -562,24 +662,18 @@ function createHandlers(config, collection, getAuth) {
562
662
  const saveVersion = async (doc, status) => {
563
663
  if (!collection.versions)
564
664
  return;
565
- const versionsTable = `${toSnakeCase(collection.slug)}_versions`;
566
- const versionDoc = {
567
- id: crypto.randomUUID(),
568
- _parent_id: doc.id,
569
- _version_data: JSON.stringify(doc),
570
- _status: status || doc._status || "published",
571
- created_at: new Date().toISOString()
572
- };
573
665
  try {
574
- await db.unsafe(`INSERT INTO ${versionsTable} (id, _parent_id, _version_data, _status, created_at) VALUES(?, ?, ?, ?, ?)`, [
575
- versionDoc.id,
576
- versionDoc._parent_id,
577
- versionDoc._version_data,
578
- versionDoc._status,
579
- versionDoc.created_at
580
- ]);
666
+ await db.db.insertInto("_doc_versions").values({
667
+ id: crypto.randomUUID(),
668
+ collection: collection.slug,
669
+ entity_id: doc.id,
670
+ data: JSON.stringify(doc),
671
+ status: status || doc._status || "published",
672
+ created_at: new Date().toISOString(),
673
+ updated_at: new Date().toISOString()
674
+ }).execute();
581
675
  } catch (err) {
582
- console.error(`[OpacaCMS] Failed to save version for ${collection.slug}: `, err);
676
+ logger.error(`[OpacaCMS] Failed to save version for ${collection.slug}: `, err);
583
677
  }
584
678
  };
585
679
  return {
@@ -597,8 +691,19 @@ function createHandlers(config, collection, getAuth) {
597
691
  const populate = parsePopulate(queries.populate);
598
692
  const filter = {};
599
693
  for (const [key, value] of Object.entries(queries)) {
600
- if (["page", "limit", "sort", "populate", "locale", "draft"].includes(key))
694
+ if ([
695
+ "page",
696
+ "limit",
697
+ "sort",
698
+ "populate",
699
+ "locale",
700
+ "draft",
701
+ "after",
702
+ "before",
703
+ "cursorColumn"
704
+ ].includes(key)) {
601
705
  continue;
706
+ }
602
707
  const match = key.match(/^([^[]+)\[([^\]]+)\]$/);
603
708
  if (match) {
604
709
  const field = match[1];
@@ -610,7 +715,17 @@ function createHandlers(config, collection, getAuth) {
610
715
  filter[key] = value;
611
716
  }
612
717
  }
613
- const results = await db.find(collection.slug, filter, { page, limit, sort });
718
+ const after = queries.after;
719
+ const before = queries.before;
720
+ const cursorColumn = queries.cursorColumn;
721
+ const results = await db.find(collection.slug, filter, {
722
+ page,
723
+ limit,
724
+ sort,
725
+ after,
726
+ before,
727
+ cursorColumn
728
+ });
614
729
  if (Object.keys(populate).length > 0) {
615
730
  results.docs = await Promise.all(results.docs.map((doc) => populateDoc(db, collection.fields, doc, populate, config)));
616
731
  }
@@ -676,13 +791,20 @@ function createHandlers(config, collection, getAuth) {
676
791
  await collection.hooks.afterCreate(doc);
677
792
  }
678
793
  if (collection.webhooks) {
679
- const afterCreateWebhooks = collection.webhooks.filter((w) => w.events.includes("afterCreate"));
794
+ const afterCreateWebhooks = collection.webhooks.filter((w) => w.type === "outgoing" && w.events.includes("after:create"));
680
795
  for (const webhook of afterCreateWebhooks) {
681
- const hookPromise = fetch(webhook.url, {
682
- method: "POST",
683
- headers: { "Content-Type": "application/json", ...webhook.headers },
684
- body: JSON.stringify(doc)
685
- }).catch((e) => logger.error(`Webhook afterCreate failed for ${collection.slug}: `, e));
796
+ if (webhook.type !== "outgoing")
797
+ continue;
798
+ const hookPromise = dispatchWebhook({
799
+ url: webhook.url,
800
+ data: doc,
801
+ event: "after:create",
802
+ collection: collection.slug,
803
+ headers: webhook.headers,
804
+ retries: webhook.retries,
805
+ timeoutMs: webhook.timeoutMs,
806
+ transform: webhook.transform
807
+ });
686
808
  if (c.executionCtx?.waitUntil) {
687
809
  c.executionCtx.waitUntil(hookPromise);
688
810
  }
@@ -736,13 +858,20 @@ function createHandlers(config, collection, getAuth) {
736
858
  await collection.hooks.afterUpdate(doc);
737
859
  }
738
860
  if (collection.webhooks) {
739
- const afterUpdateWebhooks = collection.webhooks.filter((w) => w.events.includes("afterUpdate"));
861
+ const afterUpdateWebhooks = collection.webhooks.filter((w) => w.type === "outgoing" && w.events.includes("after:update"));
740
862
  for (const webhook of afterUpdateWebhooks) {
741
- const hookPromise = fetch(webhook.url, {
742
- method: "POST",
743
- headers: { "Content-Type": "application/json", ...webhook.headers },
744
- body: JSON.stringify(doc)
745
- }).catch((e) => logger.error(`Webhook afterUpdate failed for ${collection.slug}: `, e));
863
+ if (webhook.type !== "outgoing")
864
+ continue;
865
+ const hookPromise = dispatchWebhook({
866
+ url: webhook.url,
867
+ data: doc,
868
+ event: "after:update",
869
+ collection: collection.slug,
870
+ headers: webhook.headers,
871
+ retries: webhook.retries,
872
+ timeoutMs: webhook.timeoutMs,
873
+ transform: webhook.transform
874
+ });
746
875
  if (c.executionCtx?.waitUntil) {
747
876
  c.executionCtx.waitUntil(hookPromise);
748
877
  }
@@ -761,7 +890,7 @@ function createHandlers(config, collection, getAuth) {
761
890
  const id = c.req.param("id");
762
891
  let docToPass = { id };
763
892
  try {
764
- if (collection.webhooks && collection.webhooks.some((w) => w.events.includes("afterDelete"))) {
893
+ if (collection.webhooks && collection.webhooks.some((w) => w.type === "outgoing" && w.events.includes("after:delete"))) {
765
894
  docToPass = await db.findOne(collection.slug, { id });
766
895
  }
767
896
  } catch (e) {}
@@ -773,13 +902,20 @@ function createHandlers(config, collection, getAuth) {
773
902
  await collection.hooks.afterDelete(id);
774
903
  }
775
904
  if (collection.webhooks) {
776
- const afterDeleteWebhooks = collection.webhooks.filter((w) => w.events.includes("afterDelete"));
905
+ const afterDeleteWebhooks = collection.webhooks.filter((w) => w.type === "outgoing" && w.events.includes("after:delete"));
777
906
  for (const webhook of afterDeleteWebhooks) {
778
- const hookPromise = fetch(webhook.url, {
779
- method: "POST",
780
- headers: { "Content-Type": "application/json", ...webhook.headers },
781
- body: JSON.stringify(docToPass || { id })
782
- }).catch((e) => logger.error(`Webhook afterDelete failed for ${collection.slug}: `, e));
907
+ if (webhook.type !== "outgoing")
908
+ continue;
909
+ const hookPromise = dispatchWebhook({
910
+ url: webhook.url,
911
+ data: docToPass || { id },
912
+ event: "after:delete",
913
+ collection: collection.slug,
914
+ headers: webhook.headers,
915
+ retries: webhook.retries,
916
+ timeoutMs: webhook.timeoutMs,
917
+ transform: webhook.transform
918
+ });
783
919
  if (c.executionCtx?.waitUntil) {
784
920
  c.executionCtx.waitUntil(hookPromise);
785
921
  }
@@ -792,11 +928,12 @@ function createHandlers(config, collection, getAuth) {
792
928
  return c.json({ message: "Forbidden" }, 403);
793
929
  }
794
930
  const parentId = c.req.query("parentId");
795
- const versionsTable = `${toSnakeCase(collection.slug)}_versions`;
796
- const query = parentId ? `SELECT * FROM ${versionsTable} WHERE _parent_id = ? ORDER BY created_at DESC` : `SELECT * FROM ${versionsTable} ORDER BY created_at DESC`;
797
- const params = parentId ? [parentId] : [];
798
931
  try {
799
- const rows = await db.unsafe(query, params);
932
+ let qb = db.db.selectFrom("_doc_versions").selectAll().where("collection", "=", collection.slug);
933
+ if (parentId) {
934
+ qb = qb.where("entity_id", "=", parentId);
935
+ }
936
+ const rows = await qb.orderBy("created_at", "desc").execute();
800
937
  return c.json({ docs: rows });
801
938
  } catch (err) {
802
939
  return c.json({ message: "Failed to fetch versions", error: err.message }, 500);
@@ -807,17 +944,15 @@ function createHandlers(config, collection, getAuth) {
807
944
  return c.json({ message: "Forbidden" }, 403);
808
945
  }
809
946
  const versionId = c.req.param("versionId");
810
- const versionsTable = `${toSnakeCase(collection.slug)}_versions`;
811
947
  try {
812
- const versionRows = await db.unsafe(`SELECT * FROM ${versionsTable} WHERE id = ? `, [versionId]);
813
- if (versionRows.length === 0)
948
+ const version = await db.db.selectFrom("_doc_versions").selectAll().where("id", "=", versionId).executeTakeFirst();
949
+ if (!version)
814
950
  return c.json({ message: "Version not found" }, 404);
815
- const version = versionRows[0];
816
- const data = JSON.parse(version._version_data);
817
- const id = version._parent_id;
951
+ const data = JSON.parse(version.data);
952
+ const id = version.entity_id;
818
953
  delete data.id;
819
- delete data.created_at;
820
- delete data.updated_at;
954
+ delete data.createdAt;
955
+ delete data.updatedAt;
821
956
  const doc = await db.update(collection.slug, { id }, data);
822
957
  await saveVersion(doc, "published");
823
958
  return c.json(doc);
@@ -827,6 +962,55 @@ function createHandlers(config, collection, getAuth) {
827
962
  }
828
963
  };
829
964
  }
965
+ function createIncomingWebhookHandler(webhookConfig, collection, config) {
966
+ const { db } = config;
967
+ if (!webhookConfig) {
968
+ throw new Error(`Collection ${collection.slug} does not have webhooksIncoming configured.`);
969
+ }
970
+ return async (c) => {
971
+ if (webhookConfig.verifySignature) {
972
+ const isValid = await verifyWebhookSignature(c.req.raw, webhookConfig.verifySignature);
973
+ if (!isValid) {
974
+ logger.warn(`[Webhooks] Invalid signature for incoming webhook on ${collection.slug}`);
975
+ return c.json({ message: "Invalid signature" }, 401);
976
+ }
977
+ }
978
+ try {
979
+ const payload = await c.req.json();
980
+ let data = payload;
981
+ if (webhookConfig.mapPayload) {
982
+ data = await webhookConfig.mapPayload(payload);
983
+ }
984
+ let doc = null;
985
+ let operation = "create";
986
+ const uniqueFields = collection.fields.filter((f) => f.name && f.unique && data[f.name] !== undefined);
987
+ if (data.id) {
988
+ doc = await db.findOne(collection.slug, { id: data.id });
989
+ } else if (uniqueFields.length > 0) {
990
+ const query = {};
991
+ for (const f of uniqueFields) {
992
+ const fieldName = f.name;
993
+ query[fieldName] = data[fieldName];
994
+ }
995
+ doc = await db.findOne(collection.slug, query);
996
+ }
997
+ if (doc) {
998
+ operation = "update";
999
+ doc = await db.update(collection.slug, { id: doc.id }, data);
1000
+ } else {
1001
+ doc = await db.create(collection.slug, data);
1002
+ }
1003
+ if (webhookConfig.onSuccess) {
1004
+ await webhookConfig.onSuccess({ data: doc, payload, operation });
1005
+ }
1006
+ logger.info(`[Webhooks] Incoming webhook processed for ${collection.slug} (${operation})`);
1007
+ return c.json({ success: true, operation, id: doc.id });
1008
+ } catch (err) {
1009
+ logger.error(`[Webhooks] Failed to process incoming webhook for ${collection.slug}:`, err.message);
1010
+ return c.json({ message: "Internal Server Error", error: err.message }, 500);
1011
+ }
1012
+ };
1013
+ }
830
1014
  function createGlobalHandlers(config, globalConfig, getAuth) {
831
1015
  const { db } = config;
832
1016
  const checkAccess = async (c, action, data) => {
@@ -843,7 +1027,8 @@ function createGlobalHandlers(config, globalConfig, getAuth) {
843
1027
  user,
844
1028
  session,
845
1029
  apiKey,
846
- data
1030
+ data,
1031
+ operation: action
847
1032
  });
848
1033
  };
849
1034
  return {
@@ -885,6 +1070,155 @@ function createGlobalHandlers(config, globalConfig, getAuth) {
885
1070
  // src/server/router.ts
886
1071
  import { Hono as Hono4 } from "hono";
887
1072
 
1073
+ // src/server/openapi.ts
1074
+ import { z as z2 } from "zod";
1075
+ import { zodToJsonSchema } from "zod-to-json-schema";
1076
+ function generateOpenAPISchema(config) {
1077
+ const openapi = {
1078
+ openapi: "3.1.0",
1079
+ info: {
1080
+ title: config.appName || "OpacaCMS API",
1081
+ version: "1.0.0",
1082
+ description: "Automatically generated OpenAPI schema for OpacaCMS Collections and Globals."
1083
+ },
1084
+ paths: {},
1085
+ components: {
1086
+ schemas: {},
1087
+ securitySchemes: {
1088
+ bearerAuth: {
1089
+ type: "http",
1090
+ scheme: "bearer"
1091
+ }
1092
+ }
1093
+ }
1094
+ };
1095
+ for (const collection of config.collections || []) {
1096
+ const isHidden = collection.hidden === true;
1097
+ if (isHidden)
1098
+ continue;
1099
+ const pathBase = `/api/${collection.apiPath || collection.slug}`;
1100
+ const tag = collection.label || collection.slug;
1101
+ const zodSchema = generateSchemaForCollection(collection, false, true);
1102
+ const schemaName = collection.slug.charAt(0).toUpperCase() + collection.slug.slice(1);
1103
+ const jsonSchema = typeof z2.toJSONSchema === "function" ? z2.toJSONSchema(zodSchema, { target: "openapi-3.0" }) : zodToJsonSchema(zodSchema, { target: "openApi3" });
1104
+ openapi.components.schemas[schemaName] = jsonSchema;
1105
+ const ref = `#/components/schemas/${schemaName}`;
1106
+ openapi.paths[pathBase] = {
1107
+ get: {
1108
+ tags: [tag],
1109
+ summary: `Find ${tag}`,
1110
+ parameters: [
1111
+ { name: "limit", in: "query", schema: { type: "integer" } },
1112
+ { name: "page", in: "query", schema: { type: "integer" } }
1113
+ ],
1114
+ responses: {
1115
+ "200": {
1116
+ description: "Successful response",
1117
+ content: {
1118
+ "application/json": {
1119
+ schema: {
1120
+ type: "object",
1121
+ properties: {
1122
+ docs: { type: "array", items: { $ref: ref } },
1123
+ totalDocs: { type: "integer" }
1124
+ }
1125
+ }
1126
+ }
1127
+ }
1128
+ }
1129
+ }
1130
+ },
1131
+ post: {
1132
+ tags: [tag],
1133
+ summary: `Create ${tag}`,
1134
+ requestBody: {
1135
+ required: true,
1136
+ content: {
1137
+ "application/json": { schema: { $ref: ref } }
1138
+ }
1139
+ },
1140
+ responses: {
1141
+ "201": {
1142
+ description: "Created successfully",
1143
+ content: { "application/json": { schema: { $ref: ref } } }
1144
+ }
1145
+ }
1146
+ }
1147
+ };
1148
+ openapi.paths[`${pathBase}/{id}`] = {
1149
+ get: {
1150
+ tags: [tag],
1151
+ summary: `Find ${tag} by ID`,
1152
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1153
+ responses: {
1154
+ "200": {
1155
+ description: "Successful response",
1156
+ content: { "application/json": { schema: { $ref: ref } } }
1157
+ },
1158
+ "404": { description: "Not found" }
1159
+ }
1160
+ },
1161
+ patch: {
1162
+ tags: [tag],
1163
+ summary: `Update ${tag}`,
1164
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1165
+ requestBody: {
1166
+ content: { "application/json": { schema: { $ref: ref } } }
1167
+ },
1168
+ responses: {
1169
+ "200": {
1170
+ description: "Updated successfully",
1171
+ content: { "application/json": { schema: { $ref: ref } } }
1172
+ }
1173
+ }
1174
+ },
1175
+ delete: {
1176
+ tags: [tag],
1177
+ summary: `Delete ${tag}`,
1178
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1179
+ responses: {
1180
+ "200": { description: "Deleted successfully" }
1181
+ }
1182
+ }
1183
+ };
1184
+ }
1185
+ for (const global of config.globals || []) {
1186
+ const pathBase = `/api/globals/${global.slug}`;
1187
+ const tag = global.label || global.slug;
1188
+ const zodSchema = generateSchemaForCollection(global, false, true);
1189
+ const schemaName = global.slug.charAt(0).toUpperCase() + global.slug.slice(1);
1190
+ const jsonSchema = typeof z2.toJSONSchema === "function" ? z2.toJSONSchema(zodSchema, { target: "openapi-3.0" }) : zodToJsonSchema(zodSchema, { target: "openApi3" });
1191
+ openapi.components.schemas[schemaName] = jsonSchema;
1192
+ const ref = `#/components/schemas/${schemaName}`;
1193
+ openapi.paths[pathBase] = {
1194
+ get: {
1195
+ tags: [tag],
1196
+ summary: `Find ${tag}`,
1197
+ responses: {
1198
+ "200": {
1199
+ description: "Successful response",
1200
+ content: { "application/json": { schema: { $ref: ref } } }
1201
+ }
1202
+ }
1203
+ },
1204
+ post: {
1205
+ tags: [tag],
1206
+ summary: `Update ${tag}`,
1207
+ requestBody: {
1208
+ content: { "application/json": { schema: { $ref: ref } } }
1209
+ },
1210
+ responses: {
1211
+ "200": {
1212
+ description: "Updated successfully",
1213
+ content: { "application/json": { schema: { $ref: ref } } }
1214
+ }
1215
+ }
1216
+ }
1217
+ };
1218
+ }
1219
+ return openapi;
1220
+ }
1221
+
888
1222
  // src/server/routers/admin.ts
889
1223
  import { Hono } from "hono";
890
1224
 
@@ -966,26 +1300,26 @@ function createAssetsHandlers(config) {
966
1300
  try {
967
1301
  try {
968
1302
  if (config.db.name === "sqlite" || config.db.name === "d1") {
969
- const tableInfo = await config.db.unsafe(`PRAGMA table_info(_opaca_assets)`);
1303
+ const tableInfo = await config.db.unsafe(`PRAGMA table_info(_assets)`);
970
1304
  const columns = tableInfo.map((c2) => c2.name);
971
1305
  if (!columns.includes("folder"))
972
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN folder TEXT`);
1306
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN folder TEXT`);
973
1307
  if (!columns.includes("alt_text"))
974
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN alt_text TEXT`);
1308
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN alt_text TEXT`);
975
1309
  if (!columns.includes("caption"))
976
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN caption TEXT`);
1310
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN caption TEXT`);
977
1311
  } else if (config.db.name === "postgres") {
978
1312
  const checkCols = await config.db.unsafe(`
979
1313
  SELECT column_name FROM information_schema.columns
980
- WHERE table_name = '_opaca_assets' AND column_name IN ('folder', 'alt_text', 'caption')
1314
+ WHERE table_name = '_assets' AND column_name IN ('folder', 'alt_text', 'caption')
981
1315
  `);
982
1316
  const existing = checkCols.map((c2) => c2.column_name);
983
1317
  if (!existing.includes("folder"))
984
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN folder TEXT`);
1318
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN folder TEXT`);
985
1319
  if (!existing.includes("alt_text"))
986
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN "alt_text" TEXT`);
1320
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN "alt_text" TEXT`);
987
1321
  if (!existing.includes("caption"))
988
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN caption TEXT`);
1322
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN caption TEXT`);
989
1323
  }
990
1324
  } catch (e) {
991
1325
  console.error("Auto-patch columns failed", e);
@@ -1016,7 +1350,7 @@ function createAssetsHandlers(config) {
1016
1350
  const storedKey = keyPrefix + uploadedFileData.filename;
1017
1351
  try {
1018
1352
  const assetId = (globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2)).replace(/-/g, "");
1019
- await config.db.create("_opaca_assets", {
1353
+ await config.db.create("_assets", {
1020
1354
  id: assetId,
1021
1355
  key: storedKey,
1022
1356
  filename: fileName,
@@ -1070,7 +1404,7 @@ function createAssetsHandlers(config) {
1070
1404
  query = { or: [{ folder: null }, { folder: "" }] };
1071
1405
  }
1072
1406
  }
1073
- const result = await config.db.find("_opaca_assets", query, {
1407
+ const result = await config.db.find("_assets", query, {
1074
1408
  page,
1075
1409
  limit,
1076
1410
  sort: "created_at:desc"
@@ -1080,9 +1414,9 @@ function createAssetsHandlers(config) {
1080
1414
  let folderRows = [];
1081
1415
  if (config.db.name === "postgres") {
1082
1416
  if (folder === null || folder === "") {
1083
- folderRows = await config.db.unsafe("SELECT DISTINCT split_part(folder, '/', 1) as subfolder, bucket FROM _opaca_assets WHERE folder IS NOT NULL AND folder != '' AND (bucket = $1 OR $1 = 'all')", [bucket]);
1417
+ folderRows = await config.db.unsafe("SELECT DISTINCT split_part(folder, '/', 1) as subfolder, bucket FROM _assets WHERE folder IS NOT NULL AND folder != '' AND (bucket = $1 OR $1 = 'all')", [bucket]);
1084
1418
  } else {
1085
- folderRows = await config.db.unsafe("SELECT DISTINCT split_part(substring(folder from length($1) + 2), '/', 1) as subfolder, bucket FROM _opaca_assets WHERE folder LIKE $2 AND (bucket = $3 OR $3 = 'all')", [folder, `${folder}/%`, bucket]);
1419
+ folderRows = await config.db.unsafe("SELECT DISTINCT split_part(substring(folder from length($1) + 2), '/', 1) as subfolder, bucket FROM _assets WHERE folder LIKE $2 AND (bucket = $3 OR $3 = 'all')", [folder, `${folder}/%`, bucket]);
1086
1420
  }
1087
1421
  } else {
1088
1422
  if (folder === null || folder === "") {
@@ -1093,7 +1427,7 @@ function createAssetsHandlers(config) {
1093
1427
  ELSE folder
1094
1428
  END as subfolder,
1095
1429
  bucket
1096
- FROM _opaca_assets WHERE folder IS NOT NULL AND folder != '' AND (bucket = ? OR ? = 'all')
1430
+ FROM _assets WHERE folder IS NOT NULL AND folder != '' AND (bucket = ? OR ? = 'all')
1097
1431
  `, [bucket, bucket]);
1098
1432
  } else {
1099
1433
  const skipLen = folder.length + 2;
@@ -1104,7 +1438,7 @@ function createAssetsHandlers(config) {
1104
1438
  ELSE SUBSTR(folder, ?)
1105
1439
  END as subfolder,
1106
1440
  bucket
1107
- FROM _opaca_assets WHERE folder LIKE ? AND (bucket = ? OR ? = 'all')
1441
+ FROM _assets WHERE folder LIKE ? AND (bucket = ? OR ? = 'all')
1108
1442
  `, [skipLen, skipLen, skipLen, skipLen, `${folder}/%`, bucket, bucket]);
1109
1443
  }
1110
1444
  }
@@ -1156,7 +1490,7 @@ function createAssetsHandlers(config) {
1156
1490
  async serve(c) {
1157
1491
  const id = c.req.param("id");
1158
1492
  try {
1159
- const asset = await config.db.findOne("_opaca_assets", { id });
1493
+ const asset = await config.db.findOne("_assets", { id });
1160
1494
  if (!asset) {
1161
1495
  return c.json({ error: "Asset not found" }, 404);
1162
1496
  }
@@ -1191,7 +1525,7 @@ function mountCollectionRoutes(router, config, state) {
1191
1525
  }
1192
1526
  const exposedCollections = combinedCollections.filter((c) => !c.hidden);
1193
1527
  for (const collection of exposedCollections) {
1194
- const handlers = createHandlers(config, collection, () => state.auth);
1528
+ const handlers = createHandlers(collection, config);
1195
1529
  const path = `/${collection.apiPath || collection.slug}`;
1196
1530
  router.get(path, handlers.find);
1197
1531
  router.get(`${path}/versions`, handlers.findVersions);
@@ -1202,6 +1536,18 @@ function mountCollectionRoutes(router, config, state) {
1202
1536
  router.delete(`${path}/:id`, handlers.delete);
1203
1537
  }
1204
1538
  }
1539
+ function mountIncomingWebhookRoutes(router, config) {
1540
+ for (const collection of config.collections) {
1541
+ if (collection.webhooks) {
1542
+ for (const webhook of collection.webhooks) {
1543
+ if (webhook.type === "incoming") {
1544
+ const handler = createIncomingWebhookHandler(webhook, collection, config);
1545
+ router.post(webhook.path, handler);
1546
+ }
1547
+ }
1548
+ }
1549
+ }
1550
+ }
1205
1551
  function mountGlobalRoutes(router, config, state) {
1206
1552
  if (config.globals) {
1207
1553
  for (const globalConfig of config.globals) {
@@ -1249,8 +1595,8 @@ function createAssetsServingRouter(config) {
1249
1595
  const assetsServingRouter = new Hono3;
1250
1596
  if (config.storages) {
1251
1597
  const assetsHandlers = createAssetsHandlers(config);
1252
- const assetCol = getSystemCollections().find((c) => c.slug === "_opaca_assets");
1253
- const assetPath = `/${assetCol?.apiPath || assetCol?.slug || "_opaca_assets"}`;
1598
+ const assetCol = getSystemCollections().find((c) => c.slug === "_assets");
1599
+ const assetPath = `/${assetCol?.apiPath || assetCol?.slug || "_assets"}`;
1254
1600
  assetsServingRouter.get(`${assetPath}/:id/view`, assetsHandlers.serve);
1255
1601
  }
1256
1602
  return assetsServingRouter;
@@ -1267,13 +1613,17 @@ function createAuthMiddleware(getAuth) {
1267
1613
  await next();
1268
1614
  return;
1269
1615
  }
1270
- const session = await auth.api.getSession({ headers: c.req.raw.headers });
1271
- if (session) {
1272
- c.set("user", session.user);
1273
- c.set("session", session.session);
1274
- c.set("apiKey", null);
1275
- await next();
1276
- return;
1616
+ try {
1617
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
1618
+ if (session) {
1619
+ c.set("user", session.user);
1620
+ c.set("session", session.session);
1621
+ c.set("apiKey", null);
1622
+ await next();
1623
+ return;
1624
+ }
1625
+ } catch (err) {
1626
+ logger.debug("Session verification failed or threw:", err);
1277
1627
  }
1278
1628
  const authHeader = c.req.header("Authorization");
1279
1629
  if (authHeader && authHeader.startsWith("Bearer ")) {
@@ -1453,7 +1803,6 @@ function createDatabaseInitMiddleware(config, state) {
1453
1803
  }
1454
1804
 
1455
1805
  // src/server/middlewares/rate-limit.ts
1456
- import { rateLimiter } from "hono-rate-limiter";
1457
1806
  function createRateLimitMiddleware(config) {
1458
1807
  const rateLimitConfig = config.api?.rateLimit;
1459
1808
  if (rateLimitConfig?.enabled === false) {
@@ -1470,11 +1819,17 @@ function createRateLimitMiddleware(config) {
1470
1819
  }
1471
1820
  }
1472
1821
  if (provider) {
1473
- const limiter2 = rateLimiter({
1474
- binding: () => provider,
1475
- keyGenerator: rateLimitConfig?.keyGenerator || ((c2) => c2.req.header("cf-connecting-ip") || c2.req.header("x-forwarded-for") || "anonymous")
1476
- });
1477
- return limiter2(c, next);
1822
+ try {
1823
+ const { rateLimiter } = await import("hono-rate-limiter");
1824
+ const limiter = rateLimiter({
1825
+ binding: () => provider,
1826
+ keyGenerator: rateLimitConfig?.keyGenerator || ((c2) => c2.req.header("cf-connecting-ip") || c2.req.header("x-forwarded-for") || "anonymous")
1827
+ });
1828
+ return limiter(c, next);
1829
+ } catch (e) {
1830
+ logger.error("Failed to load 'hono-rate-limiter'. Please install it to use rate limiting features.", e);
1831
+ return await next();
1832
+ }
1478
1833
  }
1479
1834
  let resolvedStore = rateLimitConfig?.store;
1480
1835
  if (!resolvedStore && c.env) {
@@ -1488,15 +1843,21 @@ function createRateLimitMiddleware(config) {
1488
1843
  }
1489
1844
  }
1490
1845
  }
1491
- const limiter = rateLimiter({
1492
- windowMs,
1493
- limit,
1494
- standardHeaders: "draft-6",
1495
- store: resolvedStore,
1496
- keyGenerator: rateLimitConfig?.keyGenerator || ((c2) => c2.req.header("cf-connecting-ip") || c2.req.header("x-forwarded-for") || "anonymous"),
1497
- message: "Too many requests from this IP, please try again after a minute."
1498
- });
1499
- return limiter(c, next);
1846
+ try {
1847
+ const { rateLimiter } = await import("hono-rate-limiter");
1848
+ const limiter = rateLimiter({
1849
+ windowMs,
1850
+ limit,
1851
+ standardHeaders: "draft-6",
1852
+ store: resolvedStore,
1853
+ keyGenerator: rateLimitConfig?.keyGenerator || ((c2) => c2.req.header("cf-connecting-ip") || c2.req.header("x-forwarded-for") || "anonymous"),
1854
+ message: "Too many requests from this IP, please try again after a minute."
1855
+ });
1856
+ return limiter(c, next);
1857
+ } catch (e) {
1858
+ logger.error("Failed to load 'hono-rate-limiter'. Please install it to use rate limiting features.", e);
1859
+ return await next();
1860
+ }
1500
1861
  };
1501
1862
  }
1502
1863
 
@@ -1518,163 +1879,319 @@ function setupMiddlewares(router, config, state) {
1518
1879
  }
1519
1880
  function setupAuthMiddlewares(router, config, state) {
1520
1881
  router.use("*", createAuthMiddleware(() => state.auth));
1882
+ router.use("*", async (c, next) => {
1883
+ const user = c.get("user");
1884
+ await requestContext.run({ userId: user?.id }, async () => {
1885
+ await next();
1886
+ });
1887
+ });
1521
1888
  }
1522
1889
 
1523
- // src/server/openapi.ts
1524
- import { z as z2 } from "zod";
1525
- import { zodToJsonSchema } from "zod-to-json-schema";
1526
- function generateOpenAPISchema(config) {
1527
- const openapi = {
1528
- openapi: "3.1.0",
1529
- info: {
1530
- title: config.appName || "OpacaCMS API",
1531
- version: "1.0.0",
1532
- description: "Automatically generated OpenAPI schema for OpacaCMS Collections and Globals."
1533
- },
1534
- paths: {},
1535
- components: {
1536
- schemas: {},
1537
- securitySchemes: {
1538
- bearerAuth: {
1539
- type: "http",
1540
- scheme: "bearer"
1541
- }
1542
- }
1890
+ // src/server/graphql.ts
1891
+ import {
1892
+ GraphQLBoolean,
1893
+ GraphQLFloat,
1894
+ GraphQLInt,
1895
+ GraphQLList,
1896
+ GraphQLNonNull,
1897
+ GraphQLObjectType,
1898
+ GraphQLScalarType,
1899
+ GraphQLSchema,
1900
+ GraphQLString
1901
+ } from "graphql";
1902
+ var GraphQLJSON = new GraphQLScalarType({
1903
+ name: "JSON",
1904
+ description: "The `JSON` scalar type represents JSON values as specified by ECMA-404",
1905
+ serialize: (value) => value,
1906
+ parseValue: (value) => value,
1907
+ parseLiteral: (ast) => {
1908
+ switch (ast.kind) {
1909
+ case "StringValue":
1910
+ return JSON.parse(ast.value);
1911
+ case "ObjectValue":
1912
+ throw new Error("Not implemented");
1913
+ default:
1914
+ return null;
1543
1915
  }
1916
+ }
1917
+ });
1918
+ function getGraphQLType(field) {
1919
+ switch (field.type) {
1920
+ case "text":
1921
+ case "slug":
1922
+ case "textarea":
1923
+ case "richtext":
1924
+ case "select":
1925
+ case "radio":
1926
+ case "date":
1927
+ case "file":
1928
+ case "relationship":
1929
+ return GraphQLString;
1930
+ case "number":
1931
+ return GraphQLFloat;
1932
+ case "boolean":
1933
+ return GraphQLBoolean;
1934
+ case "json":
1935
+ case "blocks":
1936
+ case "array":
1937
+ case "group":
1938
+ case "row":
1939
+ case "collapsible":
1940
+ case "tabs":
1941
+ return GraphQLJSON;
1942
+ default:
1943
+ return GraphQLString;
1944
+ }
1945
+ }
1946
+ function buildCollectionType(collection, nameOverride) {
1947
+ const fields = {
1948
+ id: { type: GraphQLString }
1544
1949
  };
1545
- for (const collection of config.collections || []) {
1546
- const isHidden = collection.hidden === true;
1547
- if (isHidden)
1950
+ if (collection.timestamps !== false) {
1951
+ fields.createdAt = { type: GraphQLString };
1952
+ fields.updatedAt = { type: GraphQLString };
1953
+ }
1954
+ for (const field of collection.fields) {
1955
+ if (field.name) {
1956
+ fields[sanitizeGraphQLName(field.name)] = { type: getGraphQLType(field) };
1957
+ }
1958
+ }
1959
+ return new GraphQLObjectType({
1960
+ name: nameOverride || sanitizeGraphQLName(collection.slug),
1961
+ fields
1962
+ });
1963
+ }
1964
+ function buildCollectionPaginatedType(collectionType, collectionSlug) {
1965
+ return new GraphQLObjectType({
1966
+ name: `Paginated${sanitizeGraphQLName(collectionSlug)}`,
1967
+ fields: {
1968
+ docs: { type: new GraphQLList(collectionType) },
1969
+ totalDocs: { type: GraphQLInt },
1970
+ limit: { type: GraphQLInt },
1971
+ totalPages: { type: GraphQLInt },
1972
+ page: { type: GraphQLInt },
1973
+ pagingCounter: { type: GraphQLInt },
1974
+ hasPrevPage: { type: GraphQLBoolean },
1975
+ hasNextPage: { type: GraphQLBoolean },
1976
+ prevPage: { type: GraphQLInt },
1977
+ nextPage: { type: GraphQLInt },
1978
+ nextCursor: { type: GraphQLString },
1979
+ prevCursor: { type: GraphQLString }
1980
+ }
1981
+ });
1982
+ }
1983
+ function generateGraphQLSchema(config, state) {
1984
+ const queryFields = {};
1985
+ const mutationFields = {};
1986
+ for (const collection of config.collections) {
1987
+ if (collection.hidden)
1548
1988
  continue;
1549
- const pathBase = `/api/${collection.apiPath || collection.slug}`;
1550
- const tag = collection.label || collection.slug;
1551
- const zodSchema = generateSchemaForCollection(collection, false, true);
1552
- const schemaName = collection.slug.charAt(0).toUpperCase() + collection.slug.slice(1);
1553
- const jsonSchema = typeof z2.toJSONSchema === "function" ? z2.toJSONSchema(zodSchema, { target: "openapi-3.0" }) : zodToJsonSchema(zodSchema, { target: "openApi3" });
1554
- openapi.components.schemas[schemaName] = jsonSchema;
1555
- const ref = `#/components/schemas/${schemaName}`;
1556
- openapi.paths[pathBase] = {
1557
- get: {
1558
- tags: [tag],
1559
- summary: `Find ${tag}`,
1560
- parameters: [
1561
- { name: "limit", in: "query", schema: { type: "integer" } },
1562
- { name: "page", in: "query", schema: { type: "integer" } }
1563
- ],
1564
- responses: {
1565
- "200": {
1566
- description: "Successful response",
1567
- content: {
1568
- "application/json": {
1569
- schema: {
1570
- type: "object",
1571
- properties: {
1572
- docs: { type: "array", items: { $ref: ref } },
1573
- totalDocs: { type: "integer" }
1574
- }
1575
- }
1576
- }
1577
- }
1578
- }
1579
- }
1989
+ const handlers = createHandlers(collection, config);
1990
+ const collectionType = buildCollectionType(collection);
1991
+ const paginatedType = buildCollectionPaginatedType(collectionType, collection.slug);
1992
+ const sanitizedSlug = sanitizeGraphQLName(collection.slug);
1993
+ const capitalizedSlug = sanitizedSlug.charAt(0).toUpperCase() + sanitizedSlug.slice(1);
1994
+ queryFields[`find${capitalizedSlug}`] = {
1995
+ type: paginatedType,
1996
+ args: {
1997
+ limit: { type: GraphQLInt },
1998
+ page: { type: GraphQLInt },
1999
+ sort: { type: GraphQLString }
1580
2000
  },
1581
- post: {
1582
- tags: [tag],
1583
- summary: `Create ${tag}`,
1584
- requestBody: {
1585
- required: true,
1586
- content: {
1587
- "application/json": { schema: { $ref: ref } }
1588
- }
1589
- },
1590
- responses: {
1591
- "201": {
1592
- description: "Created successfully",
1593
- content: { "application/json": { schema: { $ref: ref } } }
2001
+ resolve: async (_, args, c) => {
2002
+ const req = {
2003
+ query: (key) => {
2004
+ if (!key)
2005
+ return args;
2006
+ return args[key] !== undefined ? String(args[key]) : undefined;
2007
+ },
2008
+ queries: () => ({})
2009
+ };
2010
+ const ctx = { ...c, req: { ...c.req, ...req } };
2011
+ ctx.req.query = req.query;
2012
+ let result;
2013
+ ctx.json = (data, status) => {
2014
+ if (status && status >= 400)
2015
+ throw new Error(data?.message || "Error");
2016
+ result = data;
2017
+ return data;
2018
+ };
2019
+ await handlers.find(ctx);
2020
+ return result;
2021
+ }
2022
+ };
2023
+ queryFields[`get${capitalizedSlug}`] = {
2024
+ type: collectionType,
2025
+ args: {
2026
+ id: { type: new GraphQLNonNull(GraphQLString) }
2027
+ },
2028
+ resolve: async (_, args, c) => {
2029
+ const req = {
2030
+ param: (key) => {
2031
+ if (key === "id")
2032
+ return args.id;
2033
+ if (!key)
2034
+ return { id: args.id };
2035
+ return args.id;
1594
2036
  }
1595
- }
2037
+ };
2038
+ const ctx = { ...c, req: { ...c.req, ...req } };
2039
+ ctx.req.param = req.param;
2040
+ let result;
2041
+ ctx.json = (data, status) => {
2042
+ if (status && status >= 400)
2043
+ throw new Error(data?.message || "Error");
2044
+ result = data;
2045
+ return data;
2046
+ };
2047
+ await handlers.findOne(ctx);
2048
+ return result?.doc || result;
1596
2049
  }
1597
2050
  };
1598
- openapi.paths[`${pathBase}/{id}`] = {
1599
- get: {
1600
- tags: [tag],
1601
- summary: `Find ${tag} by ID`,
1602
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1603
- responses: {
1604
- "200": {
1605
- description: "Successful response",
1606
- content: { "application/json": { schema: { $ref: ref } } }
2051
+ mutationFields[`create${capitalizedSlug}`] = {
2052
+ type: collectionType,
2053
+ args: {
2054
+ data: { type: new GraphQLNonNull(GraphQLJSON) }
2055
+ },
2056
+ resolve: async (_, args, c) => {
2057
+ const req = {
2058
+ json: async () => args.data
2059
+ };
2060
+ const ctx = { ...c, req: { ...c.req, ...req } };
2061
+ let result;
2062
+ ctx.json = (data, status) => {
2063
+ if (status && status >= 400)
2064
+ throw new Error(data?.message || "Error");
2065
+ result = data;
2066
+ return data;
2067
+ };
2068
+ await handlers.create(ctx);
2069
+ return result?.doc || result;
2070
+ }
2071
+ };
2072
+ mutationFields[`update${capitalizedSlug}`] = {
2073
+ type: collectionType,
2074
+ args: {
2075
+ id: { type: new GraphQLNonNull(GraphQLString) },
2076
+ data: { type: new GraphQLNonNull(GraphQLJSON) }
2077
+ },
2078
+ resolve: async (_, args, c) => {
2079
+ const req = {
2080
+ param: (key) => {
2081
+ if (key === "id")
2082
+ return args.id;
2083
+ if (!key)
2084
+ return { id: args.id };
2085
+ return args.id;
1607
2086
  },
1608
- "404": { description: "Not found" }
1609
- }
2087
+ json: async () => args.data
2088
+ };
2089
+ const ctx = { ...c, req: { ...c.req, ...req } };
2090
+ ctx.req.param = req.param;
2091
+ let result;
2092
+ ctx.json = (data, status) => {
2093
+ if (status && status >= 400)
2094
+ throw new Error(data?.message || "Error");
2095
+ result = data;
2096
+ return data;
2097
+ };
2098
+ await handlers.update(ctx);
2099
+ return result?.doc || result;
2100
+ }
2101
+ };
2102
+ mutationFields[`delete${capitalizedSlug}`] = {
2103
+ type: GraphQLBoolean,
2104
+ args: {
2105
+ id: { type: new GraphQLNonNull(GraphQLString) }
1610
2106
  },
1611
- patch: {
1612
- tags: [tag],
1613
- summary: `Update ${tag}`,
1614
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1615
- requestBody: {
1616
- content: { "application/json": { schema: { $ref: ref } } }
1617
- },
1618
- responses: {
1619
- "200": {
1620
- description: "Updated successfully",
1621
- content: { "application/json": { schema: { $ref: ref } } }
2107
+ resolve: async (_, args, c) => {
2108
+ const req = {
2109
+ param: (key) => {
2110
+ if (key === "id")
2111
+ return args.id;
2112
+ if (!key)
2113
+ return { id: args.id };
2114
+ return args.id;
1622
2115
  }
1623
- }
1624
- },
1625
- delete: {
1626
- tags: [tag],
1627
- summary: `Delete ${tag}`,
1628
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1629
- responses: {
1630
- "200": { description: "Deleted successfully" }
1631
- }
2116
+ };
2117
+ const ctx = { ...c, req: { ...c.req, ...req } };
2118
+ ctx.req.param = req.param;
2119
+ let result;
2120
+ ctx.json = (data, status) => {
2121
+ if (status && status >= 400)
2122
+ throw new Error(data?.message || "Error");
2123
+ result = data;
2124
+ return data;
2125
+ };
2126
+ await handlers.delete(ctx);
2127
+ return result;
1632
2128
  }
1633
2129
  };
1634
2130
  }
1635
- for (const global of config.globals || []) {
1636
- const pathBase = `/api/globals/${global.slug}`;
1637
- const tag = global.label || global.slug;
1638
- const zodSchema = generateSchemaForCollection(global, false, true);
1639
- const schemaName = global.slug.charAt(0).toUpperCase() + global.slug.slice(1);
1640
- const jsonSchema = typeof z2.toJSONSchema === "function" ? z2.toJSONSchema(zodSchema, { target: "openapi-3.0" }) : zodToJsonSchema(zodSchema, { target: "openApi3" });
1641
- openapi.components.schemas[schemaName] = jsonSchema;
1642
- const ref = `#/components/schemas/${schemaName}`;
1643
- openapi.paths[pathBase] = {
1644
- get: {
1645
- tags: [tag],
1646
- summary: `Find ${tag}`,
1647
- responses: {
1648
- "200": {
1649
- description: "Successful response",
1650
- content: { "application/json": { schema: { $ref: ref } } }
1651
- }
2131
+ if (config.globals) {
2132
+ for (const globalConfig of config.globals) {
2133
+ const handlers = createGlobalHandlers(config, globalConfig, () => state.auth);
2134
+ const sanitizedGlobalSlug = sanitizeGraphQLName(globalConfig.slug);
2135
+ const capitalizedGlobalSlug = sanitizedGlobalSlug.charAt(0).toUpperCase() + sanitizedGlobalSlug.slice(1);
2136
+ const globalType = buildCollectionType(globalConfig, `Global_${sanitizedGlobalSlug}`);
2137
+ queryFields[`get${capitalizedGlobalSlug}`] = {
2138
+ type: globalType,
2139
+ resolve: async (_, __, c) => {
2140
+ const req = {};
2141
+ const ctx = { ...c, req: { ...c.req, ...req } };
2142
+ let result;
2143
+ ctx.json = (data) => {
2144
+ result = data;
2145
+ return data;
2146
+ };
2147
+ await handlers.find(ctx);
2148
+ return result;
1652
2149
  }
1653
- },
1654
- post: {
1655
- tags: [tag],
1656
- summary: `Update ${tag}`,
1657
- requestBody: {
1658
- content: { "application/json": { schema: { $ref: ref } } }
2150
+ };
2151
+ mutationFields[`update${capitalizedGlobalSlug}`] = {
2152
+ type: globalType,
2153
+ args: {
2154
+ data: { type: new GraphQLNonNull(GraphQLJSON) }
1659
2155
  },
1660
- responses: {
1661
- "200": {
1662
- description: "Updated successfully",
1663
- content: { "application/json": { schema: { $ref: ref } } }
1664
- }
2156
+ resolve: async (_, args, c) => {
2157
+ const req = {
2158
+ json: async () => args.data
2159
+ };
2160
+ const ctx = { ...c, req: { ...c.req, ...req } };
2161
+ let result;
2162
+ ctx.json = (data) => {
2163
+ result = data;
2164
+ return data;
2165
+ };
2166
+ await handlers.update(ctx);
2167
+ return result;
1665
2168
  }
1666
- }
1667
- };
2169
+ };
2170
+ }
1668
2171
  }
1669
- return openapi;
2172
+ if (Object.keys(queryFields).length === 0) {
2173
+ queryFields._empty = { type: GraphQLString, resolve: () => "Empty" };
2174
+ }
2175
+ const queryType = new GraphQLObjectType({
2176
+ name: "Query",
2177
+ fields: queryFields
2178
+ });
2179
+ const schemaConfig = { query: queryType };
2180
+ if (Object.keys(mutationFields).length > 0) {
2181
+ schemaConfig.mutation = new GraphQLObjectType({
2182
+ name: "Mutation",
2183
+ fields: mutationFields
2184
+ });
2185
+ }
2186
+ return new GraphQLSchema(schemaConfig);
1670
2187
  }
1671
2188
 
1672
2189
  // src/server/routers/plugins.ts
1673
- function mountPluginRoutes(config, settings, logger2, router) {
2190
+ function mountPluginRoutes(config, settings, logger2, router, env = {}) {
1674
2191
  if (config.plugins && Array.isArray(config.plugins)) {
1675
2192
  for (const plugin of config.plugins) {
1676
2193
  const pluginSettings = settings[plugin.name] || {};
1677
- const pluginContext = { config, logger: logger2, settings: pluginSettings };
2194
+ const pluginContext = { config, logger: logger2, settings: pluginSettings, env };
1678
2195
  if (plugin.onRequest) {
1679
2196
  router.use("*", async (c, next) => {
1680
2197
  const result = await plugin.onRequest(c);
@@ -1694,21 +2211,22 @@ function mountPluginRoutes(config, settings, logger2, router) {
1694
2211
  }
1695
2212
  }
1696
2213
  }
1697
- function firePluginInitComplete(config, settings, logger2) {
2214
+ function firePluginInitComplete(config, settings, logger2, env = {}) {
1698
2215
  if (config.plugins && Array.isArray(config.plugins)) {
1699
2216
  for (const plugin of config.plugins) {
1700
2217
  if (plugin.onInitComplete) {
1701
2218
  const pluginSettings = settings[plugin.name] || {};
1702
- plugin.onInitComplete({ config, logger: logger2, settings: pluginSettings });
2219
+ plugin.onInitComplete({ config, logger: logger2, settings: pluginSettings, env });
1703
2220
  }
1704
2221
  }
1705
2222
  }
1706
2223
  }
1707
2224
 
1708
2225
  // src/server/router.ts
1709
- function createAPIRouter(config, settings = {}) {
2226
+ function createAPIRouter(config, settings = {}, env = {}) {
1710
2227
  const state = { auth: undefined, migrated: false };
1711
- const router = new Hono4().basePath("/api");
2228
+ const app = new Hono4;
2229
+ const router = new Hono4;
1712
2230
  setupMiddlewares(router, config, state);
1713
2231
  setupAuthMiddlewares(router, config, state);
1714
2232
  router.get("/", (c) => {
@@ -1718,9 +2236,30 @@ function createAPIRouter(config, settings = {}) {
1718
2236
  router.route("/__admin", createAdminRouter(config, settings, state));
1719
2237
  router.route("/__system", createSystemRouter(config));
1720
2238
  router.route("/", createAssetsServingRouter(config));
1721
- mountPluginRoutes(config, settings, logger, router);
1722
- mountCollectionRoutes(router, config, state);
1723
- mountGlobalRoutes(router, config, state);
2239
+ mountPluginRoutes(config, settings, logger, router, env);
2240
+ const isRestEnabled = config.api?.rest?.enabled !== false;
2241
+ const isGraphQLEnabled = config.api?.graphql?.enabled === true;
2242
+ if (isRestEnabled) {
2243
+ mountCollectionRoutes(router, config, state);
2244
+ mountGlobalRoutes(router, config, state);
2245
+ mountIncomingWebhookRoutes(router, config);
2246
+ }
2247
+ if (isGraphQLEnabled) {
2248
+ const graphqlPath = config.api?.graphql?.path || "/graphql";
2249
+ const schema = generateGraphQLSchema(config, state);
2250
+ router.use(graphqlPath, async (c, next) => {
2251
+ try {
2252
+ const { graphqlServer } = await import("@hono/graphql-server");
2253
+ return graphqlServer({
2254
+ schema,
2255
+ graphiql: config.api?.graphql?.graphiql ?? false
2256
+ })(c, next);
2257
+ } catch (e) {
2258
+ logger.error("Failed to load @hono/graphql-server. Please install 'graphql' and '@hono/graphql-server' to use GraphQL features.", e);
2259
+ return c.json({ error: "GraphQL is enabled but dependencies are missing." }, 500);
2260
+ }
2261
+ });
2262
+ }
1724
2263
  if (config.api?.openAPI?.enabled) {
1725
2264
  router.get("/open-api.json", (c) => {
1726
2265
  const schema = generateOpenAPISchema(config);
@@ -1746,8 +2285,14 @@ function createAPIRouter(config, settings = {}) {
1746
2285
  })(c, next);
1747
2286
  });
1748
2287
  }
1749
- firePluginInitComplete(config, settings, logger);
1750
- return router;
2288
+ firePluginInitComplete(config, settings, logger, env);
2289
+ app.route("/api", router);
2290
+ app.get("/", (c) => c.redirect("/api"));
2291
+ app.notFound((c) => {
2292
+ console.error(`[OpacaRouter] 404 Not Found: ${c.req.method} ${c.req.url}`);
2293
+ return c.json({ message: "Not Found", path: c.req.path }, 404);
2294
+ });
2295
+ return app;
1751
2296
  }
1752
2297
 
1753
- export { createAdminHandlers, hydrateDoc, parsePopulate, populateDoc, createHandlers, createGlobalHandlers, createAPIRouter };
2298
+ export { createAdminHandlers, hydrateDoc, parsePopulate, populateDoc, createHandlers, createIncomingWebhookHandler, createGlobalHandlers, createAPIRouter };