@azlib/cms 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/index.cjs +199 -27
- package/dist/index.d.cts +312 -194
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +312 -194
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +199 -28
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -229,6 +229,7 @@ function normalizeConfig(config) {
|
|
|
229
229
|
},
|
|
230
230
|
collections,
|
|
231
231
|
taxonomies: config.taxonomies ?? [],
|
|
232
|
+
plugins: config.plugins ?? [],
|
|
232
233
|
admin: {
|
|
233
234
|
route: config.admin?.route ?? "/admin",
|
|
234
235
|
enableRegistration: config.admin?.enableRegistration ?? false,
|
|
@@ -1245,6 +1246,9 @@ var CMSEngine = class {
|
|
|
1245
1246
|
revisions;
|
|
1246
1247
|
lifecycle;
|
|
1247
1248
|
collections = /* @__PURE__ */ new Map();
|
|
1249
|
+
plugins = /* @__PURE__ */ new Map();
|
|
1250
|
+
customRoutes = /* @__PURE__ */ new Map();
|
|
1251
|
+
pendingPluginSetups = [];
|
|
1248
1252
|
constructor(config = {}, storage, hooks) {
|
|
1249
1253
|
this.config = normalizeConfig(config);
|
|
1250
1254
|
this.hooks = hooks ?? new HooksManager();
|
|
@@ -1257,21 +1261,114 @@ var CMSEngine = class {
|
|
|
1257
1261
|
this.lifecycle = new ContentLifecycle(this.storage, this.hooks);
|
|
1258
1262
|
for (const coll of this.config.collections) this.collections.set(coll.slug, coll);
|
|
1259
1263
|
if (this.config.taxonomies) for (const tax of this.config.taxonomies) this.taxonomies.registerTaxonomy(tax);
|
|
1264
|
+
if (this.config.plugins) for (const plugin of this.config.plugins) this.use(plugin);
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Register a plugin with the CMS engine.
|
|
1268
|
+
*/
|
|
1269
|
+
use(plugin) {
|
|
1270
|
+
if (this.plugins.has(plugin.name)) throw new Error(`[CMSEngine] Plugin '${plugin.name}' is already registered.`);
|
|
1271
|
+
this.plugins.set(plugin.name, plugin);
|
|
1272
|
+
if (plugin.collections) for (const coll of plugin.collections) this.registerCollection(coll);
|
|
1273
|
+
if (plugin.taxonomies) for (const tax of plugin.taxonomies) this.taxonomies.registerTaxonomy(tax);
|
|
1274
|
+
if (plugin.extendCollections) for (const [slug, fields] of Object.entries(plugin.extendCollections)) this.extendCollection(slug, fields);
|
|
1275
|
+
if (plugin.setup) {
|
|
1276
|
+
const context = {
|
|
1277
|
+
engine: this,
|
|
1278
|
+
hooks: this.hooks,
|
|
1279
|
+
storage: this.storage,
|
|
1280
|
+
options: this.options,
|
|
1281
|
+
taxonomies: this.taxonomies,
|
|
1282
|
+
media: this.media,
|
|
1283
|
+
rbac: this.rbac,
|
|
1284
|
+
revisions: this.revisions,
|
|
1285
|
+
lifecycle: this.lifecycle,
|
|
1286
|
+
registerCollection: (c) => this.registerCollection(c),
|
|
1287
|
+
extendCollection: (s, f) => this.extendCollection(s, f),
|
|
1288
|
+
registerTaxonomy: (t) => this.taxonomies.registerTaxonomy(t),
|
|
1289
|
+
registerRoute: (m, p, h) => this.registerRoute(m, p, h)
|
|
1290
|
+
};
|
|
1291
|
+
const setupResult = plugin.setup(context);
|
|
1292
|
+
if (setupResult instanceof Promise) this.pendingPluginSetups.push(setupResult);
|
|
1293
|
+
}
|
|
1294
|
+
return this;
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Get all registered plugins.
|
|
1298
|
+
*/
|
|
1299
|
+
getPlugins() {
|
|
1300
|
+
return Array.from(this.plugins.values());
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Get a registered plugin by name.
|
|
1304
|
+
*/
|
|
1305
|
+
getPlugin(name) {
|
|
1306
|
+
return this.plugins.get(name);
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Check if a plugin is registered.
|
|
1310
|
+
*/
|
|
1311
|
+
hasPlugin(name) {
|
|
1312
|
+
return this.plugins.has(name);
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Register a collection dynamically.
|
|
1316
|
+
*/
|
|
1317
|
+
registerCollection(coll) {
|
|
1318
|
+
this.collections.set(coll.slug, coll);
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Inject additional field definitions into an existing collection.
|
|
1322
|
+
*/
|
|
1323
|
+
extendCollection(slug, newFields) {
|
|
1324
|
+
const existing = this.collections.get(slug);
|
|
1325
|
+
if (!existing) throw new Error(`[CMSEngine] Cannot extend non-existent collection '${slug}'.`);
|
|
1326
|
+
const existingFieldNames = new Set(existing.fields.map((f) => f.name));
|
|
1327
|
+
const addedFields = newFields.filter((f) => !existingFieldNames.has(f.name));
|
|
1328
|
+
this.collections.set(slug, {
|
|
1329
|
+
...existing,
|
|
1330
|
+
fields: [...existing.fields, ...addedFields]
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Register a custom Web Standard HTTP route on the engine.
|
|
1335
|
+
*/
|
|
1336
|
+
registerRoute(method, path, handler) {
|
|
1337
|
+
const normalizedMethod = method.toUpperCase();
|
|
1338
|
+
let methodMap = this.customRoutes.get(normalizedMethod);
|
|
1339
|
+
if (!methodMap) {
|
|
1340
|
+
methodMap = /* @__PURE__ */ new Map();
|
|
1341
|
+
this.customRoutes.set(normalizedMethod, methodMap);
|
|
1342
|
+
}
|
|
1343
|
+
methodMap.set(path, handler);
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Get all custom routes registered across all HTTP methods.
|
|
1347
|
+
*/
|
|
1348
|
+
getCustomRoutes() {
|
|
1349
|
+
return this.customRoutes;
|
|
1260
1350
|
}
|
|
1261
1351
|
/**
|
|
1262
1352
|
* Initialize storage and trigger bootstrap hooks.
|
|
1263
1353
|
*/
|
|
1264
1354
|
async init() {
|
|
1265
1355
|
await this.storage.init();
|
|
1356
|
+
if (this.pendingPluginSetups.length > 0) {
|
|
1357
|
+
await Promise.all(this.pendingPluginSetups);
|
|
1358
|
+
this.pendingPluginSetups = [];
|
|
1359
|
+
}
|
|
1266
1360
|
if (this.config.site) {
|
|
1267
1361
|
if (!await this.options.has("site_name")) await this.options.set("site_name", this.config.site.name);
|
|
1268
1362
|
}
|
|
1363
|
+
for (const plugin of this.plugins.values()) if (plugin.onInit) await plugin.onInit(this);
|
|
1364
|
+
await this.hooks.doAction("cms.plugins_initialized", this);
|
|
1269
1365
|
await this.hooks.doAction("cms.init", this);
|
|
1270
1366
|
}
|
|
1271
1367
|
/**
|
|
1272
|
-
* Close storage connections.
|
|
1368
|
+
* Close storage connections and run plugin teardown hooks.
|
|
1273
1369
|
*/
|
|
1274
1370
|
async close() {
|
|
1371
|
+
for (const plugin of this.plugins.values()) if (plugin.onClose) await plugin.onClose(this);
|
|
1275
1372
|
await this.storage.close();
|
|
1276
1373
|
await this.hooks.doAction("cms.close", this);
|
|
1277
1374
|
}
|
|
@@ -1297,38 +1394,39 @@ var CMSEngine = class {
|
|
|
1297
1394
|
return {
|
|
1298
1395
|
config: collConfig,
|
|
1299
1396
|
async create(input, authorId = null) {
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1302
|
-
if (
|
|
1303
|
-
if (
|
|
1397
|
+
const filteredInput = await self.hooks.applyFilters("cms.before_create_input", input, { collection: slug });
|
|
1398
|
+
const inputData = { ...filteredInput.data || {} };
|
|
1399
|
+
if (filteredInput.title !== void 0) inputData.title = filteredInput.title;
|
|
1400
|
+
if (filteredInput.slug !== void 0) inputData.slug = filteredInput.slug;
|
|
1401
|
+
if (filteredInput.status !== void 0) inputData.status = filteredInput.status;
|
|
1304
1402
|
const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, inputData);
|
|
1305
1403
|
if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed for collection '${slug}': ${JSON.stringify(errors)}`);
|
|
1306
|
-
const title =
|
|
1307
|
-
const finalSlug = await resolveUniqueSlug(
|
|
1404
|
+
const title = filteredInput.title ?? normalizedData.title ?? "";
|
|
1405
|
+
const finalSlug = await resolveUniqueSlug(filteredInput.slug ? slugify(filteredInput.slug) : slugify(title) || "item", async (s) => {
|
|
1308
1406
|
return await self.storage.getContentBySlug(slug, s) !== null;
|
|
1309
1407
|
});
|
|
1310
|
-
const status =
|
|
1408
|
+
const status = filteredInput.status ?? (collConfig.draftable ? "draft" : "published");
|
|
1311
1409
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1312
1410
|
const item = await self.storage.createContent({
|
|
1313
1411
|
collection: slug,
|
|
1314
1412
|
slug: finalSlug,
|
|
1315
1413
|
title,
|
|
1316
1414
|
status,
|
|
1317
|
-
parentId: collConfig.hierarchical ?
|
|
1415
|
+
parentId: collConfig.hierarchical ? filteredInput.parentId ?? null : null,
|
|
1318
1416
|
authorId,
|
|
1319
1417
|
publishedAt: status === "published" ? nowIso : null,
|
|
1320
|
-
scheduledAt: status === "scheduled" ?
|
|
1418
|
+
scheduledAt: status === "scheduled" ? filteredInput.scheduledAt ?? null : null,
|
|
1321
1419
|
data: normalizedData,
|
|
1322
|
-
terms:
|
|
1420
|
+
terms: filteredInput.terms
|
|
1323
1421
|
});
|
|
1324
|
-
if (
|
|
1325
|
-
const allTermIds = Object.values(
|
|
1422
|
+
if (filteredInput.terms) {
|
|
1423
|
+
const allTermIds = Object.values(filteredInput.terms).flat();
|
|
1326
1424
|
if (allTermIds.length > 0) await self.taxonomies.assignTerms(item.id, allTermIds);
|
|
1327
1425
|
}
|
|
1328
1426
|
if (collConfig.revisions) await self.revisions.createRevision(item, authorId, "Initial creation");
|
|
1329
1427
|
await self.hooks.doAction("cms.content_created", item);
|
|
1330
1428
|
await self.hooks.doAction(`cms.${slug}_created`, item);
|
|
1331
|
-
return item;
|
|
1429
|
+
return self.hooks.applyFilters("cms.after_create_item", item, { collection: slug });
|
|
1332
1430
|
},
|
|
1333
1431
|
async findById(id) {
|
|
1334
1432
|
return self.storage.getContent(slug, id);
|
|
@@ -1337,51 +1435,58 @@ var CMSEngine = class {
|
|
|
1337
1435
|
return self.storage.getContentBySlug(slug, contentSlug);
|
|
1338
1436
|
},
|
|
1339
1437
|
async find(options = {}) {
|
|
1340
|
-
|
|
1438
|
+
const filteredOptions = await self.hooks.applyFilters("cms.find_options", options, { collection: slug });
|
|
1439
|
+
return self.storage.findContent(slug, filteredOptions);
|
|
1341
1440
|
},
|
|
1342
1441
|
async update(id, input, authorId = null, revisionNote) {
|
|
1343
1442
|
const existing = await self.storage.getContent(slug, id);
|
|
1344
1443
|
if (!existing) return null;
|
|
1444
|
+
const filteredInput = await self.hooks.applyFilters("cms.before_update_input", input, {
|
|
1445
|
+
collection: slug,
|
|
1446
|
+
id,
|
|
1447
|
+
existing
|
|
1448
|
+
});
|
|
1345
1449
|
const mergedData = {
|
|
1346
1450
|
...existing.data,
|
|
1347
|
-
...
|
|
1451
|
+
...filteredInput.data || {}
|
|
1348
1452
|
};
|
|
1349
|
-
if (
|
|
1453
|
+
if (filteredInput.title !== void 0) mergedData.title = filteredInput.title;
|
|
1350
1454
|
else if (existing.title !== void 0 && mergedData.title === void 0) mergedData.title = existing.title;
|
|
1351
|
-
if (
|
|
1455
|
+
if (filteredInput.slug !== void 0) mergedData.slug = filteredInput.slug;
|
|
1352
1456
|
else if (existing.slug !== void 0 && mergedData.slug === void 0) mergedData.slug = existing.slug;
|
|
1353
|
-
if (
|
|
1457
|
+
if (filteredInput.status !== void 0) mergedData.status = filteredInput.status;
|
|
1354
1458
|
else if (existing.status !== void 0 && mergedData.status === void 0) mergedData.status = existing.status;
|
|
1355
1459
|
const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, mergedData);
|
|
1356
1460
|
if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed updating '${slug}': ${JSON.stringify(errors)}`);
|
|
1357
1461
|
let updatedSlug = existing.slug;
|
|
1358
|
-
if (
|
|
1462
|
+
if (filteredInput.slug && filteredInput.slug !== existing.slug) updatedSlug = await resolveUniqueSlug(filteredInput.slug, async (s) => {
|
|
1359
1463
|
const check = await self.storage.getContentBySlug(slug, s);
|
|
1360
1464
|
return check !== null && check.id !== id;
|
|
1361
1465
|
}, id);
|
|
1362
|
-
const nextStatus =
|
|
1466
|
+
const nextStatus = filteredInput.status ?? existing.status;
|
|
1363
1467
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1364
1468
|
let publishedAt = existing.publishedAt;
|
|
1365
1469
|
if (nextStatus === "published" && !publishedAt) publishedAt = nowIso;
|
|
1366
1470
|
const updated = await self.storage.updateContent(slug, id, {
|
|
1367
|
-
title:
|
|
1471
|
+
title: filteredInput.title !== void 0 ? filteredInput.title : existing.title,
|
|
1368
1472
|
slug: updatedSlug,
|
|
1369
1473
|
status: nextStatus,
|
|
1370
|
-
parentId: collConfig.hierarchical ?
|
|
1371
|
-
scheduledAt:
|
|
1474
|
+
parentId: collConfig.hierarchical ? filteredInput.parentId !== void 0 ? filteredInput.parentId : existing.parentId : null,
|
|
1475
|
+
scheduledAt: filteredInput.scheduledAt !== void 0 ? filteredInput.scheduledAt : existing.scheduledAt,
|
|
1372
1476
|
publishedAt,
|
|
1373
1477
|
authorId: authorId ?? existing.authorId,
|
|
1374
1478
|
data: normalizedData,
|
|
1375
|
-
terms:
|
|
1479
|
+
terms: filteredInput.terms !== void 0 ? filteredInput.terms : existing.terms
|
|
1376
1480
|
});
|
|
1377
1481
|
if (updated) {
|
|
1378
|
-
if (
|
|
1379
|
-
const allTermIds = Object.values(
|
|
1482
|
+
if (filteredInput.terms) {
|
|
1483
|
+
const allTermIds = Object.values(filteredInput.terms).flat();
|
|
1380
1484
|
await self.taxonomies.assignTerms(updated.id, allTermIds);
|
|
1381
1485
|
}
|
|
1382
1486
|
if (collConfig.revisions) await self.revisions.createRevision(updated, authorId, revisionNote ?? `Updated version ${updated.version}`);
|
|
1383
1487
|
await self.hooks.doAction("cms.content_updated", updated);
|
|
1384
1488
|
await self.hooks.doAction(`cms.${slug}_updated`, updated);
|
|
1489
|
+
return self.hooks.applyFilters("cms.after_update_item", updated, { collection: slug });
|
|
1385
1490
|
}
|
|
1386
1491
|
return updated;
|
|
1387
1492
|
},
|
|
@@ -1438,7 +1543,33 @@ function createCMSEngine(config, storage, hooks) {
|
|
|
1438
1543
|
return new CMSEngine(config, storage, hooks);
|
|
1439
1544
|
}
|
|
1440
1545
|
//#endregion
|
|
1546
|
+
//#region src/core/plugin.ts
|
|
1547
|
+
/**
|
|
1548
|
+
* Type-safe helper for authoring reusable CMS plugins and plugin factories.
|
|
1549
|
+
*/
|
|
1550
|
+
function definePlugin(factory) {
|
|
1551
|
+
return factory;
|
|
1552
|
+
}
|
|
1553
|
+
//#endregion
|
|
1441
1554
|
//#region src/api/router.ts
|
|
1555
|
+
function compilePath(path) {
|
|
1556
|
+
const keys = [];
|
|
1557
|
+
const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
|
|
1558
|
+
keys.push(key);
|
|
1559
|
+
return "([^/]+)";
|
|
1560
|
+
});
|
|
1561
|
+
return {
|
|
1562
|
+
regex: new RegExp(`^${pattern}$`),
|
|
1563
|
+
keys
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function matchRoute(entry, pathname) {
|
|
1567
|
+
const match = pathname.match(entry.regex);
|
|
1568
|
+
if (!match) return null;
|
|
1569
|
+
const params = {};
|
|
1570
|
+
for (let i = 0; i < entry.keys.length; i++) params[entry.keys[i]] = decodeURIComponent(match[i + 1]);
|
|
1571
|
+
return params;
|
|
1572
|
+
}
|
|
1442
1573
|
function jsonResponse(data, status = 200, headers = {}) {
|
|
1443
1574
|
return new Response(JSON.stringify(data), {
|
|
1444
1575
|
status,
|
|
@@ -1453,10 +1584,25 @@ function jsonResponse(data, status = 200, headers = {}) {
|
|
|
1453
1584
|
}
|
|
1454
1585
|
var CMSRouter = class {
|
|
1455
1586
|
engine;
|
|
1587
|
+
localRoutes = [];
|
|
1456
1588
|
constructor(engine) {
|
|
1457
1589
|
this.engine = engine;
|
|
1458
1590
|
}
|
|
1459
1591
|
/**
|
|
1592
|
+
* Register a custom Web Standard route on the router.
|
|
1593
|
+
*/
|
|
1594
|
+
registerRoute(method, path, handler) {
|
|
1595
|
+
const { regex, keys } = compilePath(path);
|
|
1596
|
+
this.localRoutes.push({
|
|
1597
|
+
method: method.toUpperCase(),
|
|
1598
|
+
path,
|
|
1599
|
+
regex,
|
|
1600
|
+
keys,
|
|
1601
|
+
handler
|
|
1602
|
+
});
|
|
1603
|
+
return this;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1460
1606
|
* Universal Web Standards request handler.
|
|
1461
1607
|
*/
|
|
1462
1608
|
async handle(request) {
|
|
@@ -1484,6 +1630,8 @@ var CMSRouter = class {
|
|
|
1484
1630
|
if (pathname.startsWith("/api/taxonomies")) return this.handleTaxonomies(pathname, method, url, request);
|
|
1485
1631
|
if (pathname.startsWith("/api/media")) return this.handleMedia(pathname, method, url, request);
|
|
1486
1632
|
if (pathname.startsWith("/api/content")) return this.handleContent(pathname, method, url, request);
|
|
1633
|
+
const customResponse = await this.handleCustomRoute(pathname, method, url, request);
|
|
1634
|
+
if (customResponse) return customResponse;
|
|
1487
1635
|
return jsonResponse({
|
|
1488
1636
|
error: "Endpoint not found",
|
|
1489
1637
|
path: pathname
|
|
@@ -1492,6 +1640,29 @@ var CMSRouter = class {
|
|
|
1492
1640
|
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
1493
1641
|
}
|
|
1494
1642
|
}
|
|
1643
|
+
async handleCustomRoute(pathname, method, url, request) {
|
|
1644
|
+
for (const route of this.localRoutes) if (route.method === method) {
|
|
1645
|
+
const params = matchRoute(route, pathname);
|
|
1646
|
+
if (params !== null) {
|
|
1647
|
+
const context = {
|
|
1648
|
+
params,
|
|
1649
|
+
url,
|
|
1650
|
+
engine: this.engine
|
|
1651
|
+
};
|
|
1652
|
+
return await route.handler(request, context);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
const engineRoutes = this.engine.getCustomRoutes().get(method);
|
|
1656
|
+
if (engineRoutes) for (const [pathPattern, handler] of engineRoutes) {
|
|
1657
|
+
const params = matchRoute(compilePath(pathPattern), pathname);
|
|
1658
|
+
if (params !== null) return await handler(request, {
|
|
1659
|
+
params,
|
|
1660
|
+
url,
|
|
1661
|
+
engine: this.engine
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1495
1666
|
async handleContent(pathname, method, url, request) {
|
|
1496
1667
|
const parts = pathname.replace(/^\/api\/content\/?/, "").split("/").filter(Boolean);
|
|
1497
1668
|
const collectionSlug = parts[0];
|
|
@@ -1775,6 +1946,6 @@ function createCmsClient(options) {
|
|
|
1775
1946
|
return new CMSClient(options);
|
|
1776
1947
|
}
|
|
1777
1948
|
//#endregion
|
|
1778
|
-
export { CMSClient, CMSEngine, CMSRouter, ContentLifecycle, DEFAULT_COLLECTIONS, DEFAULT_ROLE_CAPABILITIES, HooksManager, MediaManager, MemoryStorageAdapter, OptionsManager, RBACManager, RevisionManager, TaxonomyManager, VALID_STATUS_TRANSITIONS, collection, createCMSEngine, createCMSRouter, createCmsClient, defaultHooks, defineConfig, fields, normalizeConfig, resolveUniqueSlug, slugify, validateAndNormalizeData };
|
|
1949
|
+
export { CMSClient, CMSEngine, CMSRouter, ContentLifecycle, DEFAULT_COLLECTIONS, DEFAULT_ROLE_CAPABILITIES, HooksManager, MediaManager, MemoryStorageAdapter, OptionsManager, RBACManager, RevisionManager, TaxonomyManager, VALID_STATUS_TRANSITIONS, collection, createCMSEngine, createCMSRouter, createCmsClient, defaultHooks, defineConfig, definePlugin, fields, normalizeConfig, resolveUniqueSlug, slugify, validateAndNormalizeData };
|
|
1779
1950
|
|
|
1780
1951
|
//# sourceMappingURL=index.mjs.map
|