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.
- 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 +16 -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-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/{chunk-2es275xs.js → chunk-z3ffn2b7.js} +834 -307
- 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 +11 -8
- 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 {
|
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
|
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 = '
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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(
|
|
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 === "
|
|
1253
|
-
const assetPath = `/${assetCol?.apiPath || assetCol?.slug || "
|
|
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
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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/
|
|
1524
|
-
import {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
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
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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
|
-
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1723
|
-
|
|
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
|
-
|
|
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 };
|