@dyrected/core 2.5.30 → 2.5.32
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/dist/chunk-6ZXEGKT6.js +619 -0
- package/dist/chunk-Y4AYI52W.js +631 -0
- package/dist/chunk-YDOBB7MY.js +1457 -0
- package/dist/index-CdQwnbfh.d.cts +1889 -0
- package/dist/index-CdQwnbfh.d.ts +1889 -0
- package/dist/index-M_m6dXZV.d.cts +1889 -0
- package/dist/index-M_m6dXZV.d.ts +1889 -0
- package/dist/index.cjs +853 -832
- package/dist/index.d.cts +13 -18
- package/dist/index.d.ts +13 -18
- package/dist/index.js +5 -824
- package/dist/server.cjs +605 -69
- package/dist/server.d.cts +7 -2
- package/dist/server.d.ts +7 -2
- package/dist/server.js +38 -339
- package/package.json +1 -1
package/dist/server.cjs
CHANGED
|
@@ -535,6 +535,20 @@ function createReadonlyDb(db) {
|
|
|
535
535
|
});
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
+
// src/auth/jexl.ts
|
|
539
|
+
var import_jexl = __toESM(require("jexl"), 1);
|
|
540
|
+
async function evaluateAccess(expression, context) {
|
|
541
|
+
if (expression === void 0 || expression === null) return false;
|
|
542
|
+
if (typeof expression === "boolean") return expression;
|
|
543
|
+
try {
|
|
544
|
+
const result = await import_jexl.default.eval(expression, context);
|
|
545
|
+
return !!result;
|
|
546
|
+
} catch (err) {
|
|
547
|
+
console.error("[dyrected/core] Jexl evaluation failed:", err);
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
538
552
|
// src/workflows.ts
|
|
539
553
|
var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
|
|
540
554
|
var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
|
|
@@ -550,6 +564,14 @@ function workflowCapabilities(workflow, user) {
|
|
|
550
564
|
}
|
|
551
565
|
return capabilities;
|
|
552
566
|
}
|
|
567
|
+
function canViewWorkflowDraft(workflow, user) {
|
|
568
|
+
if (!user) return false;
|
|
569
|
+
const capabilities = workflowCapabilities(workflow, user);
|
|
570
|
+
if (capabilities.has("entry.edit")) return true;
|
|
571
|
+
return workflow.transitions.some(
|
|
572
|
+
(transition) => (transition.requiredCapabilities ?? []).some((capability) => capabilities.has(capability))
|
|
573
|
+
);
|
|
574
|
+
}
|
|
553
575
|
function availableWorkflowTransitions(workflow, state, user) {
|
|
554
576
|
const capabilities = workflowCapabilities(workflow, user);
|
|
555
577
|
return workflow.transitions.filter((transition) => {
|
|
@@ -567,7 +589,7 @@ function materializeWorkflowDocument(doc, workflow, user) {
|
|
|
567
589
|
const meta = doc.__workflow;
|
|
568
590
|
if (!meta) return doc;
|
|
569
591
|
const { __published, __workflow, ...working } = doc;
|
|
570
|
-
if (!user) {
|
|
592
|
+
if (!canViewWorkflowDraft(workflow, user)) {
|
|
571
593
|
if (!__published || typeof __published !== "object") return null;
|
|
572
594
|
return { id: doc.id, ...__published, _workflow: publicMetadata(meta) };
|
|
573
595
|
}
|
|
@@ -711,12 +733,18 @@ async function transitionWorkflow(args) {
|
|
|
711
733
|
events.push(createLifecycleEvent({ name: "entry.unpublished", collection: collection.slug, documentId: id, actorId: user?.sub, payload: { revision: meta.publishedRevision } }));
|
|
712
734
|
}
|
|
713
735
|
const updated = await db.transaction(async (tx) => {
|
|
736
|
+
const locked = await tx.findOne({ collection: collection.slug, id });
|
|
737
|
+
if (!locked) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
|
|
738
|
+
const lockedMeta = locked.__workflow;
|
|
739
|
+
if (lockedMeta.revision !== meta.revision || lockedMeta.state !== meta.state || expectedRevision !== void 0 && lockedMeta.revision !== expectedRevision) {
|
|
740
|
+
throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), { statusCode: 409 });
|
|
741
|
+
}
|
|
714
742
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
715
|
-
const { __published: _published, __workflow: _workflow, id: _id, ...working } =
|
|
743
|
+
const { __published: _published, __workflow: _workflow, id: _id, ...working } = locked;
|
|
716
744
|
const nextMeta = {
|
|
717
|
-
...
|
|
745
|
+
...lockedMeta,
|
|
718
746
|
state: transition.to,
|
|
719
|
-
...targetState.published ? { publishedRevision:
|
|
747
|
+
...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now, publishedBy: user?.sub } : {},
|
|
720
748
|
...transition.unpublish ? { publishedRevision: void 0, publishedAt: void 0, publishedBy: void 0 } : {}
|
|
721
749
|
};
|
|
722
750
|
const data = { __workflow: nextMeta };
|
|
@@ -725,11 +753,11 @@ async function transitionWorkflow(args) {
|
|
|
725
753
|
const next = await tx.update({ collection: collection.slug, id, data });
|
|
726
754
|
await tx.create({
|
|
727
755
|
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
728
|
-
data: { collection: collection.slug, documentId: id, transition: transition.name, from:
|
|
756
|
+
data: { collection: collection.slug, documentId: id, transition: transition.name, from: lockedMeta.state, to: transition.to, revision: lockedMeta.revision, comment: comment ?? null, actorId: user?.sub ?? null, createdAt: now }
|
|
729
757
|
});
|
|
730
758
|
for (const event of events) await persistEvent(tx, event);
|
|
731
759
|
if (collection.audit) {
|
|
732
|
-
await tx.create({ collection: "__audit", data: { collection: collection.slug, documentId: id, operation: "workflow.transition", user: user?.sub ?? null, timestamp: now, changes: JSON.stringify({ transition: transition.name, from:
|
|
760
|
+
await tx.create({ collection: "__audit", data: { collection: collection.slug, documentId: id, operation: "workflow.transition", user: user?.sub ?? null, timestamp: now, changes: JSON.stringify({ transition: transition.name, from: lockedMeta.state, to: transition.to, revision: lockedMeta.revision }) } });
|
|
733
761
|
}
|
|
734
762
|
return next;
|
|
735
763
|
});
|
|
@@ -788,7 +816,7 @@ var CollectionController = class {
|
|
|
788
816
|
if (beforeReadResult !== void 0) {
|
|
789
817
|
where = beforeReadResult;
|
|
790
818
|
}
|
|
791
|
-
if (this.collection.workflow && !user) {
|
|
819
|
+
if (this.collection.workflow && !canViewWorkflowDraft(this.collection.workflow, user)) {
|
|
792
820
|
where = where ? { AND: [where, { __published: { exists: true } }] } : { __published: { exists: true } };
|
|
793
821
|
}
|
|
794
822
|
let result = await db.find({
|
|
@@ -1081,9 +1109,28 @@ var CollectionController = class {
|
|
|
1081
1109
|
const config = c.get("config");
|
|
1082
1110
|
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
1083
1111
|
if (!this.collection.workflow) return c.json({ message: "Workflows are not enabled for this collection" }, 404);
|
|
1112
|
+
const documentId = c.req.param("id");
|
|
1113
|
+
if (!documentId) return c.json({ message: "Missing ID" }, 400);
|
|
1114
|
+
const document = await config.db.findOne({ collection: this.collection.slug, id: documentId });
|
|
1115
|
+
if (!document) return c.json({ message: "Not Found" }, 404);
|
|
1116
|
+
const readAccess = this.collection.access?.read;
|
|
1117
|
+
if (readAccess !== void 0 && readAccess !== null) {
|
|
1118
|
+
const args = { user: c.get("user"), req: c.req, doc: document };
|
|
1119
|
+
const result2 = typeof readAccess === "function" ? await readAccess(args) : await evaluateAccess(readAccess, args);
|
|
1120
|
+
let allowed = result2 === true;
|
|
1121
|
+
if (result2 && typeof result2 === "object") {
|
|
1122
|
+
const match = await config.db.find({
|
|
1123
|
+
collection: this.collection.slug,
|
|
1124
|
+
where: { AND: [{ id: { equals: documentId } }, result2] },
|
|
1125
|
+
limit: 1
|
|
1126
|
+
});
|
|
1127
|
+
allowed = match.total > 0;
|
|
1128
|
+
}
|
|
1129
|
+
if (!allowed) return c.json({ error: true, message: `Access denied: read on ${this.collection.slug}` }, 403);
|
|
1130
|
+
}
|
|
1084
1131
|
const result = await config.db.find({
|
|
1085
1132
|
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
1086
|
-
where: { collection: { equals: this.collection.slug }, documentId: { equals:
|
|
1133
|
+
where: { collection: { equals: this.collection.slug }, documentId: { equals: documentId } },
|
|
1087
1134
|
sort: "-createdAt",
|
|
1088
1135
|
limit: Math.min(Number(c.req.query("limit")) || 50, 100)
|
|
1089
1136
|
});
|
|
@@ -2115,7 +2162,76 @@ function generateOpenApi(config) {
|
|
|
2115
2162
|
description: "Automatically generated OpenAPI specification for the Dyrected project."
|
|
2116
2163
|
},
|
|
2117
2164
|
components: {
|
|
2118
|
-
schemas: {
|
|
2165
|
+
schemas: {
|
|
2166
|
+
WorkflowMetadata: {
|
|
2167
|
+
type: "object",
|
|
2168
|
+
required: ["state", "revision"],
|
|
2169
|
+
properties: {
|
|
2170
|
+
state: { type: "string" },
|
|
2171
|
+
revision: { type: "integer", minimum: 1 },
|
|
2172
|
+
publishedRevision: { type: "integer", minimum: 1 },
|
|
2173
|
+
publishedAt: { type: "string", format: "date-time" },
|
|
2174
|
+
publishedBy: { type: "string" },
|
|
2175
|
+
availableTransitions: { type: "array", items: { type: "string" } }
|
|
2176
|
+
}
|
|
2177
|
+
},
|
|
2178
|
+
WorkflowTransitionRequest: {
|
|
2179
|
+
type: "object",
|
|
2180
|
+
properties: {
|
|
2181
|
+
expectedRevision: { type: "integer", minimum: 1 },
|
|
2182
|
+
comment: { type: "string" }
|
|
2183
|
+
}
|
|
2184
|
+
},
|
|
2185
|
+
WorkflowHistoryEntry: {
|
|
2186
|
+
type: "object",
|
|
2187
|
+
required: [
|
|
2188
|
+
"collection",
|
|
2189
|
+
"documentId",
|
|
2190
|
+
"transition",
|
|
2191
|
+
"from",
|
|
2192
|
+
"to",
|
|
2193
|
+
"revision",
|
|
2194
|
+
"createdAt"
|
|
2195
|
+
],
|
|
2196
|
+
properties: {
|
|
2197
|
+
id: { type: "string" },
|
|
2198
|
+
collection: { type: "string" },
|
|
2199
|
+
documentId: { type: "string" },
|
|
2200
|
+
transition: { type: "string" },
|
|
2201
|
+
from: { type: "string" },
|
|
2202
|
+
to: { type: "string" },
|
|
2203
|
+
revision: { type: "integer" },
|
|
2204
|
+
comment: { type: "string", nullable: true },
|
|
2205
|
+
actorId: { type: "string", nullable: true },
|
|
2206
|
+
createdAt: { type: "string", format: "date-time" }
|
|
2207
|
+
}
|
|
2208
|
+
},
|
|
2209
|
+
Error: {
|
|
2210
|
+
type: "object",
|
|
2211
|
+
properties: {
|
|
2212
|
+
message: { type: "string" },
|
|
2213
|
+
errors: {
|
|
2214
|
+
type: "array",
|
|
2215
|
+
items: { type: "object", additionalProperties: true }
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
},
|
|
2219
|
+
AuthCredentials: {
|
|
2220
|
+
type: "object",
|
|
2221
|
+
required: ["email", "password"],
|
|
2222
|
+
properties: {
|
|
2223
|
+
email: { type: "string", format: "email" },
|
|
2224
|
+
password: { type: "string" }
|
|
2225
|
+
}
|
|
2226
|
+
},
|
|
2227
|
+
TokenResponse: {
|
|
2228
|
+
type: "object",
|
|
2229
|
+
properties: {
|
|
2230
|
+
token: { type: "string" },
|
|
2231
|
+
user: { type: "object", additionalProperties: true }
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
},
|
|
2119
2235
|
securitySchemes: {
|
|
2120
2236
|
ApiKeyAuth: {
|
|
2121
2237
|
type: "apiKey",
|
|
@@ -2127,6 +2243,98 @@ function generateOpenApi(config) {
|
|
|
2127
2243
|
paths: {},
|
|
2128
2244
|
security: [{ ApiKeyAuth: [] }]
|
|
2129
2245
|
};
|
|
2246
|
+
spec.paths["/api/schemas"] = {
|
|
2247
|
+
get: {
|
|
2248
|
+
tags: ["System"],
|
|
2249
|
+
summary: "Get the serialized Dyrected schema",
|
|
2250
|
+
security: [],
|
|
2251
|
+
responses: {
|
|
2252
|
+
200: { description: "Collection and global schema definitions" }
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
spec.paths["/api/openapi.json"] = {
|
|
2257
|
+
get: {
|
|
2258
|
+
tags: ["System"],
|
|
2259
|
+
summary: "Get the OpenAPI specification",
|
|
2260
|
+
security: [],
|
|
2261
|
+
responses: { 200: { description: "OpenAPI 3.0 document" } }
|
|
2262
|
+
}
|
|
2263
|
+
};
|
|
2264
|
+
spec.paths["/api/docs"] = {
|
|
2265
|
+
get: {
|
|
2266
|
+
tags: ["System"],
|
|
2267
|
+
summary: "Open interactive API documentation",
|
|
2268
|
+
security: [],
|
|
2269
|
+
responses: { 200: { description: "Swagger UI HTML" } }
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
spec.paths["/api/dyrected/options/{collection}/{field}"] = {
|
|
2273
|
+
get: {
|
|
2274
|
+
tags: ["System"],
|
|
2275
|
+
summary: "Resolve dynamic field options",
|
|
2276
|
+
parameters: [
|
|
2277
|
+
{
|
|
2278
|
+
name: "collection",
|
|
2279
|
+
in: "path",
|
|
2280
|
+
required: true,
|
|
2281
|
+
schema: { type: "string" }
|
|
2282
|
+
},
|
|
2283
|
+
{
|
|
2284
|
+
name: "field",
|
|
2285
|
+
in: "path",
|
|
2286
|
+
required: true,
|
|
2287
|
+
schema: { type: "string" }
|
|
2288
|
+
}
|
|
2289
|
+
],
|
|
2290
|
+
responses: { 200: { description: "Resolved option items" } }
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
spec.paths["/api/preferences/{key}"] = {
|
|
2294
|
+
get: {
|
|
2295
|
+
tags: ["Preferences"],
|
|
2296
|
+
summary: "Get an authenticated user preference",
|
|
2297
|
+
parameters: [
|
|
2298
|
+
{ name: "key", in: "path", required: true, schema: { type: "string" } }
|
|
2299
|
+
],
|
|
2300
|
+
responses: { 200: { description: "Preference value" } }
|
|
2301
|
+
},
|
|
2302
|
+
put: {
|
|
2303
|
+
tags: ["Preferences"],
|
|
2304
|
+
summary: "Set an authenticated user preference",
|
|
2305
|
+
parameters: [
|
|
2306
|
+
{ name: "key", in: "path", required: true, schema: { type: "string" } }
|
|
2307
|
+
],
|
|
2308
|
+
requestBody: {
|
|
2309
|
+
required: true,
|
|
2310
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
2311
|
+
},
|
|
2312
|
+
responses: { 200: { description: "Updated preference" } }
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
spec.paths["/api/preview-token"] = {
|
|
2316
|
+
post: {
|
|
2317
|
+
tags: ["Preview"],
|
|
2318
|
+
summary: "Create a preview token",
|
|
2319
|
+
responses: { 200: { description: "Short-lived preview token" } }
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
spec.paths["/api/preview-data"] = {
|
|
2323
|
+
get: {
|
|
2324
|
+
tags: ["Preview"],
|
|
2325
|
+
summary: "Resolve preview data from a token",
|
|
2326
|
+
security: [],
|
|
2327
|
+
parameters: [
|
|
2328
|
+
{
|
|
2329
|
+
name: "token",
|
|
2330
|
+
in: "query",
|
|
2331
|
+
required: true,
|
|
2332
|
+
schema: { type: "string" }
|
|
2333
|
+
}
|
|
2334
|
+
],
|
|
2335
|
+
responses: { 200: { description: "Preview document data" } }
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2130
2338
|
for (const collection of config.collections) {
|
|
2131
2339
|
spec.components.schemas[collection.slug] = collectionToSchema(collection);
|
|
2132
2340
|
}
|
|
@@ -2142,10 +2350,28 @@ function generateOpenApi(config) {
|
|
|
2142
2350
|
tags: ["Collections"],
|
|
2143
2351
|
summary: `Find ${labels.plural}`,
|
|
2144
2352
|
parameters: [
|
|
2145
|
-
{
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2353
|
+
{
|
|
2354
|
+
name: "limit",
|
|
2355
|
+
in: "query",
|
|
2356
|
+
schema: { type: "integer", default: 10 }
|
|
2357
|
+
},
|
|
2358
|
+
{
|
|
2359
|
+
name: "page",
|
|
2360
|
+
in: "query",
|
|
2361
|
+
schema: { type: "integer", default: 1 }
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
name: "where",
|
|
2365
|
+
in: "query",
|
|
2366
|
+
schema: { type: "string" },
|
|
2367
|
+
description: "JSON filter"
|
|
2368
|
+
},
|
|
2369
|
+
{
|
|
2370
|
+
name: "sort",
|
|
2371
|
+
in: "query",
|
|
2372
|
+
schema: { type: "string" },
|
|
2373
|
+
description: "Sort field (e.g. -createdAt)"
|
|
2374
|
+
}
|
|
2149
2375
|
],
|
|
2150
2376
|
responses: {
|
|
2151
2377
|
200: {
|
|
@@ -2155,7 +2381,10 @@ function generateOpenApi(config) {
|
|
|
2155
2381
|
schema: {
|
|
2156
2382
|
type: "object",
|
|
2157
2383
|
properties: {
|
|
2158
|
-
docs: {
|
|
2384
|
+
docs: {
|
|
2385
|
+
type: "array",
|
|
2386
|
+
items: { $ref: `#/components/schemas/${slug}` }
|
|
2387
|
+
},
|
|
2159
2388
|
total: { type: "integer" },
|
|
2160
2389
|
limit: { type: "integer" },
|
|
2161
2390
|
page: { type: "integer" }
|
|
@@ -2193,7 +2422,14 @@ function generateOpenApi(config) {
|
|
|
2193
2422
|
get: {
|
|
2194
2423
|
tags: ["Collections"],
|
|
2195
2424
|
summary: `Get a single ${labels.singular}`,
|
|
2196
|
-
parameters: [
|
|
2425
|
+
parameters: [
|
|
2426
|
+
{
|
|
2427
|
+
name: "id",
|
|
2428
|
+
in: "path",
|
|
2429
|
+
required: true,
|
|
2430
|
+
schema: { type: "string" }
|
|
2431
|
+
}
|
|
2432
|
+
],
|
|
2197
2433
|
responses: {
|
|
2198
2434
|
200: {
|
|
2199
2435
|
description: "Success",
|
|
@@ -2208,7 +2444,14 @@ function generateOpenApi(config) {
|
|
|
2208
2444
|
patch: {
|
|
2209
2445
|
tags: ["Collections"],
|
|
2210
2446
|
summary: `Update ${labels.singular}`,
|
|
2211
|
-
parameters: [
|
|
2447
|
+
parameters: [
|
|
2448
|
+
{
|
|
2449
|
+
name: "id",
|
|
2450
|
+
in: "path",
|
|
2451
|
+
required: true,
|
|
2452
|
+
schema: { type: "string" }
|
|
2453
|
+
}
|
|
2454
|
+
],
|
|
2212
2455
|
requestBody: {
|
|
2213
2456
|
required: true,
|
|
2214
2457
|
content: {
|
|
@@ -2231,12 +2474,288 @@ function generateOpenApi(config) {
|
|
|
2231
2474
|
delete: {
|
|
2232
2475
|
tags: ["Collections"],
|
|
2233
2476
|
summary: `Delete ${labels.singular}`,
|
|
2234
|
-
parameters: [
|
|
2477
|
+
parameters: [
|
|
2478
|
+
{
|
|
2479
|
+
name: "id",
|
|
2480
|
+
in: "path",
|
|
2481
|
+
required: true,
|
|
2482
|
+
schema: { type: "string" }
|
|
2483
|
+
}
|
|
2484
|
+
],
|
|
2235
2485
|
responses: {
|
|
2236
2486
|
204: { description: "Deleted" }
|
|
2237
2487
|
}
|
|
2238
2488
|
}
|
|
2239
2489
|
};
|
|
2490
|
+
spec.paths[`${path}/delete-many`] = {
|
|
2491
|
+
delete: {
|
|
2492
|
+
tags: ["Collections"],
|
|
2493
|
+
summary: `Delete multiple ${labels.plural}`,
|
|
2494
|
+
requestBody: {
|
|
2495
|
+
required: true,
|
|
2496
|
+
content: {
|
|
2497
|
+
"application/json": {
|
|
2498
|
+
schema: {
|
|
2499
|
+
type: "object",
|
|
2500
|
+
required: ["ids"],
|
|
2501
|
+
properties: {
|
|
2502
|
+
ids: { type: "array", items: { type: "string" } }
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
},
|
|
2508
|
+
responses: { 200: { description: "Deleted and failed document IDs" } }
|
|
2509
|
+
}
|
|
2510
|
+
};
|
|
2511
|
+
spec.paths[`${path}/seed`] = {
|
|
2512
|
+
post: {
|
|
2513
|
+
tags: ["Collections"],
|
|
2514
|
+
summary: `Seed initial ${labels.plural}`,
|
|
2515
|
+
responses: { 200: { description: "Seeded documents" } }
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
if (collection.upload) {
|
|
2519
|
+
spec.paths[`${path}/media`] = {
|
|
2520
|
+
get: {
|
|
2521
|
+
tags: ["Media"],
|
|
2522
|
+
summary: `List ${labels.plural}`,
|
|
2523
|
+
responses: { 200: { description: "Paginated media documents" } }
|
|
2524
|
+
},
|
|
2525
|
+
post: {
|
|
2526
|
+
tags: ["Media"],
|
|
2527
|
+
summary: `Upload ${labels.singular}`,
|
|
2528
|
+
requestBody: {
|
|
2529
|
+
required: true,
|
|
2530
|
+
content: {
|
|
2531
|
+
"multipart/form-data": {
|
|
2532
|
+
schema: {
|
|
2533
|
+
type: "object",
|
|
2534
|
+
required: ["file"],
|
|
2535
|
+
properties: { file: { type: "string", format: "binary" } }
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
},
|
|
2540
|
+
responses: { 201: { description: "Uploaded media document" } }
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
spec.paths[`${path}/media/{filename}`] = {
|
|
2544
|
+
get: {
|
|
2545
|
+
tags: ["Media"],
|
|
2546
|
+
summary: `Serve ${labels.singular} bytes`,
|
|
2547
|
+
parameters: [
|
|
2548
|
+
{
|
|
2549
|
+
name: "filename",
|
|
2550
|
+
in: "path",
|
|
2551
|
+
required: true,
|
|
2552
|
+
schema: { type: "string" }
|
|
2553
|
+
}
|
|
2554
|
+
],
|
|
2555
|
+
security: [],
|
|
2556
|
+
responses: {
|
|
2557
|
+
200: { description: "Stored file" },
|
|
2558
|
+
404: { description: "File not found" }
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
if (collection.auth) {
|
|
2564
|
+
const publicAuthPost = (summary) => ({
|
|
2565
|
+
tags: ["Authentication"],
|
|
2566
|
+
summary,
|
|
2567
|
+
security: [],
|
|
2568
|
+
requestBody: {
|
|
2569
|
+
required: true,
|
|
2570
|
+
content: {
|
|
2571
|
+
"application/json": {
|
|
2572
|
+
schema: { type: "object", additionalProperties: true }
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
},
|
|
2576
|
+
responses: {
|
|
2577
|
+
200: { description: "Success" },
|
|
2578
|
+
400: { description: "Invalid request" }
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2581
|
+
spec.paths[`${path}/login`] = {
|
|
2582
|
+
post: {
|
|
2583
|
+
...publicAuthPost(`Log in to ${labels.plural}`),
|
|
2584
|
+
requestBody: {
|
|
2585
|
+
required: true,
|
|
2586
|
+
content: {
|
|
2587
|
+
"application/json": {
|
|
2588
|
+
schema: { $ref: "#/components/schemas/AuthCredentials" }
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
},
|
|
2592
|
+
responses: {
|
|
2593
|
+
200: {
|
|
2594
|
+
description: "Authenticated",
|
|
2595
|
+
content: {
|
|
2596
|
+
"application/json": {
|
|
2597
|
+
schema: { $ref: "#/components/schemas/TokenResponse" }
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
},
|
|
2601
|
+
401: { description: "Invalid credentials" }
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2605
|
+
spec.paths[`${path}/logout`] = {
|
|
2606
|
+
post: {
|
|
2607
|
+
tags: ["Authentication"],
|
|
2608
|
+
summary: `Log out of ${labels.plural}`,
|
|
2609
|
+
responses: { 200: { description: "Logged out" } }
|
|
2610
|
+
}
|
|
2611
|
+
};
|
|
2612
|
+
spec.paths[`${path}/init`] = {
|
|
2613
|
+
get: {
|
|
2614
|
+
tags: ["Authentication"],
|
|
2615
|
+
summary: `Get ${labels.plural} initialization state`,
|
|
2616
|
+
security: [],
|
|
2617
|
+
responses: { 200: { description: "Initialization state" } }
|
|
2618
|
+
}
|
|
2619
|
+
};
|
|
2620
|
+
spec.paths[`${path}/first-user`] = {
|
|
2621
|
+
post: publicAuthPost(`Register the first ${labels.singular}`)
|
|
2622
|
+
};
|
|
2623
|
+
spec.paths[`${path}/me`] = {
|
|
2624
|
+
get: {
|
|
2625
|
+
tags: ["Authentication"],
|
|
2626
|
+
summary: `Get the current ${labels.singular}`,
|
|
2627
|
+
responses: { 200: { description: "Authenticated user" } }
|
|
2628
|
+
}
|
|
2629
|
+
};
|
|
2630
|
+
spec.paths[`${path}/refresh-token`] = {
|
|
2631
|
+
post: {
|
|
2632
|
+
tags: ["Authentication"],
|
|
2633
|
+
summary: "Refresh an authentication token",
|
|
2634
|
+
responses: { 200: { description: "Refreshed token" } }
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
spec.paths[`${path}/forgot-password`] = {
|
|
2638
|
+
post: publicAuthPost("Request a password reset")
|
|
2639
|
+
};
|
|
2640
|
+
spec.paths[`${path}/reset-password`] = {
|
|
2641
|
+
post: publicAuthPost("Reset a password")
|
|
2642
|
+
};
|
|
2643
|
+
spec.paths[`${path}/invite`] = {
|
|
2644
|
+
post: {
|
|
2645
|
+
tags: ["Authentication"],
|
|
2646
|
+
summary: `Invite a ${labels.singular}`,
|
|
2647
|
+
responses: { 200: { description: "Invitation sent" } }
|
|
2648
|
+
}
|
|
2649
|
+
};
|
|
2650
|
+
spec.paths[`${path}/accept-invite`] = {
|
|
2651
|
+
post: publicAuthPost("Accept an invitation")
|
|
2652
|
+
};
|
|
2653
|
+
spec.paths[`${path}/{id}/change-password`] = {
|
|
2654
|
+
post: {
|
|
2655
|
+
tags: ["Authentication"],
|
|
2656
|
+
summary: `Change a ${labels.singular} password`,
|
|
2657
|
+
parameters: [
|
|
2658
|
+
{
|
|
2659
|
+
name: "id",
|
|
2660
|
+
in: "path",
|
|
2661
|
+
required: true,
|
|
2662
|
+
schema: { type: "string" }
|
|
2663
|
+
}
|
|
2664
|
+
],
|
|
2665
|
+
responses: { 200: { description: "Password changed" } }
|
|
2666
|
+
}
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
if (collection.workflow) {
|
|
2670
|
+
spec.paths[`${path}/{id}/transitions/{transition}`] = {
|
|
2671
|
+
post: {
|
|
2672
|
+
tags: ["Workflows"],
|
|
2673
|
+
summary: `Transition ${labels.singular} workflow`,
|
|
2674
|
+
parameters: [
|
|
2675
|
+
{
|
|
2676
|
+
name: "id",
|
|
2677
|
+
in: "path",
|
|
2678
|
+
required: true,
|
|
2679
|
+
schema: { type: "string" }
|
|
2680
|
+
},
|
|
2681
|
+
{
|
|
2682
|
+
name: "transition",
|
|
2683
|
+
in: "path",
|
|
2684
|
+
required: true,
|
|
2685
|
+
schema: {
|
|
2686
|
+
type: "string",
|
|
2687
|
+
enum: collection.workflow.transitions.map((item) => item.name)
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
],
|
|
2691
|
+
requestBody: {
|
|
2692
|
+
content: {
|
|
2693
|
+
"application/json": {
|
|
2694
|
+
schema: {
|
|
2695
|
+
$ref: "#/components/schemas/WorkflowTransitionRequest"
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
},
|
|
2700
|
+
responses: {
|
|
2701
|
+
200: {
|
|
2702
|
+
description: "Transition applied",
|
|
2703
|
+
content: {
|
|
2704
|
+
"application/json": {
|
|
2705
|
+
schema: { $ref: `#/components/schemas/${slug}` }
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
},
|
|
2709
|
+
400: { description: "Required transition input is missing" },
|
|
2710
|
+
403: { description: "Transition capability denied" },
|
|
2711
|
+
409: { description: "Invalid state or stale revision" }
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
spec.paths[`${path}/{id}/workflow-history`] = {
|
|
2716
|
+
get: {
|
|
2717
|
+
tags: ["Workflows"],
|
|
2718
|
+
summary: `Get ${labels.singular} workflow history`,
|
|
2719
|
+
parameters: [
|
|
2720
|
+
{
|
|
2721
|
+
name: "id",
|
|
2722
|
+
in: "path",
|
|
2723
|
+
required: true,
|
|
2724
|
+
schema: { type: "string" }
|
|
2725
|
+
},
|
|
2726
|
+
{
|
|
2727
|
+
name: "limit",
|
|
2728
|
+
in: "query",
|
|
2729
|
+
schema: { type: "integer", default: 50, maximum: 100 }
|
|
2730
|
+
}
|
|
2731
|
+
],
|
|
2732
|
+
responses: {
|
|
2733
|
+
200: {
|
|
2734
|
+
description: "Workflow transition history",
|
|
2735
|
+
content: {
|
|
2736
|
+
"application/json": {
|
|
2737
|
+
schema: {
|
|
2738
|
+
type: "object",
|
|
2739
|
+
properties: {
|
|
2740
|
+
docs: {
|
|
2741
|
+
type: "array",
|
|
2742
|
+
items: {
|
|
2743
|
+
$ref: "#/components/schemas/WorkflowHistoryEntry"
|
|
2744
|
+
}
|
|
2745
|
+
},
|
|
2746
|
+
total: { type: "integer" },
|
|
2747
|
+
limit: { type: "integer" },
|
|
2748
|
+
page: { type: "integer" }
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
},
|
|
2754
|
+
403: { description: "Collection read access denied" }
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2240
2759
|
}
|
|
2241
2760
|
for (const global of config.globals) {
|
|
2242
2761
|
const slug = global.slug;
|
|
@@ -2279,45 +2798,31 @@ function generateOpenApi(config) {
|
|
|
2279
2798
|
}
|
|
2280
2799
|
}
|
|
2281
2800
|
};
|
|
2801
|
+
spec.paths[`${path}/seed`] = {
|
|
2802
|
+
post: {
|
|
2803
|
+
tags: ["Globals"],
|
|
2804
|
+
summary: `Seed ${global.label || slug}`,
|
|
2805
|
+
responses: { 200: { description: "Seeded global" } }
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2282
2808
|
}
|
|
2283
2809
|
if (config.storage) {
|
|
2284
|
-
spec.paths["/api/media"] = {
|
|
2810
|
+
spec.paths["/api/media/{filename}"] = {
|
|
2285
2811
|
get: {
|
|
2286
2812
|
tags: ["Media"],
|
|
2287
|
-
summary: "
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
properties: {
|
|
2296
|
-
docs: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
},
|
|
2304
|
-
post: {
|
|
2305
|
-
tags: ["Media"],
|
|
2306
|
-
summary: "Upload Media",
|
|
2307
|
-
requestBody: {
|
|
2308
|
-
content: {
|
|
2309
|
-
"multipart/form-data": {
|
|
2310
|
-
schema: {
|
|
2311
|
-
type: "object",
|
|
2312
|
-
properties: {
|
|
2313
|
-
file: { type: "string", format: "binary" }
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2813
|
+
summary: "Serve a stored file",
|
|
2814
|
+
security: [],
|
|
2815
|
+
parameters: [
|
|
2816
|
+
{
|
|
2817
|
+
name: "filename",
|
|
2818
|
+
in: "path",
|
|
2819
|
+
required: true,
|
|
2820
|
+
schema: { type: "string" }
|
|
2317
2821
|
}
|
|
2318
|
-
|
|
2822
|
+
],
|
|
2319
2823
|
responses: {
|
|
2320
|
-
|
|
2824
|
+
200: { description: "Stored file bytes" },
|
|
2825
|
+
404: { description: "File not found" }
|
|
2321
2826
|
}
|
|
2322
2827
|
}
|
|
2323
2828
|
};
|
|
@@ -2332,6 +2837,7 @@ function collectionToSchema(collection) {
|
|
|
2332
2837
|
id: { type: "string" },
|
|
2333
2838
|
createdAt: { type: "string", format: "date-time" },
|
|
2334
2839
|
updatedAt: { type: "string", format: "date-time" },
|
|
2840
|
+
...collection.workflow ? { _workflow: { $ref: "#/components/schemas/WorkflowMetadata" } } : {},
|
|
2335
2841
|
...properties
|
|
2336
2842
|
},
|
|
2337
2843
|
required: ["id", ...required]
|
|
@@ -2349,7 +2855,13 @@ function fieldsToProperties(fields) {
|
|
|
2349
2855
|
const props = {};
|
|
2350
2856
|
const required = [];
|
|
2351
2857
|
for (const field of fields) {
|
|
2352
|
-
if (
|
|
2858
|
+
if (field.type === "row") {
|
|
2859
|
+
const nested = fieldsToProperties(field.fields || []);
|
|
2860
|
+
Object.assign(props, nested.properties);
|
|
2861
|
+
required.push(...nested.required);
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
2864
|
+
if (!field.name) continue;
|
|
2353
2865
|
props[field.name] = fieldToSchema(field);
|
|
2354
2866
|
if (field.required) {
|
|
2355
2867
|
required.push(field.name);
|
|
@@ -2363,7 +2875,17 @@ function fieldToSchema(field) {
|
|
|
2363
2875
|
case "text":
|
|
2364
2876
|
case "textarea":
|
|
2365
2877
|
case "email":
|
|
2878
|
+
schema = { type: "string" };
|
|
2879
|
+
break;
|
|
2366
2880
|
case "url":
|
|
2881
|
+
schema = {
|
|
2882
|
+
oneOf: [
|
|
2883
|
+
{ type: "string" },
|
|
2884
|
+
{ type: "object", additionalProperties: true }
|
|
2885
|
+
]
|
|
2886
|
+
};
|
|
2887
|
+
break;
|
|
2888
|
+
case "icon":
|
|
2367
2889
|
schema = { type: "string" };
|
|
2368
2890
|
break;
|
|
2369
2891
|
case "number":
|
|
@@ -2373,20 +2895,45 @@ function fieldToSchema(field) {
|
|
|
2373
2895
|
schema = { type: "boolean" };
|
|
2374
2896
|
break;
|
|
2375
2897
|
case "date":
|
|
2898
|
+
schema = { type: "string", format: "date" };
|
|
2899
|
+
break;
|
|
2900
|
+
case "datetime":
|
|
2376
2901
|
schema = { type: "string", format: "date-time" };
|
|
2377
2902
|
break;
|
|
2903
|
+
case "time":
|
|
2904
|
+
schema = { type: "string", format: "time" };
|
|
2905
|
+
break;
|
|
2378
2906
|
case "select":
|
|
2379
2907
|
case "radio":
|
|
2380
|
-
schema = {
|
|
2908
|
+
schema = {
|
|
2909
|
+
type: "string",
|
|
2910
|
+
enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0
|
|
2911
|
+
};
|
|
2381
2912
|
break;
|
|
2382
2913
|
case "multiSelect":
|
|
2383
2914
|
schema = {
|
|
2384
2915
|
type: "array",
|
|
2385
|
-
items: {
|
|
2916
|
+
items: {
|
|
2917
|
+
type: "string",
|
|
2918
|
+
enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0
|
|
2919
|
+
}
|
|
2386
2920
|
};
|
|
2387
2921
|
break;
|
|
2388
2922
|
case "relationship":
|
|
2389
|
-
|
|
2923
|
+
case "image": {
|
|
2924
|
+
const valueSchema = {
|
|
2925
|
+
type: "string",
|
|
2926
|
+
description: `ID of a ${field.relationTo} record`
|
|
2927
|
+
};
|
|
2928
|
+
schema = field.hasMany ? { type: "array", items: valueSchema } : valueSchema;
|
|
2929
|
+
break;
|
|
2930
|
+
}
|
|
2931
|
+
case "join":
|
|
2932
|
+
schema = {
|
|
2933
|
+
type: "array",
|
|
2934
|
+
readOnly: true,
|
|
2935
|
+
items: { type: "object", additionalProperties: true }
|
|
2936
|
+
};
|
|
2390
2937
|
break;
|
|
2391
2938
|
case "object": {
|
|
2392
2939
|
const { properties, required } = fieldsToProperties(field.fields || []);
|
|
@@ -2395,7 +2942,10 @@ function fieldToSchema(field) {
|
|
|
2395
2942
|
}
|
|
2396
2943
|
case "array": {
|
|
2397
2944
|
const { properties, required } = fieldsToProperties(field.fields || []);
|
|
2398
|
-
schema = {
|
|
2945
|
+
schema = {
|
|
2946
|
+
type: "array",
|
|
2947
|
+
items: { type: "object", properties, required }
|
|
2948
|
+
};
|
|
2399
2949
|
break;
|
|
2400
2950
|
}
|
|
2401
2951
|
case "json":
|
|
@@ -2476,20 +3026,6 @@ function getSwaggerHtml(specUrl = "/api/openapi.json") {
|
|
|
2476
3026
|
`;
|
|
2477
3027
|
}
|
|
2478
3028
|
|
|
2479
|
-
// src/auth/jexl.ts
|
|
2480
|
-
var import_jexl = __toESM(require("jexl"), 1);
|
|
2481
|
-
async function evaluateAccess(expression, context) {
|
|
2482
|
-
if (expression === void 0 || expression === null) return false;
|
|
2483
|
-
if (typeof expression === "boolean") return expression;
|
|
2484
|
-
try {
|
|
2485
|
-
const result = await import_jexl.default.eval(expression, context);
|
|
2486
|
-
return !!result;
|
|
2487
|
-
} catch (err) {
|
|
2488
|
-
console.error("[dyrected/core] Jexl evaluation failed:", err);
|
|
2489
|
-
return false;
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
3029
|
// src/router.ts
|
|
2494
3030
|
function accessGate(target, action) {
|
|
2495
3031
|
return async (c, next) => {
|