opacacms 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +16 -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-51z3x7kq.js → chunk-a3qae86h.js} +1 -1
  44. package/dist/{chunk-3rdhbedb.js → chunk-adq2b75c.js} +2 -2
  45. package/dist/chunk-d0tb1xjw.js +93 -0
  46. package/dist/chunk-d7cgd6vn.js +318 -0
  47. package/dist/{chunk-6d1vdfwa.js → chunk-e0g6gn7n.js} +54 -75
  48. package/dist/chunk-ec4jhybj.js +1137 -0
  49. package/dist/chunk-fatyf6f7.js +221 -0
  50. package/dist/{chunk-526a3gqx.js → chunk-fnsf1dfm.js} +1 -1
  51. package/dist/chunk-g9bxb6h0.js +205 -0
  52. package/dist/chunk-gyaf5kgf.js +10 -0
  53. package/dist/{chunk-9kxpbcb1.js → chunk-h6dhexzr.js} +16 -7
  54. package/dist/{chunk-dykn5hr6.js → chunk-j8js1y0h.js} +31 -74
  55. package/dist/{chunk-t0zg026p.js → chunk-jq1drsen.js} +12 -1
  56. package/dist/{chunk-b3kr8w41.js → chunk-m24yqkeq.js} +38 -26
  57. package/dist/chunk-m5ems3hh.js +410 -0
  58. package/dist/{chunk-8scgdznr.js → chunk-m83ybzf8.js} +15 -18
  59. package/dist/chunk-majsbncm.js +98 -0
  60. package/dist/chunk-mp2gt9yh.js +237 -0
  61. package/dist/chunk-n1twhqmf.js +54 -0
  62. package/dist/{chunk-bygjkgrx.js → chunk-naqcqj8n.js} +57 -80
  63. package/dist/chunk-q5sb5dcr.js +15 -0
  64. package/dist/{chunk-06ks4ggh.js → chunk-qhdsjek6.js} +49 -89
  65. package/dist/{chunk-n133qpsm.js → chunk-qsh2nqz3.js} +50 -81
  66. package/dist/chunk-r0ms5tk1.js +76 -0
  67. package/dist/chunk-rwqwsanx.js +75 -0
  68. package/dist/chunk-sqsfk9p4.js +700 -0
  69. package/dist/{chunk-5gvbp2qa.js → chunk-x7bnzswh.js} +25 -18
  70. package/dist/{chunk-2es275xs.js → chunk-z3ffn2b7.js} +834 -307
  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 +11 -8
  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 {
@@ -883,8 +1068,158 @@ function createGlobalHandlers(config, globalConfig, getAuth) {
883
1068
  }
884
1069
 
885
1070
  // src/server/router.ts
1071
+ import { graphqlServer } from "@hono/graphql-server";
886
1072
  import { Hono as Hono4 } from "hono";
887
1073
 
1074
+ // src/server/openapi.ts
1075
+ import { z as z2 } from "zod";
1076
+ import { zodToJsonSchema } from "zod-to-json-schema";
1077
+ function generateOpenAPISchema(config) {
1078
+ const openapi = {
1079
+ openapi: "3.1.0",
1080
+ info: {
1081
+ title: config.appName || "OpacaCMS API",
1082
+ version: "1.0.0",
1083
+ description: "Automatically generated OpenAPI schema for OpacaCMS Collections and Globals."
1084
+ },
1085
+ paths: {},
1086
+ components: {
1087
+ schemas: {},
1088
+ securitySchemes: {
1089
+ bearerAuth: {
1090
+ type: "http",
1091
+ scheme: "bearer"
1092
+ }
1093
+ }
1094
+ }
1095
+ };
1096
+ for (const collection of config.collections || []) {
1097
+ const isHidden = collection.hidden === true;
1098
+ if (isHidden)
1099
+ continue;
1100
+ const pathBase = `/api/${collection.apiPath || collection.slug}`;
1101
+ const tag = collection.label || collection.slug;
1102
+ const zodSchema = generateSchemaForCollection(collection, false, true);
1103
+ const schemaName = collection.slug.charAt(0).toUpperCase() + collection.slug.slice(1);
1104
+ const jsonSchema = typeof z2.toJSONSchema === "function" ? z2.toJSONSchema(zodSchema, { target: "openapi-3.0" }) : zodToJsonSchema(zodSchema, { target: "openApi3" });
1105
+ openapi.components.schemas[schemaName] = jsonSchema;
1106
+ const ref = `#/components/schemas/${schemaName}`;
1107
+ openapi.paths[pathBase] = {
1108
+ get: {
1109
+ tags: [tag],
1110
+ summary: `Find ${tag}`,
1111
+ parameters: [
1112
+ { name: "limit", in: "query", schema: { type: "integer" } },
1113
+ { name: "page", in: "query", schema: { type: "integer" } }
1114
+ ],
1115
+ responses: {
1116
+ "200": {
1117
+ description: "Successful response",
1118
+ content: {
1119
+ "application/json": {
1120
+ schema: {
1121
+ type: "object",
1122
+ properties: {
1123
+ docs: { type: "array", items: { $ref: ref } },
1124
+ totalDocs: { type: "integer" }
1125
+ }
1126
+ }
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+ },
1132
+ post: {
1133
+ tags: [tag],
1134
+ summary: `Create ${tag}`,
1135
+ requestBody: {
1136
+ required: true,
1137
+ content: {
1138
+ "application/json": { schema: { $ref: ref } }
1139
+ }
1140
+ },
1141
+ responses: {
1142
+ "201": {
1143
+ description: "Created successfully",
1144
+ content: { "application/json": { schema: { $ref: ref } } }
1145
+ }
1146
+ }
1147
+ }
1148
+ };
1149
+ openapi.paths[`${pathBase}/{id}`] = {
1150
+ get: {
1151
+ tags: [tag],
1152
+ summary: `Find ${tag} by ID`,
1153
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1154
+ responses: {
1155
+ "200": {
1156
+ description: "Successful response",
1157
+ content: { "application/json": { schema: { $ref: ref } } }
1158
+ },
1159
+ "404": { description: "Not found" }
1160
+ }
1161
+ },
1162
+ patch: {
1163
+ tags: [tag],
1164
+ summary: `Update ${tag}`,
1165
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1166
+ requestBody: {
1167
+ content: { "application/json": { schema: { $ref: ref } } }
1168
+ },
1169
+ responses: {
1170
+ "200": {
1171
+ description: "Updated successfully",
1172
+ content: { "application/json": { schema: { $ref: ref } } }
1173
+ }
1174
+ }
1175
+ },
1176
+ delete: {
1177
+ tags: [tag],
1178
+ summary: `Delete ${tag}`,
1179
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1180
+ responses: {
1181
+ "200": { description: "Deleted successfully" }
1182
+ }
1183
+ }
1184
+ };
1185
+ }
1186
+ for (const global of config.globals || []) {
1187
+ const pathBase = `/api/globals/${global.slug}`;
1188
+ const tag = global.label || global.slug;
1189
+ const zodSchema = generateSchemaForCollection(global, false, true);
1190
+ const schemaName = global.slug.charAt(0).toUpperCase() + global.slug.slice(1);
1191
+ const jsonSchema = typeof z2.toJSONSchema === "function" ? z2.toJSONSchema(zodSchema, { target: "openapi-3.0" }) : zodToJsonSchema(zodSchema, { target: "openApi3" });
1192
+ openapi.components.schemas[schemaName] = jsonSchema;
1193
+ const ref = `#/components/schemas/${schemaName}`;
1194
+ openapi.paths[pathBase] = {
1195
+ get: {
1196
+ tags: [tag],
1197
+ summary: `Find ${tag}`,
1198
+ responses: {
1199
+ "200": {
1200
+ description: "Successful response",
1201
+ content: { "application/json": { schema: { $ref: ref } } }
1202
+ }
1203
+ }
1204
+ },
1205
+ post: {
1206
+ tags: [tag],
1207
+ summary: `Update ${tag}`,
1208
+ requestBody: {
1209
+ content: { "application/json": { schema: { $ref: ref } } }
1210
+ },
1211
+ responses: {
1212
+ "200": {
1213
+ description: "Updated successfully",
1214
+ content: { "application/json": { schema: { $ref: ref } } }
1215
+ }
1216
+ }
1217
+ }
1218
+ };
1219
+ }
1220
+ return openapi;
1221
+ }
1222
+
888
1223
  // src/server/routers/admin.ts
889
1224
  import { Hono } from "hono";
890
1225
 
@@ -966,26 +1301,26 @@ function createAssetsHandlers(config) {
966
1301
  try {
967
1302
  try {
968
1303
  if (config.db.name === "sqlite" || config.db.name === "d1") {
969
- const tableInfo = await config.db.unsafe(`PRAGMA table_info(_opaca_assets)`);
1304
+ const tableInfo = await config.db.unsafe(`PRAGMA table_info(_assets)`);
970
1305
  const columns = tableInfo.map((c2) => c2.name);
971
1306
  if (!columns.includes("folder"))
972
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN folder TEXT`);
1307
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN folder TEXT`);
973
1308
  if (!columns.includes("alt_text"))
974
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN alt_text TEXT`);
1309
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN alt_text TEXT`);
975
1310
  if (!columns.includes("caption"))
976
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN caption TEXT`);
1311
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN caption TEXT`);
977
1312
  } else if (config.db.name === "postgres") {
978
1313
  const checkCols = await config.db.unsafe(`
979
1314
  SELECT column_name FROM information_schema.columns
980
- WHERE table_name = '_opaca_assets' AND column_name IN ('folder', 'alt_text', 'caption')
1315
+ WHERE table_name = '_assets' AND column_name IN ('folder', 'alt_text', 'caption')
981
1316
  `);
982
1317
  const existing = checkCols.map((c2) => c2.column_name);
983
1318
  if (!existing.includes("folder"))
984
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN folder TEXT`);
1319
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN folder TEXT`);
985
1320
  if (!existing.includes("alt_text"))
986
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN "alt_text" TEXT`);
1321
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN "alt_text" TEXT`);
987
1322
  if (!existing.includes("caption"))
988
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN caption TEXT`);
1323
+ await config.db.unsafe(`ALTER TABLE _assets ADD COLUMN caption TEXT`);
989
1324
  }
990
1325
  } catch (e) {
991
1326
  console.error("Auto-patch columns failed", e);
@@ -1016,7 +1351,7 @@ function createAssetsHandlers(config) {
1016
1351
  const storedKey = keyPrefix + uploadedFileData.filename;
1017
1352
  try {
1018
1353
  const assetId = (globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2)).replace(/-/g, "");
1019
- await config.db.create("_opaca_assets", {
1354
+ await config.db.create("_assets", {
1020
1355
  id: assetId,
1021
1356
  key: storedKey,
1022
1357
  filename: fileName,
@@ -1070,7 +1405,7 @@ function createAssetsHandlers(config) {
1070
1405
  query = { or: [{ folder: null }, { folder: "" }] };
1071
1406
  }
1072
1407
  }
1073
- const result = await config.db.find("_opaca_assets", query, {
1408
+ const result = await config.db.find("_assets", query, {
1074
1409
  page,
1075
1410
  limit,
1076
1411
  sort: "created_at:desc"
@@ -1080,9 +1415,9 @@ function createAssetsHandlers(config) {
1080
1415
  let folderRows = [];
1081
1416
  if (config.db.name === "postgres") {
1082
1417
  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]);
1418
+ 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
1419
  } 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]);
1420
+ 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
1421
  }
1087
1422
  } else {
1088
1423
  if (folder === null || folder === "") {
@@ -1093,7 +1428,7 @@ function createAssetsHandlers(config) {
1093
1428
  ELSE folder
1094
1429
  END as subfolder,
1095
1430
  bucket
1096
- FROM _opaca_assets WHERE folder IS NOT NULL AND folder != '' AND (bucket = ? OR ? = 'all')
1431
+ FROM _assets WHERE folder IS NOT NULL AND folder != '' AND (bucket = ? OR ? = 'all')
1097
1432
  `, [bucket, bucket]);
1098
1433
  } else {
1099
1434
  const skipLen = folder.length + 2;
@@ -1104,7 +1439,7 @@ function createAssetsHandlers(config) {
1104
1439
  ELSE SUBSTR(folder, ?)
1105
1440
  END as subfolder,
1106
1441
  bucket
1107
- FROM _opaca_assets WHERE folder LIKE ? AND (bucket = ? OR ? = 'all')
1442
+ FROM _assets WHERE folder LIKE ? AND (bucket = ? OR ? = 'all')
1108
1443
  `, [skipLen, skipLen, skipLen, skipLen, `${folder}/%`, bucket, bucket]);
1109
1444
  }
1110
1445
  }
@@ -1156,7 +1491,7 @@ function createAssetsHandlers(config) {
1156
1491
  async serve(c) {
1157
1492
  const id = c.req.param("id");
1158
1493
  try {
1159
- const asset = await config.db.findOne("_opaca_assets", { id });
1494
+ const asset = await config.db.findOne("_assets", { id });
1160
1495
  if (!asset) {
1161
1496
  return c.json({ error: "Asset not found" }, 404);
1162
1497
  }
@@ -1191,7 +1526,7 @@ function mountCollectionRoutes(router, config, state) {
1191
1526
  }
1192
1527
  const exposedCollections = combinedCollections.filter((c) => !c.hidden);
1193
1528
  for (const collection of exposedCollections) {
1194
- const handlers = createHandlers(config, collection, () => state.auth);
1529
+ const handlers = createHandlers(collection, config);
1195
1530
  const path = `/${collection.apiPath || collection.slug}`;
1196
1531
  router.get(path, handlers.find);
1197
1532
  router.get(`${path}/versions`, handlers.findVersions);
@@ -1202,6 +1537,18 @@ function mountCollectionRoutes(router, config, state) {
1202
1537
  router.delete(`${path}/:id`, handlers.delete);
1203
1538
  }
1204
1539
  }
1540
+ function mountIncomingWebhookRoutes(router, config) {
1541
+ for (const collection of config.collections) {
1542
+ if (collection.webhooks) {
1543
+ for (const webhook of collection.webhooks) {
1544
+ if (webhook.type === "incoming") {
1545
+ const handler = createIncomingWebhookHandler(webhook, collection, config);
1546
+ router.post(webhook.path, handler);
1547
+ }
1548
+ }
1549
+ }
1550
+ }
1551
+ }
1205
1552
  function mountGlobalRoutes(router, config, state) {
1206
1553
  if (config.globals) {
1207
1554
  for (const globalConfig of config.globals) {
@@ -1249,8 +1596,8 @@ function createAssetsServingRouter(config) {
1249
1596
  const assetsServingRouter = new Hono3;
1250
1597
  if (config.storages) {
1251
1598
  const assetsHandlers = createAssetsHandlers(config);
1252
- const assetCol = getSystemCollections().find((c) => c.slug === "_opaca_assets");
1253
- const assetPath = `/${assetCol?.apiPath || assetCol?.slug || "_opaca_assets"}`;
1599
+ const assetCol = getSystemCollections().find((c) => c.slug === "_assets");
1600
+ const assetPath = `/${assetCol?.apiPath || assetCol?.slug || "_assets"}`;
1254
1601
  assetsServingRouter.get(`${assetPath}/:id/view`, assetsHandlers.serve);
1255
1602
  }
1256
1603
  return assetsServingRouter;
@@ -1267,13 +1614,17 @@ function createAuthMiddleware(getAuth) {
1267
1614
  await next();
1268
1615
  return;
1269
1616
  }
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;
1617
+ try {
1618
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
1619
+ if (session) {
1620
+ c.set("user", session.user);
1621
+ c.set("session", session.session);
1622
+ c.set("apiKey", null);
1623
+ await next();
1624
+ return;
1625
+ }
1626
+ } catch (err) {
1627
+ logger.debug("Session verification failed or threw:", err);
1277
1628
  }
1278
1629
  const authHeader = c.req.header("Authorization");
1279
1630
  if (authHeader && authHeader.startsWith("Bearer ")) {
@@ -1518,163 +1869,319 @@ function setupMiddlewares(router, config, state) {
1518
1869
  }
1519
1870
  function setupAuthMiddlewares(router, config, state) {
1520
1871
  router.use("*", createAuthMiddleware(() => state.auth));
1872
+ router.use("*", async (c, next) => {
1873
+ const user = c.get("user");
1874
+ await requestContext.run({ userId: user?.id }, async () => {
1875
+ await next();
1876
+ });
1877
+ });
1521
1878
  }
1522
1879
 
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
- }
1880
+ // src/server/graphql.ts
1881
+ import {
1882
+ GraphQLBoolean,
1883
+ GraphQLFloat,
1884
+ GraphQLInt,
1885
+ GraphQLList,
1886
+ GraphQLNonNull,
1887
+ GraphQLObjectType,
1888
+ GraphQLScalarType,
1889
+ GraphQLSchema,
1890
+ GraphQLString
1891
+ } from "graphql";
1892
+ var GraphQLJSON = new GraphQLScalarType({
1893
+ name: "JSON",
1894
+ description: "The `JSON` scalar type represents JSON values as specified by ECMA-404",
1895
+ serialize: (value) => value,
1896
+ parseValue: (value) => value,
1897
+ parseLiteral: (ast) => {
1898
+ switch (ast.kind) {
1899
+ case "StringValue":
1900
+ return JSON.parse(ast.value);
1901
+ case "ObjectValue":
1902
+ throw new Error("Not implemented");
1903
+ default:
1904
+ return null;
1543
1905
  }
1906
+ }
1907
+ });
1908
+ function getGraphQLType(field) {
1909
+ switch (field.type) {
1910
+ case "text":
1911
+ case "slug":
1912
+ case "textarea":
1913
+ case "richtext":
1914
+ case "select":
1915
+ case "radio":
1916
+ case "date":
1917
+ case "file":
1918
+ case "relationship":
1919
+ return GraphQLString;
1920
+ case "number":
1921
+ return GraphQLFloat;
1922
+ case "boolean":
1923
+ return GraphQLBoolean;
1924
+ case "json":
1925
+ case "blocks":
1926
+ case "array":
1927
+ case "group":
1928
+ case "row":
1929
+ case "collapsible":
1930
+ case "tabs":
1931
+ return GraphQLJSON;
1932
+ default:
1933
+ return GraphQLString;
1934
+ }
1935
+ }
1936
+ function buildCollectionType(collection, nameOverride) {
1937
+ const fields = {
1938
+ id: { type: GraphQLString }
1544
1939
  };
1545
- for (const collection of config.collections || []) {
1546
- const isHidden = collection.hidden === true;
1547
- if (isHidden)
1940
+ if (collection.timestamps !== false) {
1941
+ fields.createdAt = { type: GraphQLString };
1942
+ fields.updatedAt = { type: GraphQLString };
1943
+ }
1944
+ for (const field of collection.fields) {
1945
+ if (field.name) {
1946
+ fields[sanitizeGraphQLName(field.name)] = { type: getGraphQLType(field) };
1947
+ }
1948
+ }
1949
+ return new GraphQLObjectType({
1950
+ name: nameOverride || sanitizeGraphQLName(collection.slug),
1951
+ fields
1952
+ });
1953
+ }
1954
+ function buildCollectionPaginatedType(collectionType, collectionSlug) {
1955
+ return new GraphQLObjectType({
1956
+ name: `Paginated${sanitizeGraphQLName(collectionSlug)}`,
1957
+ fields: {
1958
+ docs: { type: new GraphQLList(collectionType) },
1959
+ totalDocs: { type: GraphQLInt },
1960
+ limit: { type: GraphQLInt },
1961
+ totalPages: { type: GraphQLInt },
1962
+ page: { type: GraphQLInt },
1963
+ pagingCounter: { type: GraphQLInt },
1964
+ hasPrevPage: { type: GraphQLBoolean },
1965
+ hasNextPage: { type: GraphQLBoolean },
1966
+ prevPage: { type: GraphQLInt },
1967
+ nextPage: { type: GraphQLInt },
1968
+ nextCursor: { type: GraphQLString },
1969
+ prevCursor: { type: GraphQLString }
1970
+ }
1971
+ });
1972
+ }
1973
+ function generateGraphQLSchema(config, state) {
1974
+ const queryFields = {};
1975
+ const mutationFields = {};
1976
+ for (const collection of config.collections) {
1977
+ if (collection.hidden)
1548
1978
  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
- }
1979
+ const handlers = createHandlers(collection, config);
1980
+ const collectionType = buildCollectionType(collection);
1981
+ const paginatedType = buildCollectionPaginatedType(collectionType, collection.slug);
1982
+ const sanitizedSlug = sanitizeGraphQLName(collection.slug);
1983
+ const capitalizedSlug = sanitizedSlug.charAt(0).toUpperCase() + sanitizedSlug.slice(1);
1984
+ queryFields[`find${capitalizedSlug}`] = {
1985
+ type: paginatedType,
1986
+ args: {
1987
+ limit: { type: GraphQLInt },
1988
+ page: { type: GraphQLInt },
1989
+ sort: { type: GraphQLString }
1580
1990
  },
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 } } }
1991
+ resolve: async (_, args, c) => {
1992
+ const req = {
1993
+ query: (key) => {
1994
+ if (!key)
1995
+ return args;
1996
+ return args[key] !== undefined ? String(args[key]) : undefined;
1997
+ },
1998
+ queries: () => ({})
1999
+ };
2000
+ const ctx = { ...c, req: { ...c.req, ...req } };
2001
+ ctx.req.query = req.query;
2002
+ let result;
2003
+ ctx.json = (data, status) => {
2004
+ if (status && status >= 400)
2005
+ throw new Error(data?.message || "Error");
2006
+ result = data;
2007
+ return data;
2008
+ };
2009
+ await handlers.find(ctx);
2010
+ return result;
2011
+ }
2012
+ };
2013
+ queryFields[`get${capitalizedSlug}`] = {
2014
+ type: collectionType,
2015
+ args: {
2016
+ id: { type: new GraphQLNonNull(GraphQLString) }
2017
+ },
2018
+ resolve: async (_, args, c) => {
2019
+ const req = {
2020
+ param: (key) => {
2021
+ if (key === "id")
2022
+ return args.id;
2023
+ if (!key)
2024
+ return { id: args.id };
2025
+ return args.id;
1594
2026
  }
1595
- }
2027
+ };
2028
+ const ctx = { ...c, req: { ...c.req, ...req } };
2029
+ ctx.req.param = req.param;
2030
+ let result;
2031
+ ctx.json = (data, status) => {
2032
+ if (status && status >= 400)
2033
+ throw new Error(data?.message || "Error");
2034
+ result = data;
2035
+ return data;
2036
+ };
2037
+ await handlers.findOne(ctx);
2038
+ return result?.doc || result;
1596
2039
  }
1597
2040
  };
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 } } }
2041
+ mutationFields[`create${capitalizedSlug}`] = {
2042
+ type: collectionType,
2043
+ args: {
2044
+ data: { type: new GraphQLNonNull(GraphQLJSON) }
2045
+ },
2046
+ resolve: async (_, args, c) => {
2047
+ const req = {
2048
+ json: async () => args.data
2049
+ };
2050
+ const ctx = { ...c, req: { ...c.req, ...req } };
2051
+ let result;
2052
+ ctx.json = (data, status) => {
2053
+ if (status && status >= 400)
2054
+ throw new Error(data?.message || "Error");
2055
+ result = data;
2056
+ return data;
2057
+ };
2058
+ await handlers.create(ctx);
2059
+ return result?.doc || result;
2060
+ }
2061
+ };
2062
+ mutationFields[`update${capitalizedSlug}`] = {
2063
+ type: collectionType,
2064
+ args: {
2065
+ id: { type: new GraphQLNonNull(GraphQLString) },
2066
+ data: { type: new GraphQLNonNull(GraphQLJSON) }
2067
+ },
2068
+ resolve: async (_, args, c) => {
2069
+ const req = {
2070
+ param: (key) => {
2071
+ if (key === "id")
2072
+ return args.id;
2073
+ if (!key)
2074
+ return { id: args.id };
2075
+ return args.id;
1607
2076
  },
1608
- "404": { description: "Not found" }
1609
- }
2077
+ json: async () => args.data
2078
+ };
2079
+ const ctx = { ...c, req: { ...c.req, ...req } };
2080
+ ctx.req.param = req.param;
2081
+ let result;
2082
+ ctx.json = (data, status) => {
2083
+ if (status && status >= 400)
2084
+ throw new Error(data?.message || "Error");
2085
+ result = data;
2086
+ return data;
2087
+ };
2088
+ await handlers.update(ctx);
2089
+ return result?.doc || result;
2090
+ }
2091
+ };
2092
+ mutationFields[`delete${capitalizedSlug}`] = {
2093
+ type: GraphQLBoolean,
2094
+ args: {
2095
+ id: { type: new GraphQLNonNull(GraphQLString) }
1610
2096
  },
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 } } }
2097
+ resolve: async (_, args, c) => {
2098
+ const req = {
2099
+ param: (key) => {
2100
+ if (key === "id")
2101
+ return args.id;
2102
+ if (!key)
2103
+ return { id: args.id };
2104
+ return args.id;
1622
2105
  }
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
- }
2106
+ };
2107
+ const ctx = { ...c, req: { ...c.req, ...req } };
2108
+ ctx.req.param = req.param;
2109
+ let result;
2110
+ ctx.json = (data, status) => {
2111
+ if (status && status >= 400)
2112
+ throw new Error(data?.message || "Error");
2113
+ result = data;
2114
+ return data;
2115
+ };
2116
+ await handlers.delete(ctx);
2117
+ return result;
1632
2118
  }
1633
2119
  };
1634
2120
  }
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
- }
2121
+ if (config.globals) {
2122
+ for (const globalConfig of config.globals) {
2123
+ const handlers = createGlobalHandlers(config, globalConfig, () => state.auth);
2124
+ const sanitizedGlobalSlug = sanitizeGraphQLName(globalConfig.slug);
2125
+ const capitalizedGlobalSlug = sanitizedGlobalSlug.charAt(0).toUpperCase() + sanitizedGlobalSlug.slice(1);
2126
+ const globalType = buildCollectionType(globalConfig, `Global_${sanitizedGlobalSlug}`);
2127
+ queryFields[`get${capitalizedGlobalSlug}`] = {
2128
+ type: globalType,
2129
+ resolve: async (_, __, c) => {
2130
+ const req = {};
2131
+ const ctx = { ...c, req: { ...c.req, ...req } };
2132
+ let result;
2133
+ ctx.json = (data) => {
2134
+ result = data;
2135
+ return data;
2136
+ };
2137
+ await handlers.find(ctx);
2138
+ return result;
1652
2139
  }
1653
- },
1654
- post: {
1655
- tags: [tag],
1656
- summary: `Update ${tag}`,
1657
- requestBody: {
1658
- content: { "application/json": { schema: { $ref: ref } } }
2140
+ };
2141
+ mutationFields[`update${capitalizedGlobalSlug}`] = {
2142
+ type: globalType,
2143
+ args: {
2144
+ data: { type: new GraphQLNonNull(GraphQLJSON) }
1659
2145
  },
1660
- responses: {
1661
- "200": {
1662
- description: "Updated successfully",
1663
- content: { "application/json": { schema: { $ref: ref } } }
1664
- }
2146
+ resolve: async (_, args, c) => {
2147
+ const req = {
2148
+ json: async () => args.data
2149
+ };
2150
+ const ctx = { ...c, req: { ...c.req, ...req } };
2151
+ let result;
2152
+ ctx.json = (data) => {
2153
+ result = data;
2154
+ return data;
2155
+ };
2156
+ await handlers.update(ctx);
2157
+ return result;
1665
2158
  }
1666
- }
1667
- };
2159
+ };
2160
+ }
1668
2161
  }
1669
- return openapi;
2162
+ if (Object.keys(queryFields).length === 0) {
2163
+ queryFields._empty = { type: GraphQLString, resolve: () => "Empty" };
2164
+ }
2165
+ const queryType = new GraphQLObjectType({
2166
+ name: "Query",
2167
+ fields: queryFields
2168
+ });
2169
+ const schemaConfig = { query: queryType };
2170
+ if (Object.keys(mutationFields).length > 0) {
2171
+ schemaConfig.mutation = new GraphQLObjectType({
2172
+ name: "Mutation",
2173
+ fields: mutationFields
2174
+ });
2175
+ }
2176
+ return new GraphQLSchema(schemaConfig);
1670
2177
  }
1671
2178
 
1672
2179
  // src/server/routers/plugins.ts
1673
- function mountPluginRoutes(config, settings, logger2, router) {
2180
+ function mountPluginRoutes(config, settings, logger2, router, env = {}) {
1674
2181
  if (config.plugins && Array.isArray(config.plugins)) {
1675
2182
  for (const plugin of config.plugins) {
1676
2183
  const pluginSettings = settings[plugin.name] || {};
1677
- const pluginContext = { config, logger: logger2, settings: pluginSettings };
2184
+ const pluginContext = { config, logger: logger2, settings: pluginSettings, env };
1678
2185
  if (plugin.onRequest) {
1679
2186
  router.use("*", async (c, next) => {
1680
2187
  const result = await plugin.onRequest(c);
@@ -1694,21 +2201,22 @@ function mountPluginRoutes(config, settings, logger2, router) {
1694
2201
  }
1695
2202
  }
1696
2203
  }
1697
- function firePluginInitComplete(config, settings, logger2) {
2204
+ function firePluginInitComplete(config, settings, logger2, env = {}) {
1698
2205
  if (config.plugins && Array.isArray(config.plugins)) {
1699
2206
  for (const plugin of config.plugins) {
1700
2207
  if (plugin.onInitComplete) {
1701
2208
  const pluginSettings = settings[plugin.name] || {};
1702
- plugin.onInitComplete({ config, logger: logger2, settings: pluginSettings });
2209
+ plugin.onInitComplete({ config, logger: logger2, settings: pluginSettings, env });
1703
2210
  }
1704
2211
  }
1705
2212
  }
1706
2213
  }
1707
2214
 
1708
2215
  // src/server/router.ts
1709
- function createAPIRouter(config, settings = {}) {
2216
+ function createAPIRouter(config, settings = {}, env = {}) {
1710
2217
  const state = { auth: undefined, migrated: false };
1711
- const router = new Hono4().basePath("/api");
2218
+ const app = new Hono4;
2219
+ const router = new Hono4;
1712
2220
  setupMiddlewares(router, config, state);
1713
2221
  setupAuthMiddlewares(router, config, state);
1714
2222
  router.get("/", (c) => {
@@ -1718,9 +2226,22 @@ function createAPIRouter(config, settings = {}) {
1718
2226
  router.route("/__admin", createAdminRouter(config, settings, state));
1719
2227
  router.route("/__system", createSystemRouter(config));
1720
2228
  router.route("/", createAssetsServingRouter(config));
1721
- mountPluginRoutes(config, settings, logger, router);
1722
- mountCollectionRoutes(router, config, state);
1723
- mountGlobalRoutes(router, config, state);
2229
+ mountPluginRoutes(config, settings, logger, router, env);
2230
+ const isRestEnabled = config.api?.rest?.enabled !== false;
2231
+ const isGraphQLEnabled = config.api?.graphql?.enabled === true;
2232
+ if (isRestEnabled) {
2233
+ mountCollectionRoutes(router, config, state);
2234
+ mountGlobalRoutes(router, config, state);
2235
+ mountIncomingWebhookRoutes(router, config);
2236
+ }
2237
+ if (isGraphQLEnabled) {
2238
+ const graphqlPath = config.api?.graphql?.path || "/graphql";
2239
+ const schema = generateGraphQLSchema(config, state);
2240
+ router.use(graphqlPath, graphqlServer({
2241
+ schema,
2242
+ graphiql: config.api?.graphql?.graphiql ?? false
2243
+ }));
2244
+ }
1724
2245
  if (config.api?.openAPI?.enabled) {
1725
2246
  router.get("/open-api.json", (c) => {
1726
2247
  const schema = generateOpenAPISchema(config);
@@ -1746,8 +2267,14 @@ function createAPIRouter(config, settings = {}) {
1746
2267
  })(c, next);
1747
2268
  });
1748
2269
  }
1749
- firePluginInitComplete(config, settings, logger);
1750
- return router;
2270
+ firePluginInitComplete(config, settings, logger, env);
2271
+ app.route("/api", router);
2272
+ app.get("/", (c) => c.redirect("/api"));
2273
+ app.notFound((c) => {
2274
+ console.error(`[OpacaRouter] 404 Not Found: ${c.req.method} ${c.req.url}`);
2275
+ return c.json({ message: "Not Found", path: c.req.path }, 404);
2276
+ });
2277
+ return app;
1751
2278
  }
1752
2279
 
1753
- export { createAdminHandlers, hydrateDoc, parsePopulate, populateDoc, createHandlers, createGlobalHandlers, createAPIRouter };
2280
+ export { createAdminHandlers, hydrateDoc, parsePopulate, populateDoc, createHandlers, createIncomingWebhookHandler, createGlobalHandlers, createAPIRouter };