opacacms 0.1.21 → 0.2.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 +792 -50
- package/dist/admin/auth-client.d.ts +39 -39
- package/dist/admin/index.js +2360 -1392
- package/dist/admin/react.d.ts +1 -1
- package/dist/admin/react.js +8 -0
- package/dist/admin/router.d.ts +1 -0
- package/dist/admin/stores/ui.d.ts +10 -0
- package/dist/admin/ui/admin-layout.d.ts +4 -4
- package/dist/admin/ui/components/DataDetailView.d.ts +1 -1
- package/dist/admin/ui/components/DetailSheet.d.ts +19 -0
- package/dist/admin/ui/components/PluginSettingsForm.d.ts +11 -0
- package/dist/admin/ui/components/fields/BooleanField.d.ts +2 -1
- package/dist/admin/ui/components/fields/DateField.d.ts +1 -1
- package/dist/admin/ui/components/fields/FieldLabel.d.ts +11 -0
- package/dist/admin/ui/components/fields/FileField.d.ts +1 -1
- package/dist/admin/ui/components/fields/NumberField.d.ts +1 -1
- package/dist/admin/ui/components/fields/RadioField.d.ts +1 -1
- package/dist/admin/ui/components/fields/RelationshipField.d.ts +3 -1
- package/dist/admin/ui/components/fields/SelectField.d.ts +1 -1
- package/dist/admin/ui/components/fields/TextAreaField.d.ts +1 -1
- package/dist/admin/ui/components/fields/TextField.d.ts +1 -1
- package/dist/admin/ui/components/fields/VirtualField.d.ts +1 -0
- package/dist/admin/ui/components/fields/index.d.ts +16 -16
- package/dist/admin/ui/components/fields/richtext-editor/index.d.ts +1 -1
- package/dist/admin/ui/components/media/AssetManagerModal.d.ts +1 -1
- package/dist/admin/ui/components/toast.d.ts +1 -1
- package/dist/admin/ui/components/ui/accordion.d.ts +1 -1
- package/dist/admin/ui/components/ui/button.d.ts +1 -1
- package/dist/admin/ui/components/ui/collapsible.d.ts +1 -1
- package/dist/admin/ui/components/ui/dialog.d.ts +1 -1
- package/dist/admin/ui/components/ui/group.d.ts +1 -1
- package/dist/admin/ui/components/ui/index.d.ts +17 -17
- package/dist/admin/ui/components/ui/input.d.ts +1 -1
- package/dist/admin/ui/components/ui/label.d.ts +1 -1
- package/dist/admin/ui/components/ui/radio-group.d.ts +1 -1
- package/dist/admin/ui/components/ui/relationship.d.ts +4 -4
- package/dist/admin/ui/components/ui/select.d.ts +1 -1
- package/dist/admin/ui/components/ui/sheet.d.ts +1 -1
- package/dist/admin/ui/components/ui/tabs.d.ts +1 -1
- package/dist/admin/ui/components/versions-sheet.d.ts +11 -0
- package/dist/admin/ui/views/media-registry-view.d.ts +1 -1
- package/dist/admin/ui/views/settings-view.d.ts +2 -2
- package/dist/admin/vue.js +8 -0
- package/dist/admin/webcomponent.js +2 -2
- package/dist/admin.css +1 -1
- package/dist/auth/index.d.ts +101 -41
- package/dist/{chunk-0sdceeys.js → chunk-0bq155dy.js} +86 -6
- package/dist/{chunk-59sg3pw9.js → chunk-0gtxnxmd.js} +90 -7
- package/dist/{chunk-v521d72w.js → chunk-3rdhbedb.js} +1 -1
- package/dist/chunk-51z3x7kq.js +20 -0
- package/dist/{chunk-7fyepksb.js → chunk-526a3gqx.js} +1 -1
- package/dist/{chunk-wmvjvn7b.js → chunk-6qq3ne6b.js} +39 -1
- package/dist/{chunk-0am1m47g.js → chunk-6v1fw7q7.js} +5 -5
- package/dist/{chunk-t9v845m2.js → chunk-7y1nbmw6.js} +34 -3
- package/dist/chunk-8scgdznr.js +44 -0
- package/dist/{chunk-mycmsjd9.js → chunk-b3kr8w41.js} +57 -6
- package/dist/chunk-bexcv7xe.js +36 -0
- package/dist/{chunk-16vgcf3k.js → chunk-byq8g0rd.js} +1 -1
- package/dist/{chunk-fqastxq9.js → chunk-d1asgtke.js} +86 -6
- package/dist/{chunk-cpw2y3pn.js → chunk-dykn5hr6.js} +7 -7
- package/dist/{chunk-61kwqve4.js → chunk-esrg9qj0.js} +90 -9
- package/dist/chunk-fj19qccp.js +78 -0
- package/dist/{chunk-ekxkvqjm.js → chunk-gmee4mdc.js} +90 -9
- package/dist/{chunk-xa7rjsn2.js → chunk-j53pz21t.js} +2 -2
- package/dist/{chunk-xrfhhz85.js → chunk-kc4jfnv7.js} +480 -85
- package/dist/chunk-mkn49zmy.js +102 -0
- package/dist/{chunk-n1xraw7j.js → chunk-qb6ztvw9.js} +1 -1
- package/dist/{chunk-2kyhqvhc.js → chunk-qxt9vge8.js} +1 -1
- package/dist/chunk-r39em4yj.js +29 -0
- package/dist/chunk-rqyjjqgy.js +91 -0
- package/dist/chunk-rsf0tpy1.js +8 -0
- package/dist/chunk-swtcpvhf.js +2442 -0
- package/dist/chunk-t0zg026p.js +71 -0
- package/dist/chunk-twpvxfce.js +64 -0
- package/dist/{chunk-ybbbqj63.js → chunk-v9z61v3g.js} +15 -0
- package/dist/{chunk-jwjk85ze.js → chunk-ywm4t2gm.js} +6 -2
- package/dist/cli/commands/plugin-build.d.ts +1 -0
- package/dist/cli/commands/plugin-init.d.ts +1 -0
- package/dist/cli/commands/plugin-sync.d.ts +1 -0
- package/dist/cli/index.js +24 -6
- package/dist/config-utils.d.ts +1 -1
- package/dist/config.d.ts +21 -4
- package/dist/db/better-sqlite.d.ts +1 -1
- package/dist/db/better-sqlite.js +5 -5
- package/dist/db/bun-sqlite.d.ts +1 -1
- package/dist/db/bun-sqlite.js +5 -5
- package/dist/db/d1.d.ts +1 -1
- package/dist/db/d1.js +5 -5
- package/dist/db/index.js +9 -9
- package/dist/db/postgres.d.ts +1 -1
- package/dist/db/postgres.js +5 -5
- package/dist/db/sqlite.d.ts +1 -1
- package/dist/db/sqlite.js +5 -5
- package/dist/index.js +4 -3
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/ui-bridge.d.ts +12 -0
- package/dist/plugins/utils.d.ts +5 -0
- package/dist/runtimes/bun.js +13 -7
- package/dist/runtimes/cloudflare-workers.js +5 -5
- package/dist/runtimes/next.js +5 -5
- package/dist/runtimes/node.js +13 -7
- package/dist/schema/collection.d.ts +9 -26
- package/dist/schema/fields/base.d.ts +3 -2
- package/dist/schema/fields/index.d.ts +12 -0
- package/dist/schema/fields/validation.test.d.ts +1 -0
- package/dist/schema/global.d.ts +10 -7
- package/dist/schema/index.js +22 -6
- package/dist/server/admin-router.d.ts +2 -2
- package/dist/server/admin.d.ts +2 -1
- package/dist/server/collection-router.d.ts +1 -1
- package/dist/server/handlers.d.ts +10 -0
- package/dist/server/middlewares/admin.d.ts +2 -2
- package/dist/server/middlewares/auth.d.ts +1 -1
- package/dist/server/middlewares/context.d.ts +2 -0
- package/dist/server/middlewares/rate-limit.d.ts +1 -1
- package/dist/server/openapi.d.ts +2 -0
- package/dist/server/plugins-loader.d.ts +6 -0
- package/dist/server/router.d.ts +3 -3
- package/dist/server/routers/admin.d.ts +2 -2
- package/dist/server/routers/auth.d.ts +1 -1
- package/dist/server/routers/collections.d.ts +1 -1
- package/dist/server/routers/plugins.d.ts +18 -0
- package/dist/server/setup-middlewares.d.ts +2 -2
- package/dist/server/system-router.d.ts +1 -1
- package/dist/server.js +11 -7
- package/dist/storage/adapters/local.d.ts +1 -1
- package/dist/storage/adapters/s3.d.ts +1 -1
- package/dist/types.d.ts +222 -15
- package/dist/utils/logger.d.ts +13 -35
- package/dist/validation.d.ts +40 -0
- package/dist/validator.d.ts +1 -1
- package/package.json +21 -7
- package/dist/admin/ui/components/DataDetailSheet.d.ts +0 -13
- package/dist/admin/ui/components/ui/relationship-detail-sheet.d.ts +0 -9
- package/dist/chunk-62ev8gnc.js +0 -41
- package/dist/chunk-j4d50hrx.js +0 -20
- package/dist/chunk-nb7ctdg8.js +0 -311
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createAuth,
|
|
3
3
|
sanitizeConfig
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import {
|
|
6
|
-
logger
|
|
7
|
-
} from "./chunk-62ev8gnc.js";
|
|
4
|
+
} from "./chunk-b3kr8w41.js";
|
|
8
5
|
import {
|
|
9
6
|
toSnakeCase
|
|
10
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-qxt9vge8.js";
|
|
11
8
|
import {
|
|
12
9
|
exports_system_schema,
|
|
13
10
|
getSystemCollections,
|
|
14
11
|
init_system_schema
|
|
15
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-v9z61v3g.js";
|
|
13
|
+
import {
|
|
14
|
+
OpacaLogger,
|
|
15
|
+
logger
|
|
16
|
+
} from "./chunk-t0zg026p.js";
|
|
16
17
|
import {
|
|
17
18
|
__require,
|
|
18
19
|
__toCommonJS
|
|
19
20
|
} from "./chunk-8sqjbsgt.js";
|
|
20
21
|
|
|
21
22
|
// src/server/admin.ts
|
|
22
|
-
function createAdminHandlers(config, getAuth) {
|
|
23
|
+
function createAdminHandlers(config, settings, getAuth) {
|
|
23
24
|
const getMetadata = (c) => {
|
|
24
|
-
return c.json(sanitizeConfig(config));
|
|
25
|
+
return c.json(sanitizeConfig(config, settings));
|
|
25
26
|
};
|
|
26
27
|
const getCollections = (c) => {
|
|
27
28
|
const collections = [...config.collections];
|
|
@@ -101,19 +102,27 @@ function createAdminHandlers(config, getAuth) {
|
|
|
101
102
|
return c.json({ message, detail: err }, 400);
|
|
102
103
|
}
|
|
103
104
|
};
|
|
105
|
+
const getPluginSettings = async (c) => {
|
|
106
|
+
const name = c.req.param("name");
|
|
107
|
+
if (!name)
|
|
108
|
+
return c.json({ error: "Plugin name is required" }, 400);
|
|
109
|
+
const pluginSettings = settings[name] || {};
|
|
110
|
+
return c.json(pluginSettings);
|
|
111
|
+
};
|
|
104
112
|
return {
|
|
105
113
|
getMetadata,
|
|
106
114
|
getCollections,
|
|
107
115
|
getConfig,
|
|
108
116
|
getSetupStatus,
|
|
109
|
-
createApiKey
|
|
117
|
+
createApiKey,
|
|
118
|
+
getPluginSettings
|
|
110
119
|
};
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
// src/validator.ts
|
|
114
123
|
import { z } from "zod";
|
|
115
|
-
function generateSchemaForCollection(collection, isUpdate = false) {
|
|
116
|
-
const shape = mapFieldsToShape(collection.fields, isUpdate);
|
|
124
|
+
function generateSchemaForCollection(collection, isUpdate = false, forDocs = false) {
|
|
125
|
+
const shape = mapFieldsToShape(collection.fields, isUpdate, forDocs);
|
|
117
126
|
if (collection.versions) {
|
|
118
127
|
shape._status = z.enum(["draft", "published"]).optional().nullable();
|
|
119
128
|
}
|
|
@@ -122,46 +131,49 @@ function generateSchemaForCollection(collection, isUpdate = false) {
|
|
|
122
131
|
const config = typeof ts === "object" ? ts : {};
|
|
123
132
|
const createdField = config.createdAt || "createdAt";
|
|
124
133
|
const updatedField = config.updatedAt || "updatedAt";
|
|
125
|
-
|
|
126
|
-
shape[
|
|
134
|
+
const dateSchema = forDocs ? z.string() : z.union([z.string(), z.date()]);
|
|
135
|
+
shape[createdField] = dateSchema.optional().nullable();
|
|
136
|
+
shape[updatedField] = dateSchema.optional().nullable();
|
|
127
137
|
}
|
|
128
138
|
return z.object(shape);
|
|
129
139
|
}
|
|
130
|
-
function mapFieldsToShape(fields, isUpdate = false) {
|
|
140
|
+
function mapFieldsToShape(fields, isUpdate = false, forDocs = false) {
|
|
131
141
|
const shape = {};
|
|
132
142
|
for (const field of fields) {
|
|
143
|
+
if (field.type === "virtual" || field.type === "ui")
|
|
144
|
+
continue;
|
|
133
145
|
if (!field.name) {
|
|
134
146
|
if (field.type === "tabs" && field.tabs) {
|
|
135
147
|
for (const tab of field.tabs) {
|
|
136
|
-
Object.assign(shape, mapFieldsToShape(tab.fields, isUpdate));
|
|
148
|
+
Object.assign(shape, mapFieldsToShape(tab.fields, isUpdate, forDocs));
|
|
137
149
|
}
|
|
138
150
|
} else if (field.type === "group" && field.fields) {
|
|
139
|
-
Object.assign(shape, mapFieldsToShape(field.fields, isUpdate));
|
|
151
|
+
Object.assign(shape, mapFieldsToShape(field.fields, isUpdate, forDocs));
|
|
140
152
|
} else if (field.type === "row" && field.fields) {
|
|
141
|
-
Object.assign(shape, mapFieldsToShape(field.fields, isUpdate));
|
|
153
|
+
Object.assign(shape, mapFieldsToShape(field.fields, isUpdate, forDocs));
|
|
142
154
|
} else if (field.type === "collapsible" && field.fields) {
|
|
143
|
-
Object.assign(shape, mapFieldsToShape(field.fields, isUpdate));
|
|
155
|
+
Object.assign(shape, mapFieldsToShape(field.fields, isUpdate, forDocs));
|
|
144
156
|
}
|
|
145
157
|
continue;
|
|
146
158
|
}
|
|
147
159
|
const fieldName = field.name;
|
|
148
160
|
let schema;
|
|
149
161
|
if (field.type === "group" && field.fields) {
|
|
150
|
-
schema = z.object(mapFieldsToShape(field.fields, isUpdate));
|
|
162
|
+
schema = z.object(mapFieldsToShape(field.fields, isUpdate, forDocs));
|
|
151
163
|
} else if (field.type === "blocks" && field.blocks) {
|
|
152
164
|
const blockSchemas = field.blocks.map((block) => z.object({
|
|
153
165
|
blockType: z.literal(block.slug),
|
|
154
|
-
...mapFieldsToShape(block.fields, isUpdate)
|
|
166
|
+
...mapFieldsToShape(block.fields, isUpdate, forDocs)
|
|
155
167
|
}));
|
|
156
168
|
schema = z.array(z.union(blockSchemas));
|
|
157
169
|
} else if (field.type === "row" || field.type === "collapsible") {
|
|
158
170
|
if (field.fields) {
|
|
159
|
-
schema = z.object(mapFieldsToShape(field.fields, isUpdate));
|
|
171
|
+
schema = z.object(mapFieldsToShape(field.fields, isUpdate, forDocs));
|
|
160
172
|
} else {
|
|
161
173
|
schema = z.any();
|
|
162
174
|
}
|
|
163
175
|
} else {
|
|
164
|
-
schema = mapFieldToZod(field);
|
|
176
|
+
schema = mapFieldToZod(field, forDocs);
|
|
165
177
|
}
|
|
166
178
|
if (field.localized) {
|
|
167
179
|
schema = z.union([z.record(z.string(), schema), schema]);
|
|
@@ -177,23 +189,36 @@ function mapFieldsToShape(fields, isUpdate = false) {
|
|
|
177
189
|
schema = schema.default(field.defaultValue);
|
|
178
190
|
}
|
|
179
191
|
if (field.validate) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
if (typeof field.validate === "function") {
|
|
193
|
+
schema = schema.superRefine((val, ctx) => {
|
|
194
|
+
if (val === undefined || val === null)
|
|
195
|
+
return;
|
|
196
|
+
const result = field.validate(val);
|
|
197
|
+
if (result !== true) {
|
|
198
|
+
ctx.addIssue({
|
|
199
|
+
code: z.ZodIssueCode.custom,
|
|
200
|
+
message: typeof result === "string" ? result : "Invalid field"
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
} else if (typeof field.validate === "object" && field.validate !== null && "safeParse" in field.validate) {
|
|
205
|
+
schema = schema.superRefine((val, ctx) => {
|
|
206
|
+
if (val === undefined || val === null)
|
|
207
|
+
return;
|
|
208
|
+
const result = field.validate.safeParse(val);
|
|
209
|
+
if (!result.success) {
|
|
210
|
+
for (const issue of result.error.issues) {
|
|
211
|
+
ctx.addIssue(issue);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
191
216
|
}
|
|
192
217
|
shape[fieldName] = schema;
|
|
193
218
|
}
|
|
194
219
|
return shape;
|
|
195
220
|
}
|
|
196
|
-
function mapFieldToZod(field) {
|
|
221
|
+
function mapFieldToZod(field, forDocs = false) {
|
|
197
222
|
switch (field.type) {
|
|
198
223
|
case "text":
|
|
199
224
|
case "slug":
|
|
@@ -202,7 +227,7 @@ function mapFieldToZod(field) {
|
|
|
202
227
|
return z.string();
|
|
203
228
|
case "relationship": {
|
|
204
229
|
const isHasMany = "hasMany" in field && field.hasMany;
|
|
205
|
-
const base = z.union([z.string(), z.number(), z.undefined(), z.null()]);
|
|
230
|
+
const base = forDocs ? z.union([z.string(), z.number(), z.null()]) : z.union([z.string(), z.number(), z.undefined(), z.null()]);
|
|
206
231
|
const schema = isHasMany ? z.array(base.optional().nullable()) : base;
|
|
207
232
|
if (isHasMany) {
|
|
208
233
|
return z.preprocess((val) => {
|
|
@@ -215,6 +240,18 @@ function mapFieldToZod(field) {
|
|
|
215
240
|
}
|
|
216
241
|
return schema;
|
|
217
242
|
}
|
|
243
|
+
case "file":
|
|
244
|
+
return z.object({
|
|
245
|
+
id: z.string(),
|
|
246
|
+
url: z.string(),
|
|
247
|
+
filename: z.string(),
|
|
248
|
+
mime_type: z.string(),
|
|
249
|
+
filesize: z.number(),
|
|
250
|
+
width: z.number().optional().nullable(),
|
|
251
|
+
height: z.number().optional().nullable(),
|
|
252
|
+
focal_x: z.number().optional().nullable(),
|
|
253
|
+
focal_y: z.number().optional().nullable()
|
|
254
|
+
});
|
|
218
255
|
case "number":
|
|
219
256
|
return z.preprocess((val) => {
|
|
220
257
|
if (val === "" || val === undefined || val === null)
|
|
@@ -224,14 +261,16 @@ function mapFieldToZod(field) {
|
|
|
224
261
|
return Number.isNaN(num) ? undefined : num;
|
|
225
262
|
}
|
|
226
263
|
return val;
|
|
227
|
-
}, z.
|
|
264
|
+
}, z.number().optional().nullable());
|
|
228
265
|
case "boolean":
|
|
229
266
|
return z.preprocess((val) => {
|
|
230
267
|
if (typeof val === "string")
|
|
231
268
|
return val === "true";
|
|
232
269
|
return val;
|
|
233
270
|
}, z.boolean());
|
|
234
|
-
case "date":
|
|
271
|
+
case "date": {
|
|
272
|
+
if (forDocs)
|
|
273
|
+
return z.string().describe("ISO 8601 Date");
|
|
235
274
|
return z.preprocess((val) => {
|
|
236
275
|
if (val === "" || val === undefined || val === null)
|
|
237
276
|
return;
|
|
@@ -242,8 +281,20 @@ function mapFieldToZod(field) {
|
|
|
242
281
|
z.undefined(),
|
|
243
282
|
z.null()
|
|
244
283
|
]));
|
|
284
|
+
}
|
|
245
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
|
+
}
|
|
246
294
|
return z.string();
|
|
295
|
+
}
|
|
296
|
+
case "json":
|
|
297
|
+
return z.any();
|
|
247
298
|
case "array":
|
|
248
299
|
return z.preprocess((val) => {
|
|
249
300
|
if (val === undefined || val === null || val === "")
|
|
@@ -258,6 +309,7 @@ function mapFieldToZod(field) {
|
|
|
258
309
|
}
|
|
259
310
|
|
|
260
311
|
// src/server/handlers.ts
|
|
312
|
+
init_system_schema();
|
|
261
313
|
var hydrateDoc = async (doc, fields, c, config) => {
|
|
262
314
|
if (!doc)
|
|
263
315
|
return doc;
|
|
@@ -317,24 +369,119 @@ var hydrateDoc = async (doc, fields, c, config) => {
|
|
|
317
369
|
await Promise.all(hydratePromises);
|
|
318
370
|
return doc;
|
|
319
371
|
};
|
|
320
|
-
|
|
321
|
-
if (!
|
|
322
|
-
return
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const field = f;
|
|
372
|
+
function parsePopulate(populate) {
|
|
373
|
+
if (!populate)
|
|
374
|
+
return {};
|
|
375
|
+
if (typeof populate === "object" && !Array.isArray(populate)) {
|
|
376
|
+
return populate;
|
|
377
|
+
}
|
|
378
|
+
if (typeof populate === "string" && (populate.startsWith("{") || populate.startsWith("["))) {
|
|
328
379
|
try {
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
331
|
-
|
|
380
|
+
const parsed = JSON.parse(populate);
|
|
381
|
+
if (Array.isArray(parsed)) {
|
|
382
|
+
const result2 = {};
|
|
383
|
+
for (const item of parsed) {
|
|
384
|
+
if (typeof item === "string") {
|
|
385
|
+
Object.assign(result2, parsePopulate(item));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return result2;
|
|
389
|
+
}
|
|
390
|
+
return parsed;
|
|
391
|
+
} catch {}
|
|
392
|
+
}
|
|
393
|
+
const result = {};
|
|
394
|
+
const keys = typeof populate === "string" ? populate.split(",") : [];
|
|
395
|
+
for (const key of keys) {
|
|
396
|
+
const parts = key.trim().split(".");
|
|
397
|
+
let current = result;
|
|
398
|
+
for (let i = 0;i < parts.length; i++) {
|
|
399
|
+
const part = parts[i];
|
|
400
|
+
if (!part)
|
|
401
|
+
continue;
|
|
402
|
+
if (i === parts.length - 1) {
|
|
403
|
+
current[part] = current[part] || true;
|
|
404
|
+
} else {
|
|
405
|
+
if (typeof current[part] !== "object") {
|
|
406
|
+
current[part] = { populate: {} };
|
|
407
|
+
} else if (!current[part].populate) {
|
|
408
|
+
current[part].populate = { ...current[part].populate };
|
|
409
|
+
}
|
|
410
|
+
current = current[part].populate;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
var populateDoc = async (db, fields, doc, populate, config) => {
|
|
417
|
+
if (!doc || !populate || Object.keys(populate).length === 0)
|
|
418
|
+
return doc;
|
|
419
|
+
const populatePromises = fields.map(async (field) => {
|
|
420
|
+
const pValue = populate[field.name];
|
|
421
|
+
if (field.type === "relationship" && field.relationTo && pValue) {
|
|
422
|
+
const relationTo = field.relationTo;
|
|
423
|
+
const nestedPopulate = typeof pValue === "object" ? pValue.populate : undefined;
|
|
424
|
+
const fetchAndPopulate = async (id) => {
|
|
425
|
+
if (!id)
|
|
426
|
+
return id;
|
|
427
|
+
try {
|
|
428
|
+
const systemCollections = getSystemCollections();
|
|
429
|
+
const allCollections = [...config.collections, ...systemCollections];
|
|
430
|
+
const relatedCollection = allCollections.find((c) => c.slug === relationTo || c.apiPath === relationTo);
|
|
431
|
+
const targetSlug = relatedCollection ? relatedCollection.slug : relationTo;
|
|
432
|
+
let relatedDoc = await db.findOne(targetSlug, { id });
|
|
433
|
+
if (relatedDoc && nestedPopulate && relatedCollection) {
|
|
434
|
+
relatedDoc = await populateDoc(db, relatedCollection.fields, relatedDoc, nestedPopulate, config);
|
|
435
|
+
}
|
|
436
|
+
return relatedDoc || id;
|
|
437
|
+
} catch (err) {
|
|
438
|
+
console.error(`[OpacaCMS] Failed to populate relationship ${field.name}`, err);
|
|
439
|
+
return id;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
if (field.hasMany && Array.isArray(doc[field.name])) {
|
|
443
|
+
doc[field.name] = await Promise.all(doc[field.name].map(fetchAndPopulate));
|
|
444
|
+
} else if (doc[field.name]) {
|
|
445
|
+
doc[field.name] = await fetchAndPopulate(doc[field.name]);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (field.type === "group" && field.fields && doc[field.name]) {
|
|
449
|
+
if (typeof pValue === "object" && pValue.populate) {
|
|
450
|
+
await populateDoc(db, field.fields, doc[field.name], pValue.populate, config);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (field.type === "array" && field.fields && Array.isArray(doc[field.name])) {
|
|
454
|
+
if (typeof pValue === "object" && pValue.populate) {
|
|
455
|
+
await Promise.all(doc[field.name].map((item) => populateDoc(db, field.fields, item, pValue.populate, config)));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (field.type === "blocks" && field.blocks && Array.isArray(doc[field.name])) {
|
|
459
|
+
if (typeof pValue === "object" && pValue.populate) {
|
|
460
|
+
await Promise.all(doc[field.name].map((item) => {
|
|
461
|
+
const block = field.blocks.find((b) => b.slug === item.block_type);
|
|
462
|
+
if (block && block.fields) {
|
|
463
|
+
return populateDoc(db, block.fields, item, pValue.populate, config);
|
|
464
|
+
}
|
|
465
|
+
return Promise.resolve();
|
|
466
|
+
}));
|
|
332
467
|
}
|
|
333
|
-
} catch (err) {
|
|
334
|
-
console.error(`[OpacaCMS] Failed to populate relationship ${field.name} `, err);
|
|
335
468
|
}
|
|
336
469
|
});
|
|
337
470
|
await Promise.all(populatePromises);
|
|
471
|
+
const pKeys = Object.keys(populate);
|
|
472
|
+
const hasSelection = pKeys.some((k) => {
|
|
473
|
+
const field = fields.find((f) => f.name === k);
|
|
474
|
+
return field && !["relationship", "group", "array", "blocks"].includes(field.type);
|
|
475
|
+
});
|
|
476
|
+
if (hasSelection) {
|
|
477
|
+
const filtered = {};
|
|
478
|
+
for (const key of pKeys) {
|
|
479
|
+
if (populate[key]) {
|
|
480
|
+
filtered[key] = doc[key];
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return filtered;
|
|
484
|
+
}
|
|
338
485
|
return doc;
|
|
339
486
|
};
|
|
340
487
|
function createHandlers(config, collection, getAuth) {
|
|
@@ -415,7 +562,7 @@ function createHandlers(config, collection, getAuth) {
|
|
|
415
562
|
const saveVersion = async (doc, status) => {
|
|
416
563
|
if (!collection.versions)
|
|
417
564
|
return;
|
|
418
|
-
const versionsTable = `${collection.slug}_versions
|
|
565
|
+
const versionsTable = `${toSnakeCase(collection.slug)}_versions`;
|
|
419
566
|
const versionDoc = {
|
|
420
567
|
id: crypto.randomUUID(),
|
|
421
568
|
_parent_id: doc.id,
|
|
@@ -447,7 +594,7 @@ function createHandlers(config, collection, getAuth) {
|
|
|
447
594
|
if (limit > maxLimit)
|
|
448
595
|
limit = maxLimit;
|
|
449
596
|
const sort = queries.sort;
|
|
450
|
-
const populate = queries.populate
|
|
597
|
+
const populate = parsePopulate(queries.populate);
|
|
451
598
|
const filter = {};
|
|
452
599
|
for (const [key, value] of Object.entries(queries)) {
|
|
453
600
|
if (["page", "limit", "sort", "populate", "locale", "draft"].includes(key))
|
|
@@ -464,8 +611,8 @@ function createHandlers(config, collection, getAuth) {
|
|
|
464
611
|
}
|
|
465
612
|
}
|
|
466
613
|
const results = await db.find(collection.slug, filter, { page, limit, sort });
|
|
467
|
-
if (populate.length > 0) {
|
|
468
|
-
results.docs = await Promise.all(results.docs.map((doc) => populateDoc(db, collection.fields, doc, populate)));
|
|
614
|
+
if (Object.keys(populate).length > 0) {
|
|
615
|
+
results.docs = await Promise.all(results.docs.map((doc) => populateDoc(db, collection.fields, doc, populate, config)));
|
|
469
616
|
}
|
|
470
617
|
results.docs = await Promise.all(results.docs.map((doc) => hydrateDoc(doc, collection.fields, c, config)));
|
|
471
618
|
return c.json(results);
|
|
@@ -475,13 +622,13 @@ function createHandlers(config, collection, getAuth) {
|
|
|
475
622
|
return c.json({ message: "Forbidden" }, 403);
|
|
476
623
|
}
|
|
477
624
|
const queries = c.req.query();
|
|
478
|
-
const populate = queries.populate
|
|
625
|
+
const populate = parsePopulate(queries.populate);
|
|
479
626
|
const id = c.req.param("id");
|
|
480
627
|
let doc = await db.findOne(collection.slug, { id });
|
|
481
628
|
if (!doc)
|
|
482
629
|
return c.json({ message: "Not found" }, 404);
|
|
483
|
-
if (populate.length > 0) {
|
|
484
|
-
doc = await populateDoc(db, collection.fields, doc, populate);
|
|
630
|
+
if (Object.keys(populate).length > 0) {
|
|
631
|
+
doc = await populateDoc(db, collection.fields, doc, populate, config);
|
|
485
632
|
}
|
|
486
633
|
doc = await hydrateDoc(doc, collection.fields, c, config);
|
|
487
634
|
const permissions = await getFieldAccessPermissions(c, "read", collection.fields, doc);
|
|
@@ -645,7 +792,7 @@ function createHandlers(config, collection, getAuth) {
|
|
|
645
792
|
return c.json({ message: "Forbidden" }, 403);
|
|
646
793
|
}
|
|
647
794
|
const parentId = c.req.query("parentId");
|
|
648
|
-
const versionsTable = `${collection.slug}
|
|
795
|
+
const versionsTable = `${toSnakeCase(collection.slug)}_versions`;
|
|
649
796
|
const query = parentId ? `SELECT * FROM ${versionsTable} WHERE _parent_id = ? ORDER BY created_at DESC` : `SELECT * FROM ${versionsTable} ORDER BY created_at DESC`;
|
|
650
797
|
const params = parentId ? [parentId] : [];
|
|
651
798
|
try {
|
|
@@ -660,7 +807,7 @@ function createHandlers(config, collection, getAuth) {
|
|
|
660
807
|
return c.json({ message: "Forbidden" }, 403);
|
|
661
808
|
}
|
|
662
809
|
const versionId = c.req.param("versionId");
|
|
663
|
-
const versionsTable = `${collection.slug}
|
|
810
|
+
const versionsTable = `${toSnakeCase(collection.slug)}_versions`;
|
|
664
811
|
try {
|
|
665
812
|
const versionRows = await db.unsafe(`SELECT * FROM ${versionsTable} WHERE id = ? `, [versionId]);
|
|
666
813
|
if (versionRows.length === 0)
|
|
@@ -744,29 +891,43 @@ import { Hono } from "hono";
|
|
|
744
891
|
// src/server/middlewares/admin.ts
|
|
745
892
|
var adminMiddleware = async (c, next) => {
|
|
746
893
|
const user = c.get("user");
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
894
|
+
const config = c.get("config");
|
|
895
|
+
const isDevelopment = true;
|
|
896
|
+
const path = c.req.path;
|
|
897
|
+
const isMetadata = path.endsWith("/__admin/metadata");
|
|
898
|
+
const isSetup = path.endsWith("/__admin/setup");
|
|
899
|
+
const secretHeader = c.req.header("X-Opaca-Secret");
|
|
900
|
+
const isSecretValid = config?.secret && secretHeader && secretHeader === config.secret;
|
|
901
|
+
if (isSetup) {
|
|
752
902
|
await next();
|
|
753
903
|
return;
|
|
754
904
|
}
|
|
755
|
-
if (
|
|
756
|
-
|
|
757
|
-
|
|
905
|
+
if (isMetadata) {
|
|
906
|
+
if (isDevelopment || isSecretValid) {
|
|
907
|
+
await next();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (user) {
|
|
912
|
+
const isAdmin = user.role === "admin" || user.role?.includes("admin");
|
|
913
|
+
if (isAdmin) {
|
|
914
|
+
await next();
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
return c.json({ message: "Forbidden" }, 403);
|
|
758
918
|
}
|
|
759
|
-
return c.json({ message: "
|
|
919
|
+
return c.json({ message: "Unauthorized" }, 401);
|
|
760
920
|
};
|
|
761
921
|
|
|
762
922
|
// src/server/routers/admin.ts
|
|
763
|
-
function createAdminRouter(config, state) {
|
|
923
|
+
function createAdminRouter(config, settings, state) {
|
|
764
924
|
const adminRouter = new Hono;
|
|
765
|
-
const adminHandlers = createAdminHandlers(config, () => state.auth);
|
|
925
|
+
const adminHandlers = createAdminHandlers(config, settings, () => state.auth);
|
|
766
926
|
adminRouter.get("/collections", adminMiddleware, adminHandlers.getCollections);
|
|
767
|
-
adminRouter.get("/metadata", adminHandlers.getMetadata);
|
|
927
|
+
adminRouter.get("/metadata", adminMiddleware, adminHandlers.getMetadata);
|
|
768
928
|
adminRouter.get("/config", adminMiddleware, adminHandlers.getConfig);
|
|
769
929
|
adminRouter.get("/setup", adminHandlers.getSetupStatus);
|
|
930
|
+
adminRouter.get("/plugin-settings/:name", adminMiddleware, adminHandlers.getPluginSettings);
|
|
770
931
|
adminRouter.post("/api-key/create", adminMiddleware, adminHandlers.createApiKey);
|
|
771
932
|
return adminRouter;
|
|
772
933
|
}
|
|
@@ -917,14 +1078,11 @@ function createAssetsHandlers(config) {
|
|
|
917
1078
|
const rows = result.docs;
|
|
918
1079
|
const total = result.totalDocs;
|
|
919
1080
|
let folderRows = [];
|
|
920
|
-
const bucketFilter = bucket !== "all" ? `AND bucket = ?` : "";
|
|
921
|
-
const bucketParam = bucket !== "all" ? [bucket] : [];
|
|
922
1081
|
if (config.db.name === "postgres") {
|
|
923
|
-
const pgBucketFilter = bucketFilter.replace("?", "$1");
|
|
924
1082
|
if (folder === null || folder === "") {
|
|
925
|
-
folderRows = await config.db.unsafe(
|
|
1083
|
+
folderRows = await config.db.unsafe("SELECT DISTINCT split_part(folder, '/', 1) as subfolder, bucket FROM _opaca_assets WHERE folder IS NOT NULL AND folder != '' AND (bucket = $1 OR $1 = 'all')", [bucket]);
|
|
926
1084
|
} else {
|
|
927
|
-
folderRows = await config.db.unsafe(
|
|
1085
|
+
folderRows = await config.db.unsafe("SELECT DISTINCT split_part(substring(folder from length($1) + 2), '/', 1) as subfolder, bucket FROM _opaca_assets WHERE folder LIKE $2 AND (bucket = $3 OR $3 = 'all')", [folder, `${folder}/%`, bucket]);
|
|
928
1086
|
}
|
|
929
1087
|
} else {
|
|
930
1088
|
if (folder === null || folder === "") {
|
|
@@ -935,8 +1093,8 @@ function createAssetsHandlers(config) {
|
|
|
935
1093
|
ELSE folder
|
|
936
1094
|
END as subfolder,
|
|
937
1095
|
bucket
|
|
938
|
-
FROM _opaca_assets WHERE folder IS NOT NULL AND folder != ''
|
|
939
|
-
`,
|
|
1096
|
+
FROM _opaca_assets WHERE folder IS NOT NULL AND folder != '' AND (bucket = ? OR ? = 'all')
|
|
1097
|
+
`, [bucket, bucket]);
|
|
940
1098
|
} else {
|
|
941
1099
|
const skipLen = folder.length + 2;
|
|
942
1100
|
folderRows = await config.db.unsafe(`
|
|
@@ -946,8 +1104,8 @@ function createAssetsHandlers(config) {
|
|
|
946
1104
|
ELSE SUBSTR(folder, ?)
|
|
947
1105
|
END as subfolder,
|
|
948
1106
|
bucket
|
|
949
|
-
FROM _opaca_assets WHERE folder LIKE ?
|
|
950
|
-
`, [skipLen, skipLen, skipLen, skipLen, `${folder}/%`,
|
|
1107
|
+
FROM _opaca_assets WHERE folder LIKE ? AND (bucket = ? OR ? = 'all')
|
|
1108
|
+
`, [skipLen, skipLen, skipLen, skipLen, `${folder}/%`, bucket, bucket]);
|
|
951
1109
|
}
|
|
952
1110
|
}
|
|
953
1111
|
const folderMap = {};
|
|
@@ -1057,6 +1215,28 @@ function mountGlobalRoutes(router, config, state) {
|
|
|
1057
1215
|
}
|
|
1058
1216
|
function createSystemRouter(config) {
|
|
1059
1217
|
const systemRouter = new Hono3;
|
|
1218
|
+
systemRouter.get("/plugins/assets", adminMiddleware, (c) => {
|
|
1219
|
+
const response = {
|
|
1220
|
+
scripts: [],
|
|
1221
|
+
styles: []
|
|
1222
|
+
};
|
|
1223
|
+
if (config.plugins && Array.isArray(config.plugins)) {
|
|
1224
|
+
for (const plugin of config.plugins) {
|
|
1225
|
+
if (plugin.adminAssets) {
|
|
1226
|
+
try {
|
|
1227
|
+
const assets = plugin.adminAssets();
|
|
1228
|
+
if (assets.scripts)
|
|
1229
|
+
response.scripts.push(...assets.scripts);
|
|
1230
|
+
if (assets.styles)
|
|
1231
|
+
response.styles.push(...assets.styles);
|
|
1232
|
+
} catch (e) {
|
|
1233
|
+
console.error(`[Plugin] ${plugin.name} failed to expose adminAssets: `, e);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return c.json(response);
|
|
1239
|
+
});
|
|
1060
1240
|
if (config.storages) {
|
|
1061
1241
|
const assetsHandlers = createAssetsHandlers(config);
|
|
1062
1242
|
systemRouter.post("/assets/upload", adminMiddleware, assetsHandlers.upload);
|
|
@@ -1115,7 +1295,7 @@ function createAuthMiddleware(getAuth) {
|
|
|
1115
1295
|
const ownerResult = await auth.options.database?.findOne?.("_users", {
|
|
1116
1296
|
id: result.key.referenceId
|
|
1117
1297
|
});
|
|
1118
|
-
c.set("user", ownerResult
|
|
1298
|
+
c.set("user", ownerResult);
|
|
1119
1299
|
} catch (e) {
|
|
1120
1300
|
logger.warn("Failed to fetch API key owner from database:", e);
|
|
1121
1301
|
c.set("user", null);
|
|
@@ -1138,9 +1318,11 @@ function createAuthMiddleware(getAuth) {
|
|
|
1138
1318
|
|
|
1139
1319
|
// src/server/middlewares/context.ts
|
|
1140
1320
|
function createContextMiddleware(config) {
|
|
1321
|
+
const logger2 = new OpacaLogger(config.logger);
|
|
1141
1322
|
return async (c, next) => {
|
|
1142
1323
|
c.set("config", config);
|
|
1143
1324
|
c.set("db", config.db);
|
|
1325
|
+
c.set("logger", logger2);
|
|
1144
1326
|
await next();
|
|
1145
1327
|
};
|
|
1146
1328
|
}
|
|
@@ -1329,7 +1511,8 @@ function setupMiddlewares(router, config, state) {
|
|
|
1329
1511
|
router.use("*", createCorsMiddleware(config));
|
|
1330
1512
|
router.use("*", createDatabaseInitMiddleware(config, state));
|
|
1331
1513
|
router.onError((err, c) => {
|
|
1332
|
-
|
|
1514
|
+
const ctxLogger = c.get("logger");
|
|
1515
|
+
ctxLogger.error(`API Error: ${err.message}`, err);
|
|
1333
1516
|
return c.json({ message: "Internal Server Error", error: err.message }, 500);
|
|
1334
1517
|
});
|
|
1335
1518
|
}
|
|
@@ -1337,8 +1520,196 @@ function setupAuthMiddlewares(router, config, state) {
|
|
|
1337
1520
|
router.use("*", createAuthMiddleware(() => state.auth));
|
|
1338
1521
|
}
|
|
1339
1522
|
|
|
1523
|
+
// src/server/openapi.ts
|
|
1524
|
+
import { z as z2 } from "zod";
|
|
1525
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
1526
|
+
function generateOpenAPISchema(config) {
|
|
1527
|
+
const openapi = {
|
|
1528
|
+
openapi: "3.1.0",
|
|
1529
|
+
info: {
|
|
1530
|
+
title: config.appName || "OpacaCMS API",
|
|
1531
|
+
version: "1.0.0",
|
|
1532
|
+
description: "Automatically generated OpenAPI schema for OpacaCMS Collections and Globals."
|
|
1533
|
+
},
|
|
1534
|
+
paths: {},
|
|
1535
|
+
components: {
|
|
1536
|
+
schemas: {},
|
|
1537
|
+
securitySchemes: {
|
|
1538
|
+
bearerAuth: {
|
|
1539
|
+
type: "http",
|
|
1540
|
+
scheme: "bearer"
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
for (const collection of config.collections || []) {
|
|
1546
|
+
const isHidden = collection.hidden === true;
|
|
1547
|
+
if (isHidden)
|
|
1548
|
+
continue;
|
|
1549
|
+
const pathBase = `/api/${collection.apiPath || collection.slug}`;
|
|
1550
|
+
const tag = collection.label || collection.slug;
|
|
1551
|
+
const zodSchema = generateSchemaForCollection(collection, false, true);
|
|
1552
|
+
const schemaName = collection.slug.charAt(0).toUpperCase() + collection.slug.slice(1);
|
|
1553
|
+
const jsonSchema = typeof z2.toJSONSchema === "function" ? z2.toJSONSchema(zodSchema, { target: "openapi-3.0" }) : zodToJsonSchema(zodSchema, { target: "openApi3" });
|
|
1554
|
+
openapi.components.schemas[schemaName] = jsonSchema;
|
|
1555
|
+
const ref = `#/components/schemas/${schemaName}`;
|
|
1556
|
+
openapi.paths[pathBase] = {
|
|
1557
|
+
get: {
|
|
1558
|
+
tags: [tag],
|
|
1559
|
+
summary: `Find ${tag}`,
|
|
1560
|
+
parameters: [
|
|
1561
|
+
{ name: "limit", in: "query", schema: { type: "integer" } },
|
|
1562
|
+
{ name: "page", in: "query", schema: { type: "integer" } }
|
|
1563
|
+
],
|
|
1564
|
+
responses: {
|
|
1565
|
+
"200": {
|
|
1566
|
+
description: "Successful response",
|
|
1567
|
+
content: {
|
|
1568
|
+
"application/json": {
|
|
1569
|
+
schema: {
|
|
1570
|
+
type: "object",
|
|
1571
|
+
properties: {
|
|
1572
|
+
docs: { type: "array", items: { $ref: ref } },
|
|
1573
|
+
totalDocs: { type: "integer" }
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
post: {
|
|
1582
|
+
tags: [tag],
|
|
1583
|
+
summary: `Create ${tag}`,
|
|
1584
|
+
requestBody: {
|
|
1585
|
+
required: true,
|
|
1586
|
+
content: {
|
|
1587
|
+
"application/json": { schema: { $ref: ref } }
|
|
1588
|
+
}
|
|
1589
|
+
},
|
|
1590
|
+
responses: {
|
|
1591
|
+
"201": {
|
|
1592
|
+
description: "Created successfully",
|
|
1593
|
+
content: { "application/json": { schema: { $ref: ref } } }
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
openapi.paths[`${pathBase}/{id}`] = {
|
|
1599
|
+
get: {
|
|
1600
|
+
tags: [tag],
|
|
1601
|
+
summary: `Find ${tag} by ID`,
|
|
1602
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
1603
|
+
responses: {
|
|
1604
|
+
"200": {
|
|
1605
|
+
description: "Successful response",
|
|
1606
|
+
content: { "application/json": { schema: { $ref: ref } } }
|
|
1607
|
+
},
|
|
1608
|
+
"404": { description: "Not found" }
|
|
1609
|
+
}
|
|
1610
|
+
},
|
|
1611
|
+
patch: {
|
|
1612
|
+
tags: [tag],
|
|
1613
|
+
summary: `Update ${tag}`,
|
|
1614
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
1615
|
+
requestBody: {
|
|
1616
|
+
content: { "application/json": { schema: { $ref: ref } } }
|
|
1617
|
+
},
|
|
1618
|
+
responses: {
|
|
1619
|
+
"200": {
|
|
1620
|
+
description: "Updated successfully",
|
|
1621
|
+
content: { "application/json": { schema: { $ref: ref } } }
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
},
|
|
1625
|
+
delete: {
|
|
1626
|
+
tags: [tag],
|
|
1627
|
+
summary: `Delete ${tag}`,
|
|
1628
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
1629
|
+
responses: {
|
|
1630
|
+
"200": { description: "Deleted successfully" }
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
for (const global of config.globals || []) {
|
|
1636
|
+
const pathBase = `/api/globals/${global.slug}`;
|
|
1637
|
+
const tag = global.label || global.slug;
|
|
1638
|
+
const zodSchema = generateSchemaForCollection(global, false, true);
|
|
1639
|
+
const schemaName = global.slug.charAt(0).toUpperCase() + global.slug.slice(1);
|
|
1640
|
+
const jsonSchema = typeof z2.toJSONSchema === "function" ? z2.toJSONSchema(zodSchema, { target: "openapi-3.0" }) : zodToJsonSchema(zodSchema, { target: "openApi3" });
|
|
1641
|
+
openapi.components.schemas[schemaName] = jsonSchema;
|
|
1642
|
+
const ref = `#/components/schemas/${schemaName}`;
|
|
1643
|
+
openapi.paths[pathBase] = {
|
|
1644
|
+
get: {
|
|
1645
|
+
tags: [tag],
|
|
1646
|
+
summary: `Find ${tag}`,
|
|
1647
|
+
responses: {
|
|
1648
|
+
"200": {
|
|
1649
|
+
description: "Successful response",
|
|
1650
|
+
content: { "application/json": { schema: { $ref: ref } } }
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
post: {
|
|
1655
|
+
tags: [tag],
|
|
1656
|
+
summary: `Update ${tag}`,
|
|
1657
|
+
requestBody: {
|
|
1658
|
+
content: { "application/json": { schema: { $ref: ref } } }
|
|
1659
|
+
},
|
|
1660
|
+
responses: {
|
|
1661
|
+
"200": {
|
|
1662
|
+
description: "Updated successfully",
|
|
1663
|
+
content: { "application/json": { schema: { $ref: ref } } }
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
return openapi;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1340
1672
|
// src/server/router.ts
|
|
1341
|
-
|
|
1673
|
+
import { Scalar } from "@scalar/hono-api-reference";
|
|
1674
|
+
|
|
1675
|
+
// src/server/routers/plugins.ts
|
|
1676
|
+
function mountPluginRoutes(config, settings, logger2, router) {
|
|
1677
|
+
if (config.plugins && Array.isArray(config.plugins)) {
|
|
1678
|
+
for (const plugin of config.plugins) {
|
|
1679
|
+
const pluginSettings = settings[plugin.name] || {};
|
|
1680
|
+
const pluginContext = { config, logger: logger2, settings: pluginSettings };
|
|
1681
|
+
if (plugin.onRequest) {
|
|
1682
|
+
router.use("*", async (c, next) => {
|
|
1683
|
+
const result = await plugin.onRequest(c);
|
|
1684
|
+
if (result === false) {
|
|
1685
|
+
return c.json({ error: "Blocked by plugin: " + plugin.name }, 403);
|
|
1686
|
+
}
|
|
1687
|
+
await next();
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
if (plugin.onRouterInit) {
|
|
1691
|
+
try {
|
|
1692
|
+
plugin.onRouterInit(router, pluginContext);
|
|
1693
|
+
} catch (e) {
|
|
1694
|
+
logger2.error(`[Plugin] ${plugin.name} failed during onRouterInit: `, e);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
function firePluginInitComplete(config, settings, logger2) {
|
|
1701
|
+
if (config.plugins && Array.isArray(config.plugins)) {
|
|
1702
|
+
for (const plugin of config.plugins) {
|
|
1703
|
+
if (plugin.onInitComplete) {
|
|
1704
|
+
const pluginSettings = settings[plugin.name] || {};
|
|
1705
|
+
plugin.onInitComplete({ config, logger: logger2, settings: pluginSettings });
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// src/server/router.ts
|
|
1712
|
+
function createAPIRouter(config, settings = {}) {
|
|
1342
1713
|
const state = { auth: undefined, migrated: false };
|
|
1343
1714
|
const router = new Hono4().basePath("/api");
|
|
1344
1715
|
setupMiddlewares(router, config, state);
|
|
@@ -1347,12 +1718,36 @@ function createAPIRouter(config) {
|
|
|
1347
1718
|
return c.json({ status: "ok", version: "1.0.0", appName: config.appName });
|
|
1348
1719
|
});
|
|
1349
1720
|
router.route("/auth", createAuthRouter(config, state));
|
|
1350
|
-
router.route("/__admin", createAdminRouter(config, state));
|
|
1721
|
+
router.route("/__admin", createAdminRouter(config, settings, state));
|
|
1351
1722
|
router.route("/__system", createSystemRouter(config));
|
|
1352
1723
|
router.route("/", createAssetsServingRouter(config));
|
|
1724
|
+
mountPluginRoutes(config, settings, logger, router);
|
|
1353
1725
|
mountCollectionRoutes(router, config, state);
|
|
1354
1726
|
mountGlobalRoutes(router, config, state);
|
|
1727
|
+
if (config.api?.openAPI?.enabled) {
|
|
1728
|
+
router.get("/open-api.json", (c) => {
|
|
1729
|
+
const schema = generateOpenAPISchema(config);
|
|
1730
|
+
return c.json(schema);
|
|
1731
|
+
});
|
|
1732
|
+
const referencePath = config.api.openAPI.path || "/reference";
|
|
1733
|
+
const safeRefPath = referencePath.startsWith("/") ? referencePath : `/${referencePath}`;
|
|
1734
|
+
router.get(safeRefPath, Scalar({
|
|
1735
|
+
pageTitle: `${config.appName || "OpacaCMS"} API Documentation`,
|
|
1736
|
+
theme: config.api.openAPI.theme || "default",
|
|
1737
|
+
layout: config.api.openAPI.layout === "classic" ? "classic" : "modern",
|
|
1738
|
+
hideModels: config.api.openAPI.hideModels,
|
|
1739
|
+
hideDownloadButton: config.api.openAPI.hideDownloadButton,
|
|
1740
|
+
customCss: config.api.openAPI.customCss,
|
|
1741
|
+
...{
|
|
1742
|
+
sources: [
|
|
1743
|
+
{ url: "/api/open-api.json", title: "CMS Collections" },
|
|
1744
|
+
{ url: "/api/auth/open-api/generate-schema", title: "Auth APIs" }
|
|
1745
|
+
]
|
|
1746
|
+
}
|
|
1747
|
+
}));
|
|
1748
|
+
}
|
|
1749
|
+
firePluginInitComplete(config, settings, logger);
|
|
1355
1750
|
return router;
|
|
1356
1751
|
}
|
|
1357
1752
|
|
|
1358
|
-
export { createAdminHandlers, hydrateDoc, createHandlers, createGlobalHandlers, createAPIRouter };
|
|
1753
|
+
export { createAdminHandlers, hydrateDoc, parsePopulate, populateDoc, createHandlers, createGlobalHandlers, createAPIRouter };
|