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.
- package/README.md +31 -22
- package/dist/admin/auth-client.d.ts +39 -39
- package/dist/admin/index.d.ts +2 -2
- package/dist/admin/index.js +15 -10520
- package/dist/admin/plugin-client.d.ts +65 -0
- package/dist/admin/react.d.ts +2 -2
- package/dist/admin/react.js +34 -4
- package/dist/admin/stores/ui.d.ts +19 -4
- package/dist/admin/ui/components/PluginSettingsForm.d.ts +2 -2
- package/dist/admin/ui/components/custom-alert.d.ts +7 -0
- package/dist/admin/ui/components/{DetailSheet.d.ts → detail-sheet.d.ts} +1 -2
- package/dist/admin/ui/components/fields/FieldLabel.d.ts +1 -1
- package/dist/admin/ui/components/fields/RelationshipField.d.ts +1 -1
- package/dist/admin/ui/components/media/AssetManagerModal.d.ts +2 -2
- package/dist/admin/ui/components/plugin-iframe.d.ts +7 -0
- package/dist/admin/ui/components/ui/accordion.d.ts +17 -7
- package/dist/admin/ui/components/ui/alert-dialog.d.ts +16 -12
- package/dist/admin/ui/components/ui/button.d.ts +11 -7
- package/dist/admin/ui/components/ui/relationship.d.ts +1 -1
- package/dist/admin/ui/components/ui/sheet.d.ts +14 -27
- package/dist/admin/ui/components/ui/tooltip.d.ts +7 -0
- package/dist/admin/ui/components/versions-sheet.d.ts +4 -5
- package/dist/admin/ui/views/collection-list-view.d.ts +1 -1
- package/dist/admin/ui/views/dashboard-view.d.ts +1 -1
- package/dist/admin/ui/views/media-registry-view.d.ts +3 -3
- package/dist/admin/ui/views/settings-view.d.ts +2 -2
- package/dist/admin/vue.js +27 -4
- package/dist/admin/webcomponent.js +66 -16
- package/dist/admin.css +1 -1
- package/dist/auth/index.d.ts +43 -43
- package/dist/{chunk-7y1nbmw6.js → chunk-1bd7fz7n.js} +32 -2
- package/dist/chunk-1qm0m8r8.js +413 -0
- package/dist/chunk-2k3ysje3.js +31 -0
- package/dist/{chunk-jdfw4v3r.js → chunk-3j9zjfmn.js} +95 -30
- package/dist/{chunk-byq8g0rd.js → chunk-48ywpd0a.js} +16 -22
- package/dist/{chunk-tfnaf41w.js → chunk-5422w4eq.js} +41 -25
- package/dist/chunk-56n342hs.js +95 -0
- package/dist/chunk-5b8r0v8c.js +47 -0
- package/dist/chunk-63yg00vx.js +263 -0
- package/dist/{chunk-8sqjbsgt.js → chunk-6bywt602.js} +26 -1
- package/dist/{chunk-v9z61v3g.js → chunk-6qs0g65f.js} +43 -3
- package/dist/chunk-7rr5p01g.js +581 -0
- package/dist/{chunk-2es275xs.js → chunk-941zxavt.js} +867 -322
- package/dist/{chunk-51z3x7kq.js → chunk-a3qae86h.js} +1 -1
- package/dist/{chunk-3rdhbedb.js → chunk-adq2b75c.js} +2 -2
- package/dist/chunk-d0tb1xjw.js +93 -0
- package/dist/chunk-d7cgd6vn.js +318 -0
- package/dist/{chunk-6d1vdfwa.js → chunk-e0g6gn7n.js} +54 -75
- package/dist/chunk-ec4jhybj.js +1137 -0
- package/dist/chunk-fatyf6f7.js +221 -0
- package/dist/{chunk-526a3gqx.js → chunk-fnsf1dfm.js} +1 -1
- package/dist/chunk-g9bxb6h0.js +205 -0
- package/dist/chunk-gyaf5kgf.js +10 -0
- package/dist/{chunk-9kxpbcb1.js → chunk-h6dhexzr.js} +16 -7
- package/dist/{chunk-dykn5hr6.js → chunk-j8js1y0h.js} +31 -74
- package/dist/{chunk-t0zg026p.js → chunk-jq1drsen.js} +12 -1
- package/dist/{chunk-b3kr8w41.js → chunk-m24yqkeq.js} +38 -26
- package/dist/chunk-m5ems3hh.js +410 -0
- package/dist/{chunk-8scgdznr.js → chunk-m83ybzf8.js} +15 -18
- package/dist/chunk-majsbncm.js +98 -0
- package/dist/chunk-mp2gt9yh.js +237 -0
- package/dist/chunk-n1twhqmf.js +54 -0
- package/dist/{chunk-bygjkgrx.js → chunk-naqcqj8n.js} +57 -80
- package/dist/chunk-q5sb5dcr.js +15 -0
- package/dist/{chunk-06ks4ggh.js → chunk-qhdsjek6.js} +49 -89
- package/dist/{chunk-n133qpsm.js → chunk-qsh2nqz3.js} +50 -81
- package/dist/chunk-r0ms5tk1.js +76 -0
- package/dist/chunk-rwqwsanx.js +75 -0
- package/dist/chunk-sqsfk9p4.js +700 -0
- package/dist/{chunk-5gvbp2qa.js → chunk-x7bnzswh.js} +25 -18
- package/dist/cli/commands/dev.d.ts +8 -0
- package/dist/cli/commands/doctor.d.ts +8 -0
- package/dist/cli/commands/generate.d.ts +26 -0
- package/dist/cli/commands/init.d.ts +13 -1
- package/dist/cli/commands/migrate.d.ts +33 -0
- package/dist/cli/commands/plugin.d.ts +13 -0
- package/dist/cli/commands/seed.d.ts +21 -0
- package/dist/cli/{commands/migrate-commands.d.ts → core/migrations/migrate-logic.d.ts} +2 -2
- package/dist/cli/core/migrations/schema-diff-engine.d.ts +12 -0
- package/dist/cli/core/migrations/schema-diff.d.ts +11 -0
- package/dist/cli/{seeding.d.ts → core/seeding/auto-seed.d.ts} +7 -4
- package/dist/cli/core/seeding/seed-logic.d.ts +2 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +6 -170
- package/dist/client/RichText.d.ts +5 -0
- package/dist/client/rich-text-utils.d.ts +5 -0
- package/dist/client.js +3 -2
- package/dist/config.d.ts +3 -3
- package/dist/db/better-sqlite.d.ts +2 -3
- package/dist/db/better-sqlite.js +6 -5
- package/dist/db/bun-sqlite.d.ts +2 -3
- package/dist/db/bun-sqlite.js +6 -5
- package/dist/db/d1.d.ts +13 -7
- package/dist/db/d1.js +6 -5
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +10 -12
- package/dist/db/kysely/factory.d.ts +29 -0
- package/dist/db/kysely/plugins/audit-logging.d.ts +48 -0
- package/dist/db/kysely/plugins/auto-timestamps.d.ts +38 -0
- package/dist/db/kysely/plugins/cursor-pagination.d.ts +42 -0
- package/dist/db/kysely/plugins/deadlock-handler.d.ts +47 -0
- package/dist/db/kysely/plugins/draft-swapper.d.ts +33 -0
- package/dist/db/kysely/plugins/field-masking.d.ts +45 -0
- package/dist/db/kysely/plugins/fts-normalizer.d.ts +38 -0
- package/dist/db/kysely/plugins/i18n-fallback.d.ts +48 -0
- package/dist/db/kysely/plugins/id-generation.d.ts +42 -0
- package/dist/db/kysely/plugins/index.d.ts +16 -0
- package/dist/db/kysely/plugins/json-flattener.d.ts +38 -0
- package/dist/db/kysely/plugins/relationship-preloading.d.ts +39 -0
- package/dist/db/kysely/plugins/slug-generation.d.ts +37 -0
- package/dist/db/kysely/plugins/soft-delete.d.ts +42 -0
- package/dist/db/kysely/plugins/tree-resolver.d.ts +39 -0
- package/dist/db/kysely/plugins/virtual-field-resolver.d.ts +54 -0
- package/dist/db/kysely/plugins/zod-coercion.d.ts +34 -0
- package/dist/db/kysely/snapshot/snapshot-manager.d.ts +18 -0
- package/dist/db/postgres.d.ts +2 -2
- package/dist/db/postgres.js +6 -5
- package/dist/db/sqlite.d.ts +2 -3
- package/dist/db/sqlite.js +6 -5
- package/dist/index.d.ts +3 -0
- package/dist/index.js +161 -7
- package/dist/runtimes/bun.js +9 -6
- package/dist/runtimes/cloudflare-workers.d.ts +3 -1
- package/dist/runtimes/cloudflare-workers.js +36 -7
- package/dist/runtimes/next.js +8 -5
- package/dist/runtimes/node.js +9 -6
- package/dist/schema/collection.d.ts +116 -70
- package/dist/schema/compiler.d.ts +6 -0
- package/dist/schema/global.d.ts +38 -71
- package/dist/schema/index.d.ts +5 -4
- package/dist/schema/index.js +35 -550
- package/dist/schema/zod.d.ts +564 -0
- package/dist/server/admin-router.d.ts +1 -1
- package/dist/server/collection-router.d.ts +1 -1
- package/dist/server/graphql.d.ts +6 -0
- package/dist/server/handlers.d.ts +25 -7
- package/dist/server/middlewares/auth.d.ts +1 -1
- package/dist/server/plugins-loader.d.ts +1 -1
- package/dist/server/router.d.ts +2 -2
- package/dist/server/routers/admin.d.ts +1 -1
- package/dist/server/routers/auth.d.ts +1 -1
- package/dist/server/routers/collections.d.ts +4 -1
- package/dist/server/routers/plugins.d.ts +2 -2
- package/dist/server/setup-middlewares.d.ts +1 -1
- package/dist/server/system-router.d.ts +1 -1
- package/dist/server.js +11 -6
- package/dist/storage/adapters/cloudflare-r2.d.ts +11 -2
- package/dist/storage/index.js +5 -5
- package/dist/types.d.ts +253 -42
- package/dist/utils/context.d.ts +14 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/string.d.ts +10 -0
- package/dist/utils/webhooks-engine.d.ts +24 -0
- package/dist/validation.d.ts +67 -1
- package/dist/validator.d.ts +1 -0
- package/package.json +50 -11
- package/src/cli/index.ts +117 -0
- package/dist/chunk-6qq3ne6b.js +0 -288
- package/dist/chunk-6v1fw7q7.js +0 -126
- package/dist/chunk-7a9kn0np.js +0 -116
- package/dist/chunk-bexcv7xe.js +0 -36
- package/dist/chunk-d3ffeqp9.js +0 -87
- package/dist/chunk-fj19qccp.js +0 -78
- package/dist/chunk-g1jb60xd.js +0 -17
- package/dist/chunk-j53pz21t.js +0 -20
- package/dist/chunk-mkn49zmy.js +0 -102
- package/dist/chunk-r39em4yj.js +0 -29
- package/dist/chunk-rsf0tpy1.js +0 -8
- package/dist/chunk-srsac177.js +0 -85
- package/dist/chunk-twpvxfce.js +0 -64
- package/dist/chunk-ywm4t2gm.js +0 -19
- package/dist/cli/commands/plugin-sync.d.ts +0 -1
- package/dist/cli/commands/seed-command.d.ts +0 -2
- package/dist/plugins/ui-bridge.d.ts +0 -12
- package/dist/schema/fields/base.d.ts +0 -84
- package/dist/schema/fields/index.d.ts +0 -147
- package/dist/schema/infer.d.ts +0 -55
- /package/dist/admin/ui/components/{ColumnVisibilityToggle.d.ts → column-visibility-toggle.d.ts} +0 -0
- /package/dist/admin/ui/components/{DataDetailView.d.ts → data-detail-view.d.ts} +0 -0
- /package/dist/cli/{d1-mock.d.ts → core/mocks/d1-mock.d.ts} +0 -0
- /package/dist/cli/{r2-mock.d.ts → core/mocks/r2-mock.d.ts} +0 -0
- /package/dist/cli/{commands → core/plugins}/plugin-build.d.ts +0 -0
- /package/dist/cli/{commands → core/plugins}/plugin-init.d.ts +0 -0
- /package/dist/cli/{commands → core/types}/generate-types.d.ts +0 -0
- /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-
|
|
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-
|
|
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-
|
|
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 === "
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
205
|
-
schema =
|
|
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 "
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
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 ([
|
|
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
|
|
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("
|
|
794
|
+
const afterCreateWebhooks = collection.webhooks.filter((w) => w.type === "outgoing" && w.events.includes("after:create"));
|
|
680
795
|
for (const webhook of afterCreateWebhooks) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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("
|
|
861
|
+
const afterUpdateWebhooks = collection.webhooks.filter((w) => w.type === "outgoing" && w.events.includes("after:update"));
|
|
740
862
|
for (const webhook of afterUpdateWebhooks) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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("
|
|
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("
|
|
905
|
+
const afterDeleteWebhooks = collection.webhooks.filter((w) => w.type === "outgoing" && w.events.includes("after:delete"));
|
|
777
906
|
for (const webhook of afterDeleteWebhooks) {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
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
|
|
813
|
-
if (
|
|
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
|
|
816
|
-
const
|
|
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.
|
|
820
|
-
delete data.
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 = '
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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(
|
|
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 === "
|
|
1253
|
-
const assetPath = `/${assetCol?.apiPath || assetCol?.slug || "
|
|
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
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
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/
|
|
1524
|
-
import {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
|
1550
|
-
const
|
|
1551
|
-
const
|
|
1552
|
-
const
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
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
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
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
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
-
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1723
|
-
|
|
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
|
-
|
|
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 };
|