@dyrected/core 2.5.59 → 2.5.61
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/app-config-9hs8DRED.d.cts +1718 -0
- package/dist/app-config-9hs8DRED.d.ts +1718 -0
- package/dist/app-config-C0twGIaF.d.cts +1597 -0
- package/dist/app-config-C0twGIaF.d.ts +1597 -0
- package/dist/app-config-C0uxEU7Q.d.cts +1874 -0
- package/dist/app-config-C0uxEU7Q.d.ts +1874 -0
- package/dist/app-config-CBOn8IyZ.d.cts +1838 -0
- package/dist/app-config-CBOn8IyZ.d.ts +1838 -0
- package/dist/app-config-CstocO9M.d.cts +1580 -0
- package/dist/app-config-CstocO9M.d.ts +1580 -0
- package/dist/app-config-CwaU1de2.d.cts +1868 -0
- package/dist/app-config-CwaU1de2.d.ts +1868 -0
- package/dist/app-config-DIwvzbpe.d.cts +1555 -0
- package/dist/app-config-DIwvzbpe.d.ts +1555 -0
- package/dist/app-config-DVdSospO.d.cts +1842 -0
- package/dist/app-config-DVdSospO.d.ts +1842 -0
- package/dist/app-config-Dkndq5ni.d.cts +1580 -0
- package/dist/app-config-Dkndq5ni.d.ts +1580 -0
- package/dist/app-config-P-SF5nnR.d.cts +1568 -0
- package/dist/app-config-P-SF5nnR.d.ts +1568 -0
- package/dist/app-config-ouBRb6Bf.d.cts +1716 -0
- package/dist/app-config-ouBRb6Bf.d.ts +1716 -0
- package/dist/chunk-2WODKOUB.js +1627 -0
- package/dist/chunk-35NM2WPO.js +1658 -0
- package/dist/chunk-3CMG2BH2.js +1618 -0
- package/dist/chunk-CUOPCOU2.js +1644 -0
- package/dist/chunk-DCY7QHRB.js +1637 -0
- package/dist/chunk-EOW4YMAV.js +1637 -0
- package/dist/chunk-JFA22YFB.js +1625 -0
- package/dist/chunk-WF5A6PDJ.js +1618 -0
- package/dist/index.cjs +209 -47
- package/dist/index.d.cts +79 -4
- package/dist/index.d.ts +79 -4
- package/dist/index.js +42 -4
- package/dist/server.cjs +388 -56
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +224 -13
- package/package.json +1 -1
package/dist/server.cjs
CHANGED
|
@@ -627,12 +627,16 @@ async function resolveAccess(config, access, args) {
|
|
|
627
627
|
}
|
|
628
628
|
}
|
|
629
629
|
if (isNamedAccessPolicy(access)) {
|
|
630
|
-
const
|
|
631
|
-
if (
|
|
630
|
+
const policy = config.accessPolicies?.[access.policy];
|
|
631
|
+
if (policy === void 0) {
|
|
632
632
|
console.error(`[dyrected/core] Unknown access policy "${access.policy}".`);
|
|
633
633
|
return false;
|
|
634
634
|
}
|
|
635
|
+
if (typeof policy === "string" || typeof policy === "boolean") {
|
|
636
|
+
return evaluateAccess(policy, args);
|
|
637
|
+
}
|
|
635
638
|
try {
|
|
639
|
+
const resolver = policy;
|
|
636
640
|
return await resolver({ ...args, params: access.params });
|
|
637
641
|
} catch (err) {
|
|
638
642
|
console.error(`[dyrected/core] Access policy "${access.policy}" failed:`, err);
|
|
@@ -775,14 +779,15 @@ async function applyFieldWriteAccess(context, data) {
|
|
|
775
779
|
continue;
|
|
776
780
|
}
|
|
777
781
|
if (!field.name || !(field.name in result)) continue;
|
|
778
|
-
const
|
|
782
|
+
const writeRule = context.operation === "create" ? field.access?.create ?? field.access?.update : field.access?.update;
|
|
783
|
+
const canWrite = await resolveBooleanAccess(context.config, writeRule, {
|
|
779
784
|
user: context.user,
|
|
780
785
|
req: context.req,
|
|
781
786
|
doc: context.doc,
|
|
782
787
|
data: context.data,
|
|
783
788
|
id: resolveFieldAccessId(context)
|
|
784
789
|
});
|
|
785
|
-
if (!
|
|
790
|
+
if (!canWrite) {
|
|
786
791
|
delete result[field.name];
|
|
787
792
|
continue;
|
|
788
793
|
}
|
|
@@ -822,6 +827,20 @@ function mergeWhereConstraint(where, constraint) {
|
|
|
822
827
|
// src/workflows.ts
|
|
823
828
|
var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
|
|
824
829
|
var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
|
|
830
|
+
function simplePublishingWorkflow() {
|
|
831
|
+
return {
|
|
832
|
+
initialState: "draft",
|
|
833
|
+
draftState: "draft",
|
|
834
|
+
states: [
|
|
835
|
+
{ name: "draft", label: "Draft", color: "neutral" },
|
|
836
|
+
{ name: "published", label: "Published", color: "success", published: true }
|
|
837
|
+
],
|
|
838
|
+
transitions: [
|
|
839
|
+
{ name: "publish", label: "Publish", from: "draft", to: "published" },
|
|
840
|
+
{ name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
|
|
841
|
+
]
|
|
842
|
+
};
|
|
843
|
+
}
|
|
825
844
|
function publicMetadata(meta) {
|
|
826
845
|
const { availableTransitions: _availableTransitions, ...safe } = meta;
|
|
827
846
|
return safe;
|
|
@@ -855,9 +874,23 @@ function initializeWorkflowDocument(data, workflow) {
|
|
|
855
874
|
__workflow: { state: workflow.initialState, revision: 1 }
|
|
856
875
|
};
|
|
857
876
|
}
|
|
877
|
+
function publishedStateName(workflow) {
|
|
878
|
+
return workflow.states.find((state) => state.published)?.name ?? workflow.states[workflow.states.length - 1]?.name ?? workflow.initialState;
|
|
879
|
+
}
|
|
858
880
|
function materializeWorkflowDocument(doc, workflow, user) {
|
|
859
881
|
const meta = doc.__workflow;
|
|
860
|
-
if (!meta)
|
|
882
|
+
if (!meta) {
|
|
883
|
+
const { __published: _legacyPublished, __workflow: _legacyWorkflow, ...working2 } = doc;
|
|
884
|
+
const state = publishedStateName(workflow);
|
|
885
|
+
return {
|
|
886
|
+
...working2,
|
|
887
|
+
_workflow: {
|
|
888
|
+
state,
|
|
889
|
+
revision: 1,
|
|
890
|
+
availableTransitions: availableWorkflowTransitions(workflow, state, user).map((item) => item.name)
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
861
894
|
const { __published, __workflow, ...working } = doc;
|
|
862
895
|
if (!canViewWorkflowDraft(workflow, user)) {
|
|
863
896
|
if (!__published || typeof __published !== "object") return null;
|
|
@@ -1117,9 +1150,9 @@ var CollectionController = class {
|
|
|
1117
1150
|
});
|
|
1118
1151
|
}
|
|
1119
1152
|
const readonlyDb = createReadonlyDb(db);
|
|
1120
|
-
const limit = Number(c.req.query("limit")) || 10;
|
|
1153
|
+
const limit = Math.min(Number(c.req.query("limit")) || 10, 100);
|
|
1121
1154
|
const page = Number(c.req.query("page")) || 1;
|
|
1122
|
-
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) :
|
|
1155
|
+
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
|
|
1123
1156
|
const sort = c.req.query("sort") || void 0;
|
|
1124
1157
|
const user = c.get("user");
|
|
1125
1158
|
let where = void 0;
|
|
@@ -1165,7 +1198,8 @@ var CollectionController = class {
|
|
|
1165
1198
|
limit,
|
|
1166
1199
|
page,
|
|
1167
1200
|
sort,
|
|
1168
|
-
where
|
|
1201
|
+
where,
|
|
1202
|
+
fields: this.collection.fields
|
|
1169
1203
|
});
|
|
1170
1204
|
if (result.total === 0 && this.collection.initialData && !where && page === 1) {
|
|
1171
1205
|
console.log(`[dyrected/core] Auto-seeding collection "${this.collection.slug}" from config.initialData`);
|
|
@@ -1177,7 +1211,8 @@ var CollectionController = class {
|
|
|
1177
1211
|
limit,
|
|
1178
1212
|
page,
|
|
1179
1213
|
sort,
|
|
1180
|
-
where
|
|
1214
|
+
where,
|
|
1215
|
+
fields: this.collection.fields
|
|
1181
1216
|
});
|
|
1182
1217
|
}
|
|
1183
1218
|
result.docs = result.docs.map((doc) => this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc).filter((doc) => doc !== null);
|
|
@@ -1235,7 +1270,7 @@ var CollectionController = class {
|
|
|
1235
1270
|
}
|
|
1236
1271
|
const readonlyDb = createReadonlyDb(db);
|
|
1237
1272
|
const id = c.req.param("id");
|
|
1238
|
-
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) :
|
|
1273
|
+
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
|
|
1239
1274
|
const user = c.get("user");
|
|
1240
1275
|
if (!id) return c.json({ message: "Missing ID" }, 400);
|
|
1241
1276
|
let doc = await db.findOne({ collection: this.collection.slug, id });
|
|
@@ -1324,7 +1359,8 @@ var CollectionController = class {
|
|
|
1324
1359
|
fields: this.collection.fields,
|
|
1325
1360
|
user,
|
|
1326
1361
|
req: this.toHookRequestContext(c),
|
|
1327
|
-
data
|
|
1362
|
+
data,
|
|
1363
|
+
operation: "create"
|
|
1328
1364
|
}, data);
|
|
1329
1365
|
data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
|
|
1330
1366
|
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
@@ -1419,7 +1455,8 @@ var CollectionController = class {
|
|
|
1419
1455
|
fields: this.collection.fields,
|
|
1420
1456
|
user,
|
|
1421
1457
|
req: this.toHookRequestContext(c),
|
|
1422
|
-
data
|
|
1458
|
+
data,
|
|
1459
|
+
operation: "create"
|
|
1423
1460
|
}, data);
|
|
1424
1461
|
data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
|
|
1425
1462
|
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
@@ -1841,7 +1878,7 @@ var GlobalController = class {
|
|
|
1841
1878
|
const db = config.db;
|
|
1842
1879
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1843
1880
|
const readonlyDb = createReadonlyDb(db);
|
|
1844
|
-
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) :
|
|
1881
|
+
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
|
|
1845
1882
|
const user = c.get("user");
|
|
1846
1883
|
let query = void 0;
|
|
1847
1884
|
const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
|
|
@@ -3060,6 +3097,150 @@ var PreviewController = class {
|
|
|
3060
3097
|
}
|
|
3061
3098
|
};
|
|
3062
3099
|
|
|
3100
|
+
// src/controllers/audit.controller.ts
|
|
3101
|
+
var AuditController = class {
|
|
3102
|
+
parseQuery(c) {
|
|
3103
|
+
const limit = Math.min(Number(c.req.query("limit")) || 50, 100);
|
|
3104
|
+
const page = Math.max(Number(c.req.query("page")) || 1, 1);
|
|
3105
|
+
const sort = c.req.query("sort") || "-timestamp";
|
|
3106
|
+
const whereRaw = c.req.query("where");
|
|
3107
|
+
if (!whereRaw) {
|
|
3108
|
+
return { limit, page, sort };
|
|
3109
|
+
}
|
|
3110
|
+
try {
|
|
3111
|
+
return {
|
|
3112
|
+
limit,
|
|
3113
|
+
page,
|
|
3114
|
+
sort,
|
|
3115
|
+
where: JSON.parse(decodeURIComponent(whereRaw))
|
|
3116
|
+
};
|
|
3117
|
+
} catch {
|
|
3118
|
+
return { limit, page, sort };
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
emptyResult(limit, page) {
|
|
3122
|
+
return {
|
|
3123
|
+
docs: [],
|
|
3124
|
+
total: 0,
|
|
3125
|
+
limit,
|
|
3126
|
+
page,
|
|
3127
|
+
totalPages: 0,
|
|
3128
|
+
hasNextPage: false,
|
|
3129
|
+
hasPrevPage: page > 1
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
async collectAccessibleDocumentIds(db, collection, constraint) {
|
|
3133
|
+
const ids = [];
|
|
3134
|
+
const limit = 100;
|
|
3135
|
+
let page = 1;
|
|
3136
|
+
while (true) {
|
|
3137
|
+
const result = await db.find({
|
|
3138
|
+
collection: collection.slug,
|
|
3139
|
+
where: constraint,
|
|
3140
|
+
limit,
|
|
3141
|
+
page
|
|
3142
|
+
});
|
|
3143
|
+
for (const doc of result.docs) {
|
|
3144
|
+
if (typeof doc?.id === "string" && doc.id.length > 0) {
|
|
3145
|
+
ids.push(doc.id);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
if (!result.hasNextPage || page >= result.totalPages) {
|
|
3149
|
+
break;
|
|
3150
|
+
}
|
|
3151
|
+
page += 1;
|
|
3152
|
+
}
|
|
3153
|
+
return ids;
|
|
3154
|
+
}
|
|
3155
|
+
async buildCollectionAuditScope(c, collection) {
|
|
3156
|
+
const config = c.get("config");
|
|
3157
|
+
const auditAccess = collection.access?.readAudit ?? collection.access?.read;
|
|
3158
|
+
const access = await resolveCollectionAccess(
|
|
3159
|
+
config,
|
|
3160
|
+
collection.slug,
|
|
3161
|
+
"read",
|
|
3162
|
+
auditAccess,
|
|
3163
|
+
{
|
|
3164
|
+
user: c.get("user"),
|
|
3165
|
+
req: toHookRequestContext(c.req)
|
|
3166
|
+
}
|
|
3167
|
+
);
|
|
3168
|
+
if (!access.allowed) {
|
|
3169
|
+
return { denied: true, where: {} };
|
|
3170
|
+
}
|
|
3171
|
+
let where = { collection: { equals: collection.slug } };
|
|
3172
|
+
if (access.constraint) {
|
|
3173
|
+
const ids = await this.collectAccessibleDocumentIds(config.db, collection, access.constraint);
|
|
3174
|
+
where = mergeWhereConstraint(where, { documentId: { in: ids } });
|
|
3175
|
+
}
|
|
3176
|
+
return { denied: false, where };
|
|
3177
|
+
}
|
|
3178
|
+
async getVisibleCollections(c) {
|
|
3179
|
+
const config = c.get("config");
|
|
3180
|
+
const collections = /* @__PURE__ */ new Map();
|
|
3181
|
+
for (const collection of config.collections) {
|
|
3182
|
+
collections.set(collection.slug, collection);
|
|
3183
|
+
}
|
|
3184
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
3185
|
+
if (config.onSchemaFetch && siteId) {
|
|
3186
|
+
const dynamic = await config.onSchemaFetch(siteId);
|
|
3187
|
+
for (const collection of dynamic.collections || []) {
|
|
3188
|
+
if (!collections.has(collection.slug)) {
|
|
3189
|
+
collections.set(collection.slug, collection);
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
return Array.from(collections.values());
|
|
3194
|
+
}
|
|
3195
|
+
async findForCollection(c, collection) {
|
|
3196
|
+
const config = c.get("config");
|
|
3197
|
+
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
3198
|
+
if (!collection.audit) return c.json({ message: "Audit is not enabled for this collection" }, 404);
|
|
3199
|
+
const query = this.parseQuery(c);
|
|
3200
|
+
const scope = await this.buildCollectionAuditScope(c, collection);
|
|
3201
|
+
if (scope.denied) {
|
|
3202
|
+
return c.json({ error: true, message: `Access denied: read on ${collection.slug}` }, 403);
|
|
3203
|
+
}
|
|
3204
|
+
const where = query.where ? mergeWhereConstraint(scope.where, query.where) : scope.where;
|
|
3205
|
+
const result = await config.db.find({
|
|
3206
|
+
collection: "__audit",
|
|
3207
|
+
where,
|
|
3208
|
+
limit: query.limit,
|
|
3209
|
+
page: query.page,
|
|
3210
|
+
sort: query.sort
|
|
3211
|
+
});
|
|
3212
|
+
return c.json(result);
|
|
3213
|
+
}
|
|
3214
|
+
async findAll(c) {
|
|
3215
|
+
const config = c.get("config");
|
|
3216
|
+
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
3217
|
+
const query = this.parseQuery(c);
|
|
3218
|
+
const collections = (await this.getVisibleCollections(c)).filter((collection) => collection.audit);
|
|
3219
|
+
const scopes = [];
|
|
3220
|
+
for (const collection of collections) {
|
|
3221
|
+
const scope = await this.buildCollectionAuditScope(c, collection);
|
|
3222
|
+
if (!scope.denied) {
|
|
3223
|
+
scopes.push(scope.where);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
if (scopes.length === 0) {
|
|
3227
|
+
return c.json(this.emptyResult(query.limit, query.page));
|
|
3228
|
+
}
|
|
3229
|
+
let where = scopes.length === 1 ? scopes[0] : { OR: scopes };
|
|
3230
|
+
if (query.where) {
|
|
3231
|
+
where = mergeWhereConstraint(where, query.where);
|
|
3232
|
+
}
|
|
3233
|
+
const result = await config.db.find({
|
|
3234
|
+
collection: "__audit",
|
|
3235
|
+
where,
|
|
3236
|
+
limit: query.limit,
|
|
3237
|
+
page: query.page,
|
|
3238
|
+
sort: query.sort
|
|
3239
|
+
});
|
|
3240
|
+
return c.json(result);
|
|
3241
|
+
}
|
|
3242
|
+
};
|
|
3243
|
+
|
|
3063
3244
|
// src/middleware/auth.ts
|
|
3064
3245
|
function getBearerToken(c) {
|
|
3065
3246
|
const authHeader = c.req.header("Authorization");
|
|
@@ -3137,6 +3318,15 @@ function optionalAuth(config) {
|
|
|
3137
3318
|
}
|
|
3138
3319
|
|
|
3139
3320
|
// src/utils/openapi.ts
|
|
3321
|
+
function getCollectionLabels(collection) {
|
|
3322
|
+
return collection.labels || { singular: collection.slug, plural: collection.slug };
|
|
3323
|
+
}
|
|
3324
|
+
function getCollectionTag(collection) {
|
|
3325
|
+
return `Collection: ${getCollectionLabels(collection).plural}`;
|
|
3326
|
+
}
|
|
3327
|
+
function getGlobalTag(global) {
|
|
3328
|
+
return `Global: ${global.label || global.slug}`;
|
|
3329
|
+
}
|
|
3140
3330
|
function generateOpenApi(config) {
|
|
3141
3331
|
const spec = {
|
|
3142
3332
|
openapi: "3.0.0",
|
|
@@ -3168,15 +3358,7 @@ function generateOpenApi(config) {
|
|
|
3168
3358
|
},
|
|
3169
3359
|
WorkflowHistoryEntry: {
|
|
3170
3360
|
type: "object",
|
|
3171
|
-
required: [
|
|
3172
|
-
"collection",
|
|
3173
|
-
"documentId",
|
|
3174
|
-
"transition",
|
|
3175
|
-
"from",
|
|
3176
|
-
"to",
|
|
3177
|
-
"revision",
|
|
3178
|
-
"createdAt"
|
|
3179
|
-
],
|
|
3361
|
+
required: ["collection", "documentId", "transition", "from", "to", "revision", "createdAt"],
|
|
3180
3362
|
properties: {
|
|
3181
3363
|
id: { type: "string" },
|
|
3182
3364
|
collection: { type: "string" },
|
|
@@ -3190,6 +3372,21 @@ function generateOpenApi(config) {
|
|
|
3190
3372
|
createdAt: { type: "string", format: "date-time" }
|
|
3191
3373
|
}
|
|
3192
3374
|
},
|
|
3375
|
+
AuditEntry: {
|
|
3376
|
+
type: "object",
|
|
3377
|
+
required: ["collection", "operation", "timestamp"],
|
|
3378
|
+
properties: {
|
|
3379
|
+
id: { type: "string" },
|
|
3380
|
+
collection: { type: "string" },
|
|
3381
|
+
documentId: { type: "string", nullable: true },
|
|
3382
|
+
operation: { type: "string" },
|
|
3383
|
+
user: { type: "string", nullable: true },
|
|
3384
|
+
timestamp: { type: "string", format: "date-time" },
|
|
3385
|
+
changes: {
|
|
3386
|
+
oneOf: [{ type: "string" }, { type: "object", additionalProperties: true }, { type: "null" }]
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
},
|
|
3193
3390
|
Error: {
|
|
3194
3391
|
type: "object",
|
|
3195
3392
|
properties: {
|
|
@@ -3278,17 +3475,13 @@ function generateOpenApi(config) {
|
|
|
3278
3475
|
get: {
|
|
3279
3476
|
tags: ["Preferences"],
|
|
3280
3477
|
summary: "Get an authenticated user preference",
|
|
3281
|
-
parameters: [
|
|
3282
|
-
{ name: "key", in: "path", required: true, schema: { type: "string" } }
|
|
3283
|
-
],
|
|
3478
|
+
parameters: [{ name: "key", in: "path", required: true, schema: { type: "string" } }],
|
|
3284
3479
|
responses: { 200: { description: "Preference value" } }
|
|
3285
3480
|
},
|
|
3286
3481
|
put: {
|
|
3287
3482
|
tags: ["Preferences"],
|
|
3288
3483
|
summary: "Set an authenticated user preference",
|
|
3289
|
-
parameters: [
|
|
3290
|
-
{ name: "key", in: "path", required: true, schema: { type: "string" } }
|
|
3291
|
-
],
|
|
3484
|
+
parameters: [{ name: "key", in: "path", required: true, schema: { type: "string" } }],
|
|
3292
3485
|
requestBody: {
|
|
3293
3486
|
required: true,
|
|
3294
3487
|
content: { "application/json": { schema: { type: "object" } } }
|
|
@@ -3319,6 +3512,39 @@ function generateOpenApi(config) {
|
|
|
3319
3512
|
responses: { 200: { description: "Preview document data" } }
|
|
3320
3513
|
}
|
|
3321
3514
|
};
|
|
3515
|
+
spec.paths["/api/audit"] = {
|
|
3516
|
+
get: {
|
|
3517
|
+
tags: ["Audit"],
|
|
3518
|
+
summary: "Get audit entries across all readable audited collections",
|
|
3519
|
+
parameters: [
|
|
3520
|
+
{ name: "limit", in: "query", schema: { type: "integer", default: 50, maximum: 100 } },
|
|
3521
|
+
{ name: "page", in: "query", schema: { type: "integer", default: 1 } },
|
|
3522
|
+
{ name: "where", in: "query", schema: { type: "string" }, description: "JSON filter" },
|
|
3523
|
+
{ name: "sort", in: "query", schema: { type: "string", default: "-timestamp" } }
|
|
3524
|
+
],
|
|
3525
|
+
responses: {
|
|
3526
|
+
200: {
|
|
3527
|
+
description: "Audit entries",
|
|
3528
|
+
content: {
|
|
3529
|
+
"application/json": {
|
|
3530
|
+
schema: {
|
|
3531
|
+
type: "object",
|
|
3532
|
+
properties: {
|
|
3533
|
+
docs: {
|
|
3534
|
+
type: "array",
|
|
3535
|
+
items: { $ref: "#/components/schemas/AuditEntry" }
|
|
3536
|
+
},
|
|
3537
|
+
total: { type: "integer" },
|
|
3538
|
+
limit: { type: "integer" },
|
|
3539
|
+
page: { type: "integer" }
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
};
|
|
3322
3548
|
for (const collection of config.collections) {
|
|
3323
3549
|
spec.components.schemas[collection.slug] = collectionToSchema(collection);
|
|
3324
3550
|
}
|
|
@@ -3328,10 +3554,11 @@ function generateOpenApi(config) {
|
|
|
3328
3554
|
for (const collection of config.collections) {
|
|
3329
3555
|
const slug = collection.slug;
|
|
3330
3556
|
const path = `/api/collections/${slug}`;
|
|
3331
|
-
const labels = collection
|
|
3557
|
+
const labels = getCollectionLabels(collection);
|
|
3558
|
+
const collectionTag = getCollectionTag(collection);
|
|
3332
3559
|
spec.paths[path] = {
|
|
3333
3560
|
get: {
|
|
3334
|
-
tags: [
|
|
3561
|
+
tags: [collectionTag],
|
|
3335
3562
|
summary: `Find ${labels.plural}`,
|
|
3336
3563
|
parameters: [
|
|
3337
3564
|
{
|
|
@@ -3380,7 +3607,7 @@ function generateOpenApi(config) {
|
|
|
3380
3607
|
}
|
|
3381
3608
|
},
|
|
3382
3609
|
post: {
|
|
3383
|
-
tags: [
|
|
3610
|
+
tags: [collectionTag],
|
|
3384
3611
|
summary: `Create ${labels.singular}`,
|
|
3385
3612
|
requestBody: {
|
|
3386
3613
|
required: true,
|
|
@@ -3404,7 +3631,7 @@ function generateOpenApi(config) {
|
|
|
3404
3631
|
};
|
|
3405
3632
|
spec.paths[`${path}/{id}`] = {
|
|
3406
3633
|
get: {
|
|
3407
|
-
tags: [
|
|
3634
|
+
tags: [collectionTag],
|
|
3408
3635
|
summary: `Get a single ${labels.singular}`,
|
|
3409
3636
|
parameters: [
|
|
3410
3637
|
{
|
|
@@ -3426,7 +3653,7 @@ function generateOpenApi(config) {
|
|
|
3426
3653
|
}
|
|
3427
3654
|
},
|
|
3428
3655
|
patch: {
|
|
3429
|
-
tags: [
|
|
3656
|
+
tags: [collectionTag],
|
|
3430
3657
|
summary: `Update ${labels.singular}`,
|
|
3431
3658
|
parameters: [
|
|
3432
3659
|
{
|
|
@@ -3456,7 +3683,7 @@ function generateOpenApi(config) {
|
|
|
3456
3683
|
}
|
|
3457
3684
|
},
|
|
3458
3685
|
delete: {
|
|
3459
|
-
tags: [
|
|
3686
|
+
tags: [collectionTag],
|
|
3460
3687
|
summary: `Delete ${labels.singular}`,
|
|
3461
3688
|
parameters: [
|
|
3462
3689
|
{
|
|
@@ -3473,7 +3700,7 @@ function generateOpenApi(config) {
|
|
|
3473
3700
|
};
|
|
3474
3701
|
spec.paths[`${path}/delete-many`] = {
|
|
3475
3702
|
delete: {
|
|
3476
|
-
tags: [
|
|
3703
|
+
tags: [collectionTag],
|
|
3477
3704
|
summary: `Delete multiple ${labels.plural}`,
|
|
3478
3705
|
requestBody: {
|
|
3479
3706
|
required: true,
|
|
@@ -3492,9 +3719,46 @@ function generateOpenApi(config) {
|
|
|
3492
3719
|
responses: { 200: { description: "Deleted and failed document IDs" } }
|
|
3493
3720
|
}
|
|
3494
3721
|
};
|
|
3722
|
+
if (collection.audit) {
|
|
3723
|
+
spec.paths[`${path}/__audit`] = {
|
|
3724
|
+
get: {
|
|
3725
|
+
tags: [collectionTag],
|
|
3726
|
+
summary: `Get ${labels.singular} audit entries`,
|
|
3727
|
+
parameters: [
|
|
3728
|
+
{ name: "limit", in: "query", schema: { type: "integer", default: 50, maximum: 100 } },
|
|
3729
|
+
{ name: "page", in: "query", schema: { type: "integer", default: 1 } },
|
|
3730
|
+
{ name: "where", in: "query", schema: { type: "string" }, description: "JSON filter" },
|
|
3731
|
+
{ name: "sort", in: "query", schema: { type: "string", default: "-timestamp" } }
|
|
3732
|
+
],
|
|
3733
|
+
responses: {
|
|
3734
|
+
200: {
|
|
3735
|
+
description: "Audit entries for this collection",
|
|
3736
|
+
content: {
|
|
3737
|
+
"application/json": {
|
|
3738
|
+
schema: {
|
|
3739
|
+
type: "object",
|
|
3740
|
+
properties: {
|
|
3741
|
+
docs: {
|
|
3742
|
+
type: "array",
|
|
3743
|
+
items: { $ref: "#/components/schemas/AuditEntry" }
|
|
3744
|
+
},
|
|
3745
|
+
total: { type: "integer" },
|
|
3746
|
+
limit: { type: "integer" },
|
|
3747
|
+
page: { type: "integer" }
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
},
|
|
3753
|
+
403: { description: "Collection read access denied" },
|
|
3754
|
+
404: { description: "Audit is not enabled for this collection" }
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3495
3759
|
spec.paths[`${path}/seed`] = {
|
|
3496
3760
|
post: {
|
|
3497
|
-
tags: [
|
|
3761
|
+
tags: [collectionTag],
|
|
3498
3762
|
summary: `Seed initial ${labels.plural}`,
|
|
3499
3763
|
responses: { 200: { description: "Seeded documents" } }
|
|
3500
3764
|
}
|
|
@@ -3502,12 +3766,12 @@ function generateOpenApi(config) {
|
|
|
3502
3766
|
if (collection.upload) {
|
|
3503
3767
|
spec.paths[`${path}/media`] = {
|
|
3504
3768
|
get: {
|
|
3505
|
-
tags: [
|
|
3769
|
+
tags: [collectionTag],
|
|
3506
3770
|
summary: `List ${labels.plural}`,
|
|
3507
3771
|
responses: { 200: { description: "Paginated media documents" } }
|
|
3508
3772
|
},
|
|
3509
3773
|
post: {
|
|
3510
|
-
tags: [
|
|
3774
|
+
tags: [collectionTag],
|
|
3511
3775
|
summary: `Upload ${labels.singular}`,
|
|
3512
3776
|
requestBody: {
|
|
3513
3777
|
required: true,
|
|
@@ -3526,7 +3790,7 @@ function generateOpenApi(config) {
|
|
|
3526
3790
|
};
|
|
3527
3791
|
spec.paths[`${path}/media/{filename}`] = {
|
|
3528
3792
|
get: {
|
|
3529
|
-
tags: [
|
|
3793
|
+
tags: [collectionTag],
|
|
3530
3794
|
summary: `Serve ${labels.singular} bytes`,
|
|
3531
3795
|
parameters: [
|
|
3532
3796
|
{
|
|
@@ -3546,7 +3810,7 @@ function generateOpenApi(config) {
|
|
|
3546
3810
|
}
|
|
3547
3811
|
if (collection.auth) {
|
|
3548
3812
|
const publicAuthPost = (summary) => ({
|
|
3549
|
-
tags: [
|
|
3813
|
+
tags: [collectionTag],
|
|
3550
3814
|
summary,
|
|
3551
3815
|
security: [],
|
|
3552
3816
|
requestBody: {
|
|
@@ -3588,14 +3852,14 @@ function generateOpenApi(config) {
|
|
|
3588
3852
|
};
|
|
3589
3853
|
spec.paths[`${path}/logout`] = {
|
|
3590
3854
|
post: {
|
|
3591
|
-
tags: [
|
|
3855
|
+
tags: [collectionTag],
|
|
3592
3856
|
summary: `Log out of ${labels.plural}`,
|
|
3593
3857
|
responses: { 200: { description: "Logged out" } }
|
|
3594
3858
|
}
|
|
3595
3859
|
};
|
|
3596
3860
|
spec.paths[`${path}/init`] = {
|
|
3597
3861
|
get: {
|
|
3598
|
-
tags: [
|
|
3862
|
+
tags: [collectionTag],
|
|
3599
3863
|
summary: `Get ${labels.plural} initialization state`,
|
|
3600
3864
|
security: [],
|
|
3601
3865
|
responses: { 200: { description: "Initialization state" } }
|
|
@@ -3606,14 +3870,14 @@ function generateOpenApi(config) {
|
|
|
3606
3870
|
};
|
|
3607
3871
|
spec.paths[`${path}/me`] = {
|
|
3608
3872
|
get: {
|
|
3609
|
-
tags: [
|
|
3873
|
+
tags: [collectionTag],
|
|
3610
3874
|
summary: `Get the current ${labels.singular}`,
|
|
3611
3875
|
responses: { 200: { description: "Authenticated user" } }
|
|
3612
3876
|
}
|
|
3613
3877
|
};
|
|
3614
3878
|
spec.paths[`${path}/refresh-token`] = {
|
|
3615
3879
|
post: {
|
|
3616
|
-
tags: [
|
|
3880
|
+
tags: [collectionTag],
|
|
3617
3881
|
summary: "Refresh an authentication token",
|
|
3618
3882
|
responses: { 200: { description: "Refreshed token" } }
|
|
3619
3883
|
}
|
|
@@ -3626,7 +3890,7 @@ function generateOpenApi(config) {
|
|
|
3626
3890
|
};
|
|
3627
3891
|
spec.paths[`${path}/invite`] = {
|
|
3628
3892
|
post: {
|
|
3629
|
-
tags: [
|
|
3893
|
+
tags: [collectionTag],
|
|
3630
3894
|
summary: `Invite a ${labels.singular}`,
|
|
3631
3895
|
responses: { 200: { description: "Invitation sent" } }
|
|
3632
3896
|
}
|
|
@@ -3636,7 +3900,7 @@ function generateOpenApi(config) {
|
|
|
3636
3900
|
};
|
|
3637
3901
|
spec.paths[`${path}/{id}/change-password`] = {
|
|
3638
3902
|
post: {
|
|
3639
|
-
tags: [
|
|
3903
|
+
tags: [collectionTag],
|
|
3640
3904
|
summary: `Change a ${labels.singular} password`,
|
|
3641
3905
|
parameters: [
|
|
3642
3906
|
{
|
|
@@ -3653,7 +3917,7 @@ function generateOpenApi(config) {
|
|
|
3653
3917
|
if (collection.workflow) {
|
|
3654
3918
|
spec.paths[`${path}/{id}/transitions/{transition}`] = {
|
|
3655
3919
|
post: {
|
|
3656
|
-
tags: [
|
|
3920
|
+
tags: [collectionTag],
|
|
3657
3921
|
summary: `Transition ${labels.singular} workflow`,
|
|
3658
3922
|
parameters: [
|
|
3659
3923
|
{
|
|
@@ -3698,7 +3962,7 @@ function generateOpenApi(config) {
|
|
|
3698
3962
|
};
|
|
3699
3963
|
spec.paths[`${path}/{id}/workflow-history`] = {
|
|
3700
3964
|
get: {
|
|
3701
|
-
tags: [
|
|
3965
|
+
tags: [collectionTag],
|
|
3702
3966
|
summary: `Get ${labels.singular} workflow history`,
|
|
3703
3967
|
parameters: [
|
|
3704
3968
|
{
|
|
@@ -3744,9 +4008,10 @@ function generateOpenApi(config) {
|
|
|
3744
4008
|
for (const global of config.globals) {
|
|
3745
4009
|
const slug = global.slug;
|
|
3746
4010
|
const path = `/api/globals/${slug}`;
|
|
4011
|
+
const globalTag = getGlobalTag(global);
|
|
3747
4012
|
spec.paths[path] = {
|
|
3748
4013
|
get: {
|
|
3749
|
-
tags: [
|
|
4014
|
+
tags: [globalTag],
|
|
3750
4015
|
summary: `Get ${global.label || slug}`,
|
|
3751
4016
|
responses: {
|
|
3752
4017
|
200: {
|
|
@@ -3760,7 +4025,7 @@ function generateOpenApi(config) {
|
|
|
3760
4025
|
}
|
|
3761
4026
|
},
|
|
3762
4027
|
patch: {
|
|
3763
|
-
tags: [
|
|
4028
|
+
tags: [globalTag],
|
|
3764
4029
|
summary: `Update ${global.label || slug}`,
|
|
3765
4030
|
requestBody: {
|
|
3766
4031
|
required: true,
|
|
@@ -3784,7 +4049,7 @@ function generateOpenApi(config) {
|
|
|
3784
4049
|
};
|
|
3785
4050
|
spec.paths[`${path}/seed`] = {
|
|
3786
4051
|
post: {
|
|
3787
|
-
tags: [
|
|
4052
|
+
tags: [globalTag],
|
|
3788
4053
|
summary: `Seed ${global.label || slug}`,
|
|
3789
4054
|
responses: { 200: { description: "Seeded global" } }
|
|
3790
4055
|
}
|
|
@@ -3863,10 +4128,7 @@ function fieldToSchema(field) {
|
|
|
3863
4128
|
break;
|
|
3864
4129
|
case "url":
|
|
3865
4130
|
schema = {
|
|
3866
|
-
oneOf: [
|
|
3867
|
-
{ type: "string" },
|
|
3868
|
-
{ type: "object", additionalProperties: true }
|
|
3869
|
-
]
|
|
4131
|
+
oneOf: [{ type: "string" }, { type: "object", additionalProperties: true }]
|
|
3870
4132
|
};
|
|
3871
4133
|
break;
|
|
3872
4134
|
case "icon":
|
|
@@ -4061,6 +4323,7 @@ function serializeFieldForApi(f) {
|
|
|
4061
4323
|
return serialized;
|
|
4062
4324
|
}
|
|
4063
4325
|
function registerRoutes(app, config) {
|
|
4326
|
+
const optionsCache = /* @__PURE__ */ new Map();
|
|
4064
4327
|
app.get("/api/schemas", optionalAuth(config), async (c) => {
|
|
4065
4328
|
const siteId = c.req.header("X-Site-Id");
|
|
4066
4329
|
let collections = [...config.collections];
|
|
@@ -4081,6 +4344,10 @@ function registerRoutes(app, config) {
|
|
|
4081
4344
|
const serializeAccess = async (access) => {
|
|
4082
4345
|
if (typeof access === "string") return access;
|
|
4083
4346
|
if (typeof access === "boolean") return access;
|
|
4347
|
+
if (access && typeof access === "object" && typeof access.policy === "string") {
|
|
4348
|
+
const policy = config.accessPolicies?.[access.policy];
|
|
4349
|
+
if (typeof policy === "string" || typeof policy === "boolean") return policy;
|
|
4350
|
+
}
|
|
4084
4351
|
return resolveBooleanAccess(config, access, accessArgs);
|
|
4085
4352
|
};
|
|
4086
4353
|
const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
|
|
@@ -4109,11 +4376,14 @@ function registerRoutes(app, config) {
|
|
|
4109
4376
|
admin: f.admin,
|
|
4110
4377
|
access: {
|
|
4111
4378
|
read: await serializeAccess(f.access?.read),
|
|
4379
|
+
create: await serializeAccess(f.access?.create),
|
|
4112
4380
|
update: await serializeAccess(f.access?.update)
|
|
4113
4381
|
}
|
|
4114
4382
|
}))),
|
|
4115
4383
|
upload: !!col.upload,
|
|
4116
4384
|
auth: !!col.auth,
|
|
4385
|
+
audit: !!col.audit,
|
|
4386
|
+
drafts: !!col.drafts,
|
|
4117
4387
|
admin: col.admin,
|
|
4118
4388
|
workflow: col.workflow ? {
|
|
4119
4389
|
initialState: col.workflow.initialState,
|
|
@@ -4216,10 +4486,12 @@ function registerRoutes(app, config) {
|
|
|
4216
4486
|
return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
|
|
4217
4487
|
}
|
|
4218
4488
|
let resolver;
|
|
4489
|
+
let cacheTTL;
|
|
4219
4490
|
if (typeof field.options === "function") {
|
|
4220
4491
|
resolver = field.options;
|
|
4221
4492
|
} else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
|
|
4222
4493
|
resolver = field.options.resolve;
|
|
4494
|
+
cacheTTL = field.options.cacheTTL;
|
|
4223
4495
|
}
|
|
4224
4496
|
if (!resolver) {
|
|
4225
4497
|
return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
|
|
@@ -4232,11 +4504,32 @@ function registerRoutes(app, config) {
|
|
|
4232
4504
|
headers: c.req.header(),
|
|
4233
4505
|
raw: c.req.raw
|
|
4234
4506
|
};
|
|
4507
|
+
const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
|
|
4508
|
+
let cacheKey = "";
|
|
4509
|
+
if (shouldCache) {
|
|
4510
|
+
cacheKey = JSON.stringify([
|
|
4511
|
+
siteId ?? "",
|
|
4512
|
+
colSlug,
|
|
4513
|
+
fieldName,
|
|
4514
|
+
user?.id ?? null,
|
|
4515
|
+
queryParams
|
|
4516
|
+
]);
|
|
4517
|
+
const hit = optionsCache.get(cacheKey);
|
|
4518
|
+
if (hit && hit.expires > Date.now()) {
|
|
4519
|
+
return c.json(hit.value);
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4235
4522
|
const result = await resolver({
|
|
4236
4523
|
db,
|
|
4237
4524
|
user,
|
|
4238
4525
|
req: reqContext
|
|
4239
4526
|
});
|
|
4527
|
+
if (shouldCache) {
|
|
4528
|
+
optionsCache.set(cacheKey, {
|
|
4529
|
+
expires: Date.now() + cacheTTL * 1e3,
|
|
4530
|
+
value: result
|
|
4531
|
+
});
|
|
4532
|
+
}
|
|
4240
4533
|
return c.json(result);
|
|
4241
4534
|
} catch (err) {
|
|
4242
4535
|
console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
|
|
@@ -4381,6 +4674,7 @@ function registerRoutes(app, config) {
|
|
|
4381
4674
|
app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
|
|
4382
4675
|
app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
|
|
4383
4676
|
}
|
|
4677
|
+
const auditController = new AuditController();
|
|
4384
4678
|
for (const collection of config.collections) {
|
|
4385
4679
|
const path = `/api/collections/${collection.slug}`;
|
|
4386
4680
|
const controller = new CollectionController(collection);
|
|
@@ -4388,6 +4682,9 @@ function registerRoutes(app, config) {
|
|
|
4388
4682
|
app.post(path, (c) => controller.create(c));
|
|
4389
4683
|
app.post(`${path}/media`, (c) => controller.create(c));
|
|
4390
4684
|
app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
|
|
4685
|
+
if (collection.audit) {
|
|
4686
|
+
app.get(`${path}/__audit`, (c) => auditController.findForCollection(c, collection));
|
|
4687
|
+
}
|
|
4391
4688
|
app.get(`${path}/:id`, (c) => controller.findOne(c));
|
|
4392
4689
|
app.patch(`${path}/:id`, (c) => controller.update(c));
|
|
4393
4690
|
app.delete(`${path}/:id`, (c) => controller.delete(c));
|
|
@@ -4407,9 +4704,32 @@ function registerRoutes(app, config) {
|
|
|
4407
4704
|
app.patch(path, (c) => controller.update(c));
|
|
4408
4705
|
app.post(`${path}/seed`, (c) => controller.seed(c));
|
|
4409
4706
|
}
|
|
4707
|
+
if (!process.env.DYRECTED_JWT_SECRET) {
|
|
4708
|
+
console.warn(
|
|
4709
|
+
'[dyrected] DYRECTED_JWT_SECRET is not set \u2014 token-mode live preview is signing with an insecure default. Set DYRECTED_JWT_SECRET before relying on `previewMode: "token"` in production.'
|
|
4710
|
+
);
|
|
4711
|
+
}
|
|
4410
4712
|
const previewController = new PreviewController();
|
|
4411
4713
|
app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
|
|
4412
4714
|
app.get("/api/preview-data", (c) => previewController.getData(c));
|
|
4715
|
+
app.get("/api/audit", (c) => auditController.findAll(c));
|
|
4716
|
+
app.get("/api/collections/:slug/__audit", async (c) => {
|
|
4717
|
+
const slug = c.req.param("slug");
|
|
4718
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
4719
|
+
const config2 = c.get("config");
|
|
4720
|
+
if (config2.collections.some((col) => col.slug === slug)) {
|
|
4721
|
+
return c.json({ message: "Not Found" }, 404);
|
|
4722
|
+
}
|
|
4723
|
+
if (!config2.onSchemaFetch || !siteId) {
|
|
4724
|
+
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
4725
|
+
}
|
|
4726
|
+
const dynamic = await config2.onSchemaFetch(siteId);
|
|
4727
|
+
const collection = dynamic.collections?.find((col) => col.slug === slug);
|
|
4728
|
+
if (!collection?.audit) {
|
|
4729
|
+
return c.json({ message: `Collection "${slug}" not found or has no audit log` }, 404);
|
|
4730
|
+
}
|
|
4731
|
+
return auditController.findForCollection(c, collection);
|
|
4732
|
+
});
|
|
4413
4733
|
app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
|
|
4414
4734
|
const slug = c.req.param("slug");
|
|
4415
4735
|
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
@@ -4558,6 +4878,7 @@ var AUDIT_COLLECTION = {
|
|
|
4558
4878
|
{ name: "timestamp", type: "date", label: "Timestamp", required: true },
|
|
4559
4879
|
{ name: "changes", type: "json", label: "Changes" }
|
|
4560
4880
|
],
|
|
4881
|
+
access: { read: () => false, create: () => false, update: () => false, delete: () => false },
|
|
4561
4882
|
admin: { hidden: true }
|
|
4562
4883
|
};
|
|
4563
4884
|
var WORKFLOW_HISTORY_COLLECTION_CONFIG = {
|
|
@@ -4600,7 +4921,7 @@ function normalizeConfig(config) {
|
|
|
4600
4921
|
const collections = config?.collections || [];
|
|
4601
4922
|
const globals = config?.globals || [];
|
|
4602
4923
|
const needsAudit = collections.some((col) => col.audit);
|
|
4603
|
-
const needsWorkflow = collections.some((col) => col.workflow);
|
|
4924
|
+
const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
|
|
4604
4925
|
const adminAuthCollectionSlug = getAdminAuthCollection({
|
|
4605
4926
|
collections,
|
|
4606
4927
|
adminAuth: config.adminAuth
|
|
@@ -4662,6 +4983,12 @@ function normalizeConfig(config) {
|
|
|
4662
4983
|
if (field.name === "email") {
|
|
4663
4984
|
return {
|
|
4664
4985
|
...field,
|
|
4986
|
+
// Email is the login identifier. Keep the integrity constraints
|
|
4987
|
+
// auth relies on even when the field is explicitly redefined, so a
|
|
4988
|
+
// custom `email` field can never silently drop uniqueness.
|
|
4989
|
+
required: true,
|
|
4990
|
+
unique: true,
|
|
4991
|
+
promoted: true,
|
|
4665
4992
|
access: {
|
|
4666
4993
|
...field.access || {},
|
|
4667
4994
|
update: "!id"
|
|
@@ -4671,6 +4998,9 @@ function normalizeConfig(config) {
|
|
|
4671
4998
|
if (field.name === "password") {
|
|
4672
4999
|
return {
|
|
4673
5000
|
...field,
|
|
5001
|
+
// Password is required to authenticate; enforce it regardless of
|
|
5002
|
+
// how the field was declared.
|
|
5003
|
+
required: true,
|
|
4674
5004
|
admin: { ...field.admin || {} },
|
|
4675
5005
|
access: {
|
|
4676
5006
|
...field.access || {},
|
|
@@ -4726,8 +5056,10 @@ function normalizeConfig(config) {
|
|
|
4726
5056
|
}
|
|
4727
5057
|
const updatedFieldNames = new Set(fields.map((f) => f.name));
|
|
4728
5058
|
const fieldsToInject = SYSTEM_FIELDS.filter((f) => !updatedFieldNames.has(f.name));
|
|
5059
|
+
const workflow = col.workflow || (col.drafts ? simplePublishingWorkflow() : void 0);
|
|
4729
5060
|
return {
|
|
4730
5061
|
...col,
|
|
5062
|
+
workflow,
|
|
4731
5063
|
fields: [...fields, ...fieldsToInject]
|
|
4732
5064
|
};
|
|
4733
5065
|
});
|