@azlib/cms 0.7.1 → 0.8.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 +176 -0
- package/dist/index.cjs +2227 -55
- package/dist/index.d.cts +658 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +658 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2198 -56
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -2008,6 +2008,12 @@ var CMSEngine = class {
|
|
|
2008
2008
|
return this.plugins.has(name);
|
|
2009
2009
|
}
|
|
2010
2010
|
/**
|
|
2011
|
+
* Check if a collection is registered.
|
|
2012
|
+
*/
|
|
2013
|
+
hasCollection(slug) {
|
|
2014
|
+
return this.collections.has(slug);
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2011
2017
|
* Register a collection dynamically.
|
|
2012
2018
|
*/
|
|
2013
2019
|
registerCollection(coll) {
|
|
@@ -2215,7 +2221,11 @@ var CMSEngine = class {
|
|
|
2215
2221
|
await self.hooks.doAction("cms.content_updated", updated);
|
|
2216
2222
|
await self.hooks.doAction(`cms.${slug}_updated`, updated);
|
|
2217
2223
|
self.webhooks.dispatch("content.updated", updated);
|
|
2218
|
-
if (updated.status === "published" && existing.status !== "published")
|
|
2224
|
+
if (updated.status === "published" && existing.status !== "published") {
|
|
2225
|
+
await self.hooks.doAction("cms.content_published", updated);
|
|
2226
|
+
await self.hooks.doAction(`cms.${slug}_published`, updated);
|
|
2227
|
+
self.webhooks.dispatch("content.published", updated);
|
|
2228
|
+
}
|
|
2219
2229
|
return self.hooks.applyFilters("cms.after_update_item", updated, { collection: slug });
|
|
2220
2230
|
}
|
|
2221
2231
|
return updated;
|
|
@@ -4389,7 +4399,7 @@ var EcommerceService = class {
|
|
|
4389
4399
|
};
|
|
4390
4400
|
//#endregion
|
|
4391
4401
|
//#region src/plugins/ecommerce/routes.ts
|
|
4392
|
-
function json$
|
|
4402
|
+
function json$7(data, status = 200) {
|
|
4393
4403
|
return new Response(JSON.stringify(data), {
|
|
4394
4404
|
status,
|
|
4395
4405
|
headers: {
|
|
@@ -4399,10 +4409,10 @@ function json$2(data, status = 200) {
|
|
|
4399
4409
|
});
|
|
4400
4410
|
}
|
|
4401
4411
|
function badRequest$2(message) {
|
|
4402
|
-
return json$
|
|
4412
|
+
return json$7({ error: message }, 400);
|
|
4403
4413
|
}
|
|
4404
4414
|
function notFound$2(message) {
|
|
4405
|
-
return json$
|
|
4415
|
+
return json$7({ error: message }, 404);
|
|
4406
4416
|
}
|
|
4407
4417
|
function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
4408
4418
|
const prefix = (options.apiPrefix ?? "/api/ecommerce").replace(/\/+$/, "");
|
|
@@ -4425,7 +4435,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4425
4435
|
const offsetStr = url.searchParams.get("offset");
|
|
4426
4436
|
const limit = limitStr ? parseInt(limitStr, 10) : 20;
|
|
4427
4437
|
const offset = offsetStr ? parseInt(offsetStr, 10) : 0;
|
|
4428
|
-
return json$
|
|
4438
|
+
return json$7(await service.listProducts({
|
|
4429
4439
|
categorySlug,
|
|
4430
4440
|
categoryId,
|
|
4431
4441
|
tagSlug,
|
|
@@ -4454,14 +4464,14 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4454
4464
|
if (!product) product = await service.getProductBySlug(id);
|
|
4455
4465
|
}
|
|
4456
4466
|
if (!product) return notFound$2(`Product '${id}' not found`);
|
|
4457
|
-
return json$
|
|
4467
|
+
return json$7(product);
|
|
4458
4468
|
});
|
|
4459
4469
|
ctx.registerRoute("POST", `${prefix}/products`, async (req) => {
|
|
4460
4470
|
try {
|
|
4461
4471
|
const body = await req.json();
|
|
4462
4472
|
if (!body.title) return badRequest$2("Product 'title' is required.");
|
|
4463
4473
|
if (body.price === void 0 || body.price < 0) return badRequest$2("Valid product 'price' is required.");
|
|
4464
|
-
return json$
|
|
4474
|
+
return json$7(await service.createProduct(body), 201);
|
|
4465
4475
|
} catch (err) {
|
|
4466
4476
|
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
4467
4477
|
}
|
|
@@ -4471,14 +4481,14 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4471
4481
|
const body = await req.json();
|
|
4472
4482
|
const updated = await service.updateProduct(params.id, body);
|
|
4473
4483
|
if (!updated) return notFound$2(`Product '${params.id}' not found.`);
|
|
4474
|
-
return json$
|
|
4484
|
+
return json$7(updated);
|
|
4475
4485
|
} catch (err) {
|
|
4476
4486
|
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
4477
4487
|
}
|
|
4478
4488
|
});
|
|
4479
4489
|
ctx.registerRoute("DELETE", `${prefix}/products/:id`, async (_req, { params }) => {
|
|
4480
4490
|
if (!await service.deleteProduct(params.id)) return notFound$2(`Product '${params.id}' not found.`);
|
|
4481
|
-
return json$
|
|
4491
|
+
return json$7({
|
|
4482
4492
|
success: true,
|
|
4483
4493
|
id: params.id
|
|
4484
4494
|
});
|
|
@@ -4487,7 +4497,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4487
4497
|
try {
|
|
4488
4498
|
const body = await req.json();
|
|
4489
4499
|
if (!body.filename || !body.mimeType) return badRequest$2("'filename' and 'mimeType' are required.");
|
|
4490
|
-
return json$
|
|
4500
|
+
return json$7(await service.uploadProductImage(params.id, {
|
|
4491
4501
|
filename: body.filename,
|
|
4492
4502
|
mimeType: body.mimeType,
|
|
4493
4503
|
sizeBytes: body.sizeBytes ?? 0,
|
|
@@ -4504,9 +4514,9 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4504
4514
|
});
|
|
4505
4515
|
ctx.registerRoute("GET", `${prefix}/categories`, async (_req, { url }) => {
|
|
4506
4516
|
try {
|
|
4507
|
-
if (url.searchParams.get("tree") === "true") return json$
|
|
4517
|
+
if (url.searchParams.get("tree") === "true") return json$7(await service.getCategoryTree());
|
|
4508
4518
|
const parentId = url.searchParams.get("parentId");
|
|
4509
|
-
return json$
|
|
4519
|
+
return json$7(await service.getCategories({ parentId: parentId === "null" ? null : parentId ?? void 0 }));
|
|
4510
4520
|
} catch (err) {
|
|
4511
4521
|
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
4512
4522
|
}
|
|
@@ -4515,7 +4525,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4515
4525
|
try {
|
|
4516
4526
|
const body = await req.json();
|
|
4517
4527
|
if (!body.name) return badRequest$2("Category 'name' is required.");
|
|
4518
|
-
return json$
|
|
4528
|
+
return json$7(await service.createCategory(body), 201);
|
|
4519
4529
|
} catch (err) {
|
|
4520
4530
|
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
4521
4531
|
}
|
|
@@ -4526,7 +4536,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4526
4536
|
const body = await req.json();
|
|
4527
4537
|
if (!body.title || !body.code) return badRequest$2("'title' and 'code' are required.");
|
|
4528
4538
|
if (body.value === void 0 || body.value < 0) return badRequest$2("Valid discount 'value' is required.");
|
|
4529
|
-
return json$
|
|
4539
|
+
return json$7(await service.createDiscount(body), 201);
|
|
4530
4540
|
} catch (err) {
|
|
4531
4541
|
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
4532
4542
|
}
|
|
@@ -4537,7 +4547,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4537
4547
|
if (!body.code) return badRequest$2("Discount 'code' is required.");
|
|
4538
4548
|
const subtotal = Number(body.subtotal ?? 0);
|
|
4539
4549
|
const productIds = Array.isArray(body.productIds) ? body.productIds : [];
|
|
4540
|
-
return json$
|
|
4550
|
+
return json$7(await service.validateDiscount(body.code, subtotal, productIds));
|
|
4541
4551
|
} catch (err) {
|
|
4542
4552
|
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
4543
4553
|
}
|
|
@@ -4547,7 +4557,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4547
4557
|
try {
|
|
4548
4558
|
const body = await req.json();
|
|
4549
4559
|
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$2("'items' array is required and must not be empty.");
|
|
4550
|
-
return json$
|
|
4560
|
+
return json$7(await service.calculateCart(body));
|
|
4551
4561
|
} catch (err) {
|
|
4552
4562
|
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
4553
4563
|
}
|
|
@@ -4558,7 +4568,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4558
4568
|
const body = await req.json();
|
|
4559
4569
|
if (!body.customerEmail) return badRequest$2("'customerEmail' is required.");
|
|
4560
4570
|
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$2("'items' array is required.");
|
|
4561
|
-
return json$
|
|
4571
|
+
return json$7(await service.createOrder(body), 201);
|
|
4562
4572
|
} catch (err) {
|
|
4563
4573
|
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
4564
4574
|
}
|
|
@@ -4573,7 +4583,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4573
4583
|
if (!order) order = await service.getOrderByNumber(id);
|
|
4574
4584
|
}
|
|
4575
4585
|
if (!order) return notFound$2(`Order '${id}' not found.`);
|
|
4576
|
-
return json$
|
|
4586
|
+
return json$7(order);
|
|
4577
4587
|
});
|
|
4578
4588
|
ctx.registerRoute("PATCH", `${prefix}/orders/:id/status`, async (req, { params }) => {
|
|
4579
4589
|
try {
|
|
@@ -4581,7 +4591,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
4581
4591
|
if (!body.status) return badRequest$2("New 'status' is required.");
|
|
4582
4592
|
const updated = await service.updateOrderStatus(params.id, body.status, body.note);
|
|
4583
4593
|
if (!updated) return notFound$2(`Order '${params.id}' not found.`);
|
|
4584
|
-
return json$
|
|
4594
|
+
return json$7(updated);
|
|
4585
4595
|
} catch (err) {
|
|
4586
4596
|
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
4587
4597
|
}
|
|
@@ -5817,7 +5827,7 @@ var HRMSService = class {
|
|
|
5817
5827
|
};
|
|
5818
5828
|
//#endregion
|
|
5819
5829
|
//#region src/plugins/hrms/routes.ts
|
|
5820
|
-
function json$
|
|
5830
|
+
function json$6(data, status = 200) {
|
|
5821
5831
|
return new Response(JSON.stringify(data), {
|
|
5822
5832
|
status,
|
|
5823
5833
|
headers: {
|
|
@@ -5827,10 +5837,10 @@ function json$1(data, status = 200) {
|
|
|
5827
5837
|
});
|
|
5828
5838
|
}
|
|
5829
5839
|
function badRequest$1(message) {
|
|
5830
|
-
return json$
|
|
5840
|
+
return json$6({ error: message }, 400);
|
|
5831
5841
|
}
|
|
5832
5842
|
function notFound$1(message) {
|
|
5833
|
-
return json$
|
|
5843
|
+
return json$6({ error: message }, 404);
|
|
5834
5844
|
}
|
|
5835
5845
|
function registerHRMSRoutes(ctx, service, options = {}) {
|
|
5836
5846
|
const prefix = (options.apiPrefix ?? "/api/hrms").replace(/\/+$/, "");
|
|
@@ -5841,7 +5851,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5841
5851
|
const limitStr = url.searchParams.get("limit");
|
|
5842
5852
|
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
5843
5853
|
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
5844
|
-
return json$
|
|
5854
|
+
return json$6(await service.listEmployers({
|
|
5845
5855
|
status,
|
|
5846
5856
|
page,
|
|
5847
5857
|
limit
|
|
@@ -5854,7 +5864,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5854
5864
|
try {
|
|
5855
5865
|
const body = await req.json();
|
|
5856
5866
|
if (!body.companyName) return badRequest$1("companyName is required.");
|
|
5857
|
-
return json$
|
|
5867
|
+
return json$6(await service.createEmployer(body), 201);
|
|
5858
5868
|
} catch (err) {
|
|
5859
5869
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
5860
5870
|
}
|
|
@@ -5863,7 +5873,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5863
5873
|
try {
|
|
5864
5874
|
const employer = await service.getEmployer(params.id);
|
|
5865
5875
|
if (!employer) return notFound$1(`Employer '${params.id}' not found.`);
|
|
5866
|
-
return json$
|
|
5876
|
+
return json$6(employer);
|
|
5867
5877
|
} catch (err) {
|
|
5868
5878
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
5869
5879
|
}
|
|
@@ -5873,7 +5883,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5873
5883
|
const body = await req.json();
|
|
5874
5884
|
const updated = await service.updateEmployer(params.id, body);
|
|
5875
5885
|
if (!updated) return notFound$1(`Employer '${params.id}' not found.`);
|
|
5876
|
-
return json$
|
|
5886
|
+
return json$6(updated);
|
|
5877
5887
|
} catch (err) {
|
|
5878
5888
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
5879
5889
|
}
|
|
@@ -5889,7 +5899,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5889
5899
|
const limitStr = url.searchParams.get("limit");
|
|
5890
5900
|
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
5891
5901
|
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
5892
|
-
return json$
|
|
5902
|
+
return json$6(await service.listEmployees({
|
|
5893
5903
|
employerId,
|
|
5894
5904
|
department,
|
|
5895
5905
|
employmentType,
|
|
@@ -5909,7 +5919,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5909
5919
|
if (!body.employeeNumber) return badRequest$1("employeeNumber is required.");
|
|
5910
5920
|
if (!body.firstName || !body.lastName) return badRequest$1("firstName and lastName are required.");
|
|
5911
5921
|
if (!body.email) return badRequest$1("email is required.");
|
|
5912
|
-
return json$
|
|
5922
|
+
return json$6(await service.createEmployee(body), 201);
|
|
5913
5923
|
} catch (err) {
|
|
5914
5924
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
5915
5925
|
}
|
|
@@ -5918,7 +5928,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5918
5928
|
try {
|
|
5919
5929
|
const employee = await service.getEmployee(params.id);
|
|
5920
5930
|
if (!employee) return notFound$1(`Employee '${params.id}' not found.`);
|
|
5921
|
-
return json$
|
|
5931
|
+
return json$6(employee);
|
|
5922
5932
|
} catch (err) {
|
|
5923
5933
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
5924
5934
|
}
|
|
@@ -5928,7 +5938,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5928
5938
|
const body = await req.json();
|
|
5929
5939
|
const updated = await service.updateEmployee(params.id, body);
|
|
5930
5940
|
if (!updated) return notFound$1(`Employee '${params.id}' not found.`);
|
|
5931
|
-
return json$
|
|
5941
|
+
return json$6(updated);
|
|
5932
5942
|
} catch (err) {
|
|
5933
5943
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
5934
5944
|
}
|
|
@@ -5936,7 +5946,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5936
5946
|
ctx.registerRoute("DELETE", `${prefix}/employees/:id`, async (_req, { params }) => {
|
|
5937
5947
|
try {
|
|
5938
5948
|
if (!await service.deleteEmployee(params.id)) return notFound$1(`Employee '${params.id}' not found.`);
|
|
5939
|
-
return json$
|
|
5949
|
+
return json$6({ success: true });
|
|
5940
5950
|
} catch (err) {
|
|
5941
5951
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
5942
5952
|
}
|
|
@@ -5945,7 +5955,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5945
5955
|
try {
|
|
5946
5956
|
const yearStr = url.searchParams.get("year");
|
|
5947
5957
|
const year = yearStr ? parseInt(yearStr, 10) : void 0;
|
|
5948
|
-
return json$
|
|
5958
|
+
return json$6(await service.calculateLeaveBalance(params.id, year));
|
|
5949
5959
|
} catch (err) {
|
|
5950
5960
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
5951
5961
|
}
|
|
@@ -5953,7 +5963,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5953
5963
|
ctx.registerRoute("GET", `${prefix}/employees/:id/direct-reports`, async (_req, { params }) => {
|
|
5954
5964
|
try {
|
|
5955
5965
|
const reports = await service.getDirectReports(params.id);
|
|
5956
|
-
return json$
|
|
5966
|
+
return json$6({
|
|
5957
5967
|
items: reports,
|
|
5958
5968
|
total: reports.length
|
|
5959
5969
|
});
|
|
@@ -5965,7 +5975,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5965
5975
|
try {
|
|
5966
5976
|
const body = await req.json();
|
|
5967
5977
|
if (!body.employeeId) return badRequest$1("employeeId is required.");
|
|
5968
|
-
return json$
|
|
5978
|
+
return json$6(await service.checkIn(body), 201);
|
|
5969
5979
|
} catch (err) {
|
|
5970
5980
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
5971
5981
|
}
|
|
@@ -5974,7 +5984,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5974
5984
|
try {
|
|
5975
5985
|
const body = await req.json();
|
|
5976
5986
|
if (!body.employeeId) return badRequest$1("employeeId is required.");
|
|
5977
|
-
return json$
|
|
5987
|
+
return json$6(await service.checkOut(body));
|
|
5978
5988
|
} catch (err) {
|
|
5979
5989
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
5980
5990
|
}
|
|
@@ -5991,7 +6001,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
5991
6001
|
const limitStr = url.searchParams.get("limit");
|
|
5992
6002
|
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
5993
6003
|
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
5994
|
-
return json$
|
|
6004
|
+
return json$6(await service.listAttendance({
|
|
5995
6005
|
employerId,
|
|
5996
6006
|
employeeId,
|
|
5997
6007
|
date,
|
|
@@ -6009,7 +6019,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
6009
6019
|
try {
|
|
6010
6020
|
const body = await req.json();
|
|
6011
6021
|
if (!body.employerId || !body.employeeId || !body.date || !body.checkInAt) return badRequest$1("employerId, employeeId, date, and checkInAt are required.");
|
|
6012
|
-
return json$
|
|
6022
|
+
return json$6(await service.recordAttendanceManual(body), 201);
|
|
6013
6023
|
} catch (err) {
|
|
6014
6024
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
6015
6025
|
}
|
|
@@ -6018,7 +6028,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
6018
6028
|
try {
|
|
6019
6029
|
const employerId = url.searchParams.get("employerId") ?? void 0;
|
|
6020
6030
|
const types = await service.listLeaveTypes(employerId);
|
|
6021
|
-
return json$
|
|
6031
|
+
return json$6({
|
|
6022
6032
|
items: types,
|
|
6023
6033
|
total: types.length
|
|
6024
6034
|
});
|
|
@@ -6030,7 +6040,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
6030
6040
|
try {
|
|
6031
6041
|
const body = await req.json();
|
|
6032
6042
|
if (!body.name || !body.code || body.daysAllowedPerYear === void 0) return badRequest$1("name, code, and daysAllowedPerYear are required.");
|
|
6033
|
-
return json$
|
|
6043
|
+
return json$6(await service.createLeaveType(body), 201);
|
|
6034
6044
|
} catch (err) {
|
|
6035
6045
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
6036
6046
|
}
|
|
@@ -6039,7 +6049,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
6039
6049
|
try {
|
|
6040
6050
|
const leaveType = await service.getLeaveType(params.id);
|
|
6041
6051
|
if (!leaveType) return notFound$1(`Leave type '${params.id}' not found.`);
|
|
6042
|
-
return json$
|
|
6052
|
+
return json$6(leaveType);
|
|
6043
6053
|
} catch (err) {
|
|
6044
6054
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
6045
6055
|
}
|
|
@@ -6056,7 +6066,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
6056
6066
|
const limitStr = url.searchParams.get("limit");
|
|
6057
6067
|
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
6058
6068
|
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
6059
|
-
return json$
|
|
6069
|
+
return json$6(await service.listLeaveRequests({
|
|
6060
6070
|
employerId,
|
|
6061
6071
|
employeeId,
|
|
6062
6072
|
leaveTypeId,
|
|
@@ -6073,7 +6083,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
6073
6083
|
try {
|
|
6074
6084
|
const body = await req.json();
|
|
6075
6085
|
if (!body.employeeId || !body.leaveTypeId || !body.startDate || !body.endDate) return badRequest$1("employeeId, leaveTypeId, startDate, and endDate are required.");
|
|
6076
|
-
return json$
|
|
6086
|
+
return json$6(await service.requestLeave(body), 201);
|
|
6077
6087
|
} catch (err) {
|
|
6078
6088
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
6079
6089
|
}
|
|
@@ -6081,7 +6091,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
6081
6091
|
ctx.registerRoute("POST", `${prefix}/leave-requests/:id/approve`, async (req, { params }) => {
|
|
6082
6092
|
try {
|
|
6083
6093
|
const approverId = (await req.json().catch(() => ({}))).approverId ?? "admin";
|
|
6084
|
-
return json$
|
|
6094
|
+
return json$6(await service.approveLeave({
|
|
6085
6095
|
requestId: params.id,
|
|
6086
6096
|
approverId
|
|
6087
6097
|
}));
|
|
@@ -6093,7 +6103,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
6093
6103
|
try {
|
|
6094
6104
|
const body = await req.json().catch(() => ({}));
|
|
6095
6105
|
const approverId = body.approverId ?? "admin";
|
|
6096
|
-
return json$
|
|
6106
|
+
return json$6(await service.rejectLeave({
|
|
6097
6107
|
requestId: params.id,
|
|
6098
6108
|
approverId,
|
|
6099
6109
|
reason: body.reason
|
|
@@ -6104,7 +6114,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
6104
6114
|
});
|
|
6105
6115
|
ctx.registerRoute("POST", `${prefix}/leave-requests/:id/cancel`, async (_req, { params }) => {
|
|
6106
6116
|
try {
|
|
6107
|
-
return json$
|
|
6117
|
+
return json$6(await service.cancelLeave(params.id));
|
|
6108
6118
|
} catch (err) {
|
|
6109
6119
|
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
6110
6120
|
}
|
|
@@ -7839,7 +7849,7 @@ var TransferService = class {
|
|
|
7839
7849
|
};
|
|
7840
7850
|
//#endregion
|
|
7841
7851
|
//#region src/plugins/transfer/routes.ts
|
|
7842
|
-
function json(data, status = 200, headers = {}) {
|
|
7852
|
+
function json$5(data, status = 200, headers = {}) {
|
|
7843
7853
|
return new Response(JSON.stringify(data), {
|
|
7844
7854
|
status,
|
|
7845
7855
|
headers: {
|
|
@@ -7850,16 +7860,16 @@ function json(data, status = 200, headers = {}) {
|
|
|
7850
7860
|
});
|
|
7851
7861
|
}
|
|
7852
7862
|
function badRequest(message) {
|
|
7853
|
-
return json({ error: message }, 400);
|
|
7863
|
+
return json$5({ error: message }, 400);
|
|
7854
7864
|
}
|
|
7855
7865
|
function notFound(message) {
|
|
7856
|
-
return json({ error: message }, 404);
|
|
7866
|
+
return json$5({ error: message }, 404);
|
|
7857
7867
|
}
|
|
7858
7868
|
function registerTransferRoutes(ctx, service, options = {}) {
|
|
7859
7869
|
const prefix = (options.apiPrefix ?? "/api/transfer").replace(/\/+$/, "");
|
|
7860
7870
|
ctx.registerRoute("GET", `${prefix}/collections`, async () => {
|
|
7861
7871
|
try {
|
|
7862
|
-
return json({ collections: service.listCollections() });
|
|
7872
|
+
return json$5({ collections: service.listCollections() });
|
|
7863
7873
|
} catch (err) {
|
|
7864
7874
|
return badRequest(err instanceof Error ? err.message : String(err));
|
|
7865
7875
|
}
|
|
@@ -7867,7 +7877,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
|
|
|
7867
7877
|
ctx.registerRoute("GET", `${prefix}/presets`, async (_req, { url }) => {
|
|
7868
7878
|
try {
|
|
7869
7879
|
const collectionSlug = url.searchParams.get("collection") || void 0;
|
|
7870
|
-
return json({ presets: service.getPresets(collectionSlug) });
|
|
7880
|
+
return json$5({ presets: service.getPresets(collectionSlug) });
|
|
7871
7881
|
} catch (err) {
|
|
7872
7882
|
return badRequest(err instanceof Error ? err.message : String(err));
|
|
7873
7883
|
}
|
|
@@ -7877,7 +7887,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
|
|
|
7877
7887
|
const body = await req.json();
|
|
7878
7888
|
if (!body.collectionSlug || !Array.isArray(body.fields)) return badRequest("collectionSlug and fields array are required.");
|
|
7879
7889
|
service.registerPreset(body);
|
|
7880
|
-
return json({
|
|
7890
|
+
return json$5({
|
|
7881
7891
|
success: true,
|
|
7882
7892
|
preset: body
|
|
7883
7893
|
}, 201);
|
|
@@ -7887,7 +7897,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
|
|
|
7887
7897
|
});
|
|
7888
7898
|
ctx.registerRoute("GET", `${prefix}/:collection/schema`, async (_req, { params }) => {
|
|
7889
7899
|
try {
|
|
7890
|
-
return json({
|
|
7900
|
+
return json$5({
|
|
7891
7901
|
schema: service.getCollectionSchema(params.collection),
|
|
7892
7902
|
template: service.getMappingTemplate(params.collection)
|
|
7893
7903
|
});
|
|
@@ -7898,7 +7908,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
|
|
|
7898
7908
|
ctx.registerRoute("POST", `${prefix}/inspect`, async (req) => {
|
|
7899
7909
|
try {
|
|
7900
7910
|
const payload = await extractPayload(req);
|
|
7901
|
-
return json(await service.inspectSource(payload.data, {
|
|
7911
|
+
return json$5(await service.inspectSource(payload.data, {
|
|
7902
7912
|
collectionSlug: payload.collectionSlug,
|
|
7903
7913
|
format: payload.format,
|
|
7904
7914
|
fileName: payload.fileName,
|
|
@@ -7911,7 +7921,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
|
|
|
7911
7921
|
ctx.registerRoute("POST", `${prefix}/:collection/preview`, async (req, { params }) => {
|
|
7912
7922
|
try {
|
|
7913
7923
|
const payload = await extractPayload(req);
|
|
7914
|
-
return json(await service.validateImport(params.collection, payload.data, payload.mapping, {
|
|
7924
|
+
return json$5(await service.validateImport(params.collection, payload.data, payload.mapping, {
|
|
7915
7925
|
format: payload.format,
|
|
7916
7926
|
fileName: payload.fileName,
|
|
7917
7927
|
sheetName: payload.sheetName,
|
|
@@ -7934,7 +7944,7 @@ function registerTransferRoutes(ctx, service, options = {}) {
|
|
|
7934
7944
|
authorId: payload.authorId,
|
|
7935
7945
|
revisionNote: payload.revisionNote
|
|
7936
7946
|
});
|
|
7937
|
-
return json(result, result.success ? 200 : 207);
|
|
7947
|
+
return json$5(result, result.success ? 200 : 207);
|
|
7938
7948
|
} catch (err) {
|
|
7939
7949
|
return badRequest(err instanceof Error ? err.message : String(err));
|
|
7940
7950
|
}
|
|
@@ -8162,6 +8172,2140 @@ function getTransferService(engine, options) {
|
|
|
8162
8172
|
return service;
|
|
8163
8173
|
}
|
|
8164
8174
|
//#endregion
|
|
8175
|
+
//#region src/plugins/seo/service.ts
|
|
8176
|
+
var SeoService = class {
|
|
8177
|
+
engine;
|
|
8178
|
+
options;
|
|
8179
|
+
siteUrl;
|
|
8180
|
+
constructor(engine, options = {}) {
|
|
8181
|
+
this.engine = engine;
|
|
8182
|
+
this.options = options;
|
|
8183
|
+
this.siteUrl = (options.siteUrl || "").replace(/\/+$/, "");
|
|
8184
|
+
}
|
|
8185
|
+
/**
|
|
8186
|
+
* Generate an XML sitemap for all published content items in configured collections.
|
|
8187
|
+
*/
|
|
8188
|
+
async generateSitemap() {
|
|
8189
|
+
const sitemapItems = [];
|
|
8190
|
+
if (this.siteUrl) sitemapItems.push({
|
|
8191
|
+
loc: `${this.siteUrl}/`,
|
|
8192
|
+
changefreq: "daily",
|
|
8193
|
+
priority: 1
|
|
8194
|
+
});
|
|
8195
|
+
const collectionSlugs = this.options.sitemapCollections ?? this.engine.config.collections.map((c) => c.slug);
|
|
8196
|
+
for (const slug of collectionSlugs) {
|
|
8197
|
+
if (slug.startsWith("cms_") || slug.startsWith("audit_")) continue;
|
|
8198
|
+
try {
|
|
8199
|
+
const result = await this.engine.collection(slug).find({
|
|
8200
|
+
status: "published",
|
|
8201
|
+
limit: 1e3
|
|
8202
|
+
});
|
|
8203
|
+
for (const item of result.items) {
|
|
8204
|
+
if (item.data?.noIndex === true) continue;
|
|
8205
|
+
const path = slug === "pages" ? `/${item.slug}` : `/${slug}/${item.slug}`;
|
|
8206
|
+
const loc = this.siteUrl ? `${this.siteUrl}${path}` : path;
|
|
8207
|
+
const lastmod = item.updatedAt || item.publishedAt || item.createdAt;
|
|
8208
|
+
sitemapItems.push({
|
|
8209
|
+
loc,
|
|
8210
|
+
lastmod: lastmod ? new Date(lastmod).toISOString() : void 0,
|
|
8211
|
+
changefreq: "weekly",
|
|
8212
|
+
priority: .8
|
|
8213
|
+
});
|
|
8214
|
+
}
|
|
8215
|
+
} catch {}
|
|
8216
|
+
}
|
|
8217
|
+
const xmlLines = ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"];
|
|
8218
|
+
for (const entry of sitemapItems) {
|
|
8219
|
+
xmlLines.push(" <url>");
|
|
8220
|
+
xmlLines.push(` <loc>${this.escapeXml(entry.loc)}</loc>`);
|
|
8221
|
+
if (entry.lastmod) xmlLines.push(` <lastmod>${entry.lastmod}</lastmod>`);
|
|
8222
|
+
if (entry.changefreq) xmlLines.push(` <changefreq>${entry.changefreq}</changefreq>`);
|
|
8223
|
+
if (entry.priority !== void 0) xmlLines.push(` <priority>${entry.priority.toFixed(1)}</priority>`);
|
|
8224
|
+
xmlLines.push(" </url>");
|
|
8225
|
+
}
|
|
8226
|
+
xmlLines.push("</urlset>");
|
|
8227
|
+
return xmlLines.join("\n");
|
|
8228
|
+
}
|
|
8229
|
+
/**
|
|
8230
|
+
* Generate robots.txt content adhering to search crawler specifications.
|
|
8231
|
+
*/
|
|
8232
|
+
generateRobotsTxt() {
|
|
8233
|
+
const config = this.options.robotsTxt || {};
|
|
8234
|
+
const lines = [];
|
|
8235
|
+
if (config.policies && config.policies.length > 0) for (const policy of config.policies) {
|
|
8236
|
+
lines.push(`User-agent: ${policy.userAgent}`);
|
|
8237
|
+
if (policy.allow) for (const a of policy.allow) lines.push(`Allow: ${a}`);
|
|
8238
|
+
if (policy.disallow) for (const d of policy.disallow) lines.push(`Disallow: ${d}`);
|
|
8239
|
+
lines.push("");
|
|
8240
|
+
}
|
|
8241
|
+
else {
|
|
8242
|
+
lines.push("User-agent: *");
|
|
8243
|
+
lines.push("Allow: /");
|
|
8244
|
+
lines.push("Disallow: /api/");
|
|
8245
|
+
lines.push("Disallow: /admin/");
|
|
8246
|
+
lines.push("");
|
|
8247
|
+
}
|
|
8248
|
+
const sitemap = config.sitemapUrl || (this.siteUrl ? `${this.siteUrl}/sitemap.xml` : "/sitemap.xml");
|
|
8249
|
+
lines.push(`Sitemap: ${sitemap}`);
|
|
8250
|
+
if (config.host || this.siteUrl) lines.push(`Host: ${config.host || this.siteUrl}`);
|
|
8251
|
+
return lines.join("\n").trim() + "\n";
|
|
8252
|
+
}
|
|
8253
|
+
/**
|
|
8254
|
+
* Resolve consolidated SEO metadata for a document, using smart fallbacks.
|
|
8255
|
+
*/
|
|
8256
|
+
async resolveMetadata(collectionSlug, documentOrId) {
|
|
8257
|
+
let item = null;
|
|
8258
|
+
if (typeof documentOrId === "string") item = await this.engine.collection(collectionSlug).findById(documentOrId);
|
|
8259
|
+
else item = documentOrId;
|
|
8260
|
+
if (!item) return {
|
|
8261
|
+
title: this.formatTitle(this.options.defaultTitle || "Not Found"),
|
|
8262
|
+
description: this.options.defaultDescription,
|
|
8263
|
+
robots: "noindex, nofollow"
|
|
8264
|
+
};
|
|
8265
|
+
const data = item.data || {};
|
|
8266
|
+
const rawTitle = typeof data.seoTitle === "string" && data.seoTitle.trim() ? data.seoTitle : item.title || this.options.defaultTitle || "Untitled";
|
|
8267
|
+
const title = this.formatTitle(rawTitle);
|
|
8268
|
+
let description = typeof data.seoDescription === "string" && data.seoDescription.trim() ? data.seoDescription : typeof data.excerpt === "string" && data.excerpt.trim() ? data.excerpt : this.options.defaultDescription;
|
|
8269
|
+
if (!description && typeof data.content === "string") description = this.stripHtml(data.content).slice(0, 160);
|
|
8270
|
+
const path = collectionSlug === "pages" ? `/${item.slug}` : `/${collectionSlug}/${item.slug}`;
|
|
8271
|
+
const canonicalUrl = typeof data.canonicalUrl === "string" && data.canonicalUrl.trim() ? data.canonicalUrl : this.siteUrl ? `${this.siteUrl}${path}` : path;
|
|
8272
|
+
const ogImage = typeof data.ogImage === "string" && data.ogImage.trim() ? data.ogImage : typeof data.featuredImage === "string" && data.featuredImage.trim() ? data.featuredImage : this.options.defaultOgImage;
|
|
8273
|
+
const robots = data.noIndex === true || item.status !== "published" ? "noindex, nofollow" : "index, follow";
|
|
8274
|
+
let keywords;
|
|
8275
|
+
if (typeof data.seoKeywords === "string" && data.seoKeywords.trim()) keywords = data.seoKeywords.split(",").map((k) => k.trim()).filter(Boolean);
|
|
8276
|
+
const jsonLd = data.structuredData && typeof data.structuredData === "object" ? data.structuredData : this.generateJsonLd(collectionSlug, item);
|
|
8277
|
+
return {
|
|
8278
|
+
title,
|
|
8279
|
+
description,
|
|
8280
|
+
canonicalUrl,
|
|
8281
|
+
ogTitle: title,
|
|
8282
|
+
ogDescription: description,
|
|
8283
|
+
ogImage,
|
|
8284
|
+
ogType: collectionSlug === "posts" ? "article" : "website",
|
|
8285
|
+
twitterCard: this.options.defaultTwitterCard || "summary_large_image",
|
|
8286
|
+
robots,
|
|
8287
|
+
keywords,
|
|
8288
|
+
jsonLd
|
|
8289
|
+
};
|
|
8290
|
+
}
|
|
8291
|
+
/**
|
|
8292
|
+
* Generate schema.org JSON-LD structured data for a content item.
|
|
8293
|
+
*/
|
|
8294
|
+
generateJsonLd(collectionSlug, item) {
|
|
8295
|
+
const schemaType = collectionSlug === "posts" || collectionSlug === "articles" ? "Article" : "WebPage";
|
|
8296
|
+
const path = collectionSlug === "pages" ? `/${item.slug}` : `/${collectionSlug}/${item.slug}`;
|
|
8297
|
+
const url = this.siteUrl ? `${this.siteUrl}${path}` : path;
|
|
8298
|
+
const schema = {
|
|
8299
|
+
"@context": "https://schema.org",
|
|
8300
|
+
"@type": schemaType,
|
|
8301
|
+
headline: item.title,
|
|
8302
|
+
url,
|
|
8303
|
+
datePublished: item.publishedAt || item.createdAt,
|
|
8304
|
+
dateModified: item.updatedAt || item.publishedAt || item.createdAt
|
|
8305
|
+
};
|
|
8306
|
+
if (item.data?.seoDescription) schema.description = item.data.seoDescription;
|
|
8307
|
+
else if (item.data?.excerpt) schema.description = item.data.excerpt;
|
|
8308
|
+
if (item.data?.ogImage || item.data?.featuredImage) schema.image = item.data.ogImage || item.data.featuredImage;
|
|
8309
|
+
if (item.authorId) schema.author = {
|
|
8310
|
+
"@type": "Person",
|
|
8311
|
+
name: item.authorId
|
|
8312
|
+
};
|
|
8313
|
+
return schema;
|
|
8314
|
+
}
|
|
8315
|
+
/**
|
|
8316
|
+
* Analyze text content and title for SEO optimization, readability, and keyword density.
|
|
8317
|
+
*/
|
|
8318
|
+
analyzeContent(input) {
|
|
8319
|
+
const title = (input.title || "").trim();
|
|
8320
|
+
const rawContent = (input.content || "").trim();
|
|
8321
|
+
const cleanContent = this.stripHtml(rawContent);
|
|
8322
|
+
const keyword = (input.keyword || "").trim().toLowerCase();
|
|
8323
|
+
const metaDesc = (input.metaDescription || "").trim();
|
|
8324
|
+
const wordCount = (cleanContent.match(/\b[\w'-]+\b/g) || []).length;
|
|
8325
|
+
const readingTimeMinutes = Math.max(1, Math.ceil(wordCount / 200));
|
|
8326
|
+
let keywordDensity = 0;
|
|
8327
|
+
let keywordCount = 0;
|
|
8328
|
+
if (keyword && wordCount > 0) {
|
|
8329
|
+
const lowerText = cleanContent.toLowerCase();
|
|
8330
|
+
let pos = 0;
|
|
8331
|
+
while ((pos = lowerText.indexOf(keyword, pos)) !== -1) {
|
|
8332
|
+
keywordCount++;
|
|
8333
|
+
pos += keyword.length;
|
|
8334
|
+
}
|
|
8335
|
+
keywordDensity = Number((keywordCount / wordCount * 100).toFixed(2));
|
|
8336
|
+
}
|
|
8337
|
+
const checks = [];
|
|
8338
|
+
const recommendations = [];
|
|
8339
|
+
let score = 100;
|
|
8340
|
+
if (title.length >= 30 && title.length <= 60) checks.push({
|
|
8341
|
+
name: "title_length",
|
|
8342
|
+
passed: true,
|
|
8343
|
+
message: `Title length (${title.length} chars) is optimal.`,
|
|
8344
|
+
scoreImpact: 0
|
|
8345
|
+
});
|
|
8346
|
+
else if (title.length === 0) {
|
|
8347
|
+
checks.push({
|
|
8348
|
+
name: "title_length",
|
|
8349
|
+
passed: false,
|
|
8350
|
+
message: "Title is missing.",
|
|
8351
|
+
scoreImpact: -20
|
|
8352
|
+
});
|
|
8353
|
+
score -= 20;
|
|
8354
|
+
recommendations.push("Provide a page title between 30 and 60 characters.");
|
|
8355
|
+
} else {
|
|
8356
|
+
checks.push({
|
|
8357
|
+
name: "title_length",
|
|
8358
|
+
passed: false,
|
|
8359
|
+
message: `Title is ${title.length} characters long. Recommended is 30–60 characters.`,
|
|
8360
|
+
scoreImpact: -10
|
|
8361
|
+
});
|
|
8362
|
+
score -= 10;
|
|
8363
|
+
recommendations.push(title.length < 30 ? "Expand title to at least 30 characters." : "Shorten title to under 60 characters to avoid snippet truncation.");
|
|
8364
|
+
}
|
|
8365
|
+
if (metaDesc.length >= 50 && metaDesc.length <= 160) checks.push({
|
|
8366
|
+
name: "meta_description_length",
|
|
8367
|
+
passed: true,
|
|
8368
|
+
message: `Meta description length (${metaDesc.length} chars) is optimal.`,
|
|
8369
|
+
scoreImpact: 0
|
|
8370
|
+
});
|
|
8371
|
+
else if (metaDesc.length === 0) {
|
|
8372
|
+
checks.push({
|
|
8373
|
+
name: "meta_description_length",
|
|
8374
|
+
passed: false,
|
|
8375
|
+
message: "Meta description is missing.",
|
|
8376
|
+
scoreImpact: -15
|
|
8377
|
+
});
|
|
8378
|
+
score -= 15;
|
|
8379
|
+
recommendations.push("Add a meta description between 50 and 160 characters.");
|
|
8380
|
+
} else {
|
|
8381
|
+
checks.push({
|
|
8382
|
+
name: "meta_description_length",
|
|
8383
|
+
passed: false,
|
|
8384
|
+
message: `Meta description length (${metaDesc.length} chars) is outside optimal range (50-160).`,
|
|
8385
|
+
scoreImpact: -10
|
|
8386
|
+
});
|
|
8387
|
+
score -= 10;
|
|
8388
|
+
recommendations.push("Adjust meta description length to 50–160 characters.");
|
|
8389
|
+
}
|
|
8390
|
+
if (wordCount >= 300) checks.push({
|
|
8391
|
+
name: "word_count",
|
|
8392
|
+
passed: true,
|
|
8393
|
+
message: `Content length (${wordCount} words) is sufficient for indexing.`,
|
|
8394
|
+
scoreImpact: 0
|
|
8395
|
+
});
|
|
8396
|
+
else {
|
|
8397
|
+
const impact = wordCount === 0 ? -25 : -15;
|
|
8398
|
+
checks.push({
|
|
8399
|
+
name: "word_count",
|
|
8400
|
+
passed: false,
|
|
8401
|
+
message: `Content has ${wordCount} words. At least 300 words recommended.`,
|
|
8402
|
+
scoreImpact: impact
|
|
8403
|
+
});
|
|
8404
|
+
score += impact;
|
|
8405
|
+
recommendations.push("Increase content length to at least 300 words.");
|
|
8406
|
+
}
|
|
8407
|
+
if (keyword) {
|
|
8408
|
+
const titleHasKeyword = title.toLowerCase().includes(keyword);
|
|
8409
|
+
checks.push({
|
|
8410
|
+
name: "keyword_in_title",
|
|
8411
|
+
passed: titleHasKeyword,
|
|
8412
|
+
message: titleHasKeyword ? "Focus keyword appears in the title." : `Focus keyword "${keyword}" is missing from the title.`,
|
|
8413
|
+
scoreImpact: titleHasKeyword ? 0 : -10
|
|
8414
|
+
});
|
|
8415
|
+
if (!titleHasKeyword) {
|
|
8416
|
+
score -= 10;
|
|
8417
|
+
recommendations.push(`Include target keyword "${keyword}" in the title.`);
|
|
8418
|
+
}
|
|
8419
|
+
const descHasKeyword = metaDesc.toLowerCase().includes(keyword);
|
|
8420
|
+
checks.push({
|
|
8421
|
+
name: "keyword_in_description",
|
|
8422
|
+
passed: descHasKeyword,
|
|
8423
|
+
message: descHasKeyword ? "Focus keyword appears in the meta description." : `Focus keyword "${keyword}" is missing from the meta description.`,
|
|
8424
|
+
scoreImpact: descHasKeyword ? 0 : -10
|
|
8425
|
+
});
|
|
8426
|
+
if (!descHasKeyword) {
|
|
8427
|
+
score -= 10;
|
|
8428
|
+
recommendations.push(`Include target keyword "${keyword}" in the meta description.`);
|
|
8429
|
+
}
|
|
8430
|
+
const densityOptimal = keywordDensity >= .5 && keywordDensity <= 2.5;
|
|
8431
|
+
checks.push({
|
|
8432
|
+
name: "keyword_density",
|
|
8433
|
+
passed: densityOptimal,
|
|
8434
|
+
message: `Keyword density is ${keywordDensity}%. Recommended: 0.5% - 2.5%.`,
|
|
8435
|
+
scoreImpact: densityOptimal ? 0 : -10
|
|
8436
|
+
});
|
|
8437
|
+
if (!densityOptimal) {
|
|
8438
|
+
score -= 10;
|
|
8439
|
+
recommendations.push(keywordDensity < .5 ? `Use the keyword "${keyword}" more frequently in content.` : `Reduce repetition of "${keyword}" to prevent keyword stuffing penalties.`);
|
|
8440
|
+
}
|
|
8441
|
+
}
|
|
8442
|
+
return {
|
|
8443
|
+
wordCount,
|
|
8444
|
+
readingTimeMinutes,
|
|
8445
|
+
keywordDensity,
|
|
8446
|
+
score: Math.max(0, Math.min(100, score)),
|
|
8447
|
+
checks,
|
|
8448
|
+
recommendations
|
|
8449
|
+
};
|
|
8450
|
+
}
|
|
8451
|
+
formatTitle(rawTitle) {
|
|
8452
|
+
if (typeof this.options.titleTemplate === "function") return this.options.titleTemplate(rawTitle);
|
|
8453
|
+
if (typeof this.options.titleTemplate === "string") return this.options.titleTemplate.replace("%s", rawTitle);
|
|
8454
|
+
return rawTitle;
|
|
8455
|
+
}
|
|
8456
|
+
stripHtml(html) {
|
|
8457
|
+
return html.replace(/<[^>]*>?/gm, " ").replace(/\s+/g, " ").trim();
|
|
8458
|
+
}
|
|
8459
|
+
escapeXml(unsafe) {
|
|
8460
|
+
return unsafe.replace(/[<>&'"]/g, (c) => {
|
|
8461
|
+
switch (c) {
|
|
8462
|
+
case "<": return "<";
|
|
8463
|
+
case ">": return ">";
|
|
8464
|
+
case "&": return "&";
|
|
8465
|
+
case "'": return "'";
|
|
8466
|
+
case "\"": return """;
|
|
8467
|
+
default: return c;
|
|
8468
|
+
}
|
|
8469
|
+
});
|
|
8470
|
+
}
|
|
8471
|
+
};
|
|
8472
|
+
/**
|
|
8473
|
+
* Retrieve the active SeoService instance associated with a CMSEngine.
|
|
8474
|
+
*/
|
|
8475
|
+
function getSeoService(engine, options) {
|
|
8476
|
+
if (engine.__seoService) return engine.__seoService;
|
|
8477
|
+
const service = new SeoService(engine, options);
|
|
8478
|
+
engine.__seoService = service;
|
|
8479
|
+
return service;
|
|
8480
|
+
}
|
|
8481
|
+
//#endregion
|
|
8482
|
+
//#region src/plugins/seo/routes.ts
|
|
8483
|
+
function json$4(data, status = 200) {
|
|
8484
|
+
return new Response(JSON.stringify(data), {
|
|
8485
|
+
status,
|
|
8486
|
+
headers: {
|
|
8487
|
+
"Content-Type": "application/json",
|
|
8488
|
+
"Access-Control-Allow-Origin": "*"
|
|
8489
|
+
}
|
|
8490
|
+
});
|
|
8491
|
+
}
|
|
8492
|
+
function xml(content, status = 200) {
|
|
8493
|
+
return new Response(content, {
|
|
8494
|
+
status,
|
|
8495
|
+
headers: {
|
|
8496
|
+
"Content-Type": "application/xml; charset=utf-8",
|
|
8497
|
+
"Access-Control-Allow-Origin": "*"
|
|
8498
|
+
}
|
|
8499
|
+
});
|
|
8500
|
+
}
|
|
8501
|
+
function text(content, status = 200) {
|
|
8502
|
+
return new Response(content, {
|
|
8503
|
+
status,
|
|
8504
|
+
headers: {
|
|
8505
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
8506
|
+
"Access-Control-Allow-Origin": "*"
|
|
8507
|
+
}
|
|
8508
|
+
});
|
|
8509
|
+
}
|
|
8510
|
+
function registerSeoRoutes(ctx, service, options = {}) {
|
|
8511
|
+
const prefix = (options.apiPrefix ?? "/api/seo").replace(/\/+$/, "");
|
|
8512
|
+
ctx.registerRoute("GET", "/sitemap.xml", async () => {
|
|
8513
|
+
try {
|
|
8514
|
+
return xml(await service.generateSitemap());
|
|
8515
|
+
} catch (err) {
|
|
8516
|
+
return json$4({ error: err.message || "Failed to generate sitemap" }, 500);
|
|
8517
|
+
}
|
|
8518
|
+
});
|
|
8519
|
+
ctx.registerRoute("GET", "/robots.txt", () => {
|
|
8520
|
+
try {
|
|
8521
|
+
return text(service.generateRobotsTxt());
|
|
8522
|
+
} catch (err) {
|
|
8523
|
+
return text("User-agent: *\nDisallow: /api/\n", 500);
|
|
8524
|
+
}
|
|
8525
|
+
});
|
|
8526
|
+
ctx.registerRoute("GET", `${prefix}/metadata/:collection/:id`, async (_req, { params }) => {
|
|
8527
|
+
try {
|
|
8528
|
+
const { collection, id } = params;
|
|
8529
|
+
return json$4(await service.resolveMetadata(collection, id));
|
|
8530
|
+
} catch (err) {
|
|
8531
|
+
return json$4({ error: err.message || "Failed to resolve metadata" }, 400);
|
|
8532
|
+
}
|
|
8533
|
+
});
|
|
8534
|
+
ctx.registerRoute("POST", `${prefix}/analyze`, async (req) => {
|
|
8535
|
+
try {
|
|
8536
|
+
const body = await req.json();
|
|
8537
|
+
return json$4(service.analyzeContent(body));
|
|
8538
|
+
} catch (err) {
|
|
8539
|
+
return json$4({ error: err.message || "Invalid SEO analysis request" }, 400);
|
|
8540
|
+
}
|
|
8541
|
+
});
|
|
8542
|
+
}
|
|
8543
|
+
//#endregion
|
|
8544
|
+
//#region src/plugins/seo/schemas.ts
|
|
8545
|
+
/**
|
|
8546
|
+
* Generate standard SEO field definitions to attach to or extend content collections.
|
|
8547
|
+
*/
|
|
8548
|
+
function createSeoFieldDefinitions() {
|
|
8549
|
+
return [
|
|
8550
|
+
fields.text({
|
|
8551
|
+
name: "seoTitle",
|
|
8552
|
+
label: "SEO Meta Title",
|
|
8553
|
+
description: "Custom title tag override for search engines and social cards",
|
|
8554
|
+
max: 70
|
|
8555
|
+
}),
|
|
8556
|
+
fields.text({
|
|
8557
|
+
name: "seoDescription",
|
|
8558
|
+
label: "SEO Meta Description",
|
|
8559
|
+
description: "Custom meta description for search engine snippets",
|
|
8560
|
+
max: 160
|
|
8561
|
+
}),
|
|
8562
|
+
fields.text({
|
|
8563
|
+
name: "canonicalUrl",
|
|
8564
|
+
label: "Canonical URL",
|
|
8565
|
+
description: "Authoritative canonical link URL for this content"
|
|
8566
|
+
}),
|
|
8567
|
+
fields.image({
|
|
8568
|
+
name: "ogImage",
|
|
8569
|
+
label: "OpenGraph / Social Image",
|
|
8570
|
+
description: "Featured image displayed when shared on Facebook, Twitter, LinkedIn"
|
|
8571
|
+
}),
|
|
8572
|
+
fields.boolean({
|
|
8573
|
+
name: "noIndex",
|
|
8574
|
+
label: "No Index",
|
|
8575
|
+
description: "Instruct search engines not to index this page (robots noindex)",
|
|
8576
|
+
defaultValue: false
|
|
8577
|
+
}),
|
|
8578
|
+
fields.text({
|
|
8579
|
+
name: "seoKeywords",
|
|
8580
|
+
label: "Keywords",
|
|
8581
|
+
description: "Comma-separated target keywords"
|
|
8582
|
+
}),
|
|
8583
|
+
fields.json({
|
|
8584
|
+
name: "structuredData",
|
|
8585
|
+
label: "Structured Data (JSON-LD)",
|
|
8586
|
+
description: "Custom schema.org structured data JSON override"
|
|
8587
|
+
})
|
|
8588
|
+
];
|
|
8589
|
+
}
|
|
8590
|
+
//#endregion
|
|
8591
|
+
//#region src/plugins/seo/client.ts
|
|
8592
|
+
var SeoClient = class {
|
|
8593
|
+
client;
|
|
8594
|
+
options;
|
|
8595
|
+
service;
|
|
8596
|
+
prefix;
|
|
8597
|
+
constructor(client, options = {}) {
|
|
8598
|
+
this.client = client;
|
|
8599
|
+
this.options = options;
|
|
8600
|
+
this.prefix = (options.apiPrefix ?? "/api/seo").replace(/\/+$/, "");
|
|
8601
|
+
const engine = client.getEngine();
|
|
8602
|
+
if (engine) this.service = getSeoService(engine, options);
|
|
8603
|
+
}
|
|
8604
|
+
/**
|
|
8605
|
+
* Fetch XML sitemap string.
|
|
8606
|
+
*/
|
|
8607
|
+
async getSitemapXml() {
|
|
8608
|
+
if (this.service) return this.service.generateSitemap();
|
|
8609
|
+
const res = await this.client.fetchFn?.(`${this.client.baseUrl || ""}/sitemap.xml`);
|
|
8610
|
+
if (!res) throw new Error("Fetch not available in client");
|
|
8611
|
+
return res.text();
|
|
8612
|
+
}
|
|
8613
|
+
/**
|
|
8614
|
+
* Fetch robots.txt plain text string.
|
|
8615
|
+
*/
|
|
8616
|
+
async getRobotsTxt() {
|
|
8617
|
+
if (this.service) return this.service.generateRobotsTxt();
|
|
8618
|
+
const res = await this.client.fetchFn?.(`${this.client.baseUrl || ""}/robots.txt`);
|
|
8619
|
+
if (!res) throw new Error("Fetch not available in client");
|
|
8620
|
+
return res.text();
|
|
8621
|
+
}
|
|
8622
|
+
/**
|
|
8623
|
+
* Resolve consolidated SEO metadata for a document.
|
|
8624
|
+
*/
|
|
8625
|
+
async getMetadata(collection, id) {
|
|
8626
|
+
if (this.service) return this.service.resolveMetadata(collection, id);
|
|
8627
|
+
return this.client.request(`${this.prefix}/metadata/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`);
|
|
8628
|
+
}
|
|
8629
|
+
/**
|
|
8630
|
+
* Run SEO and readability analysis on draft content.
|
|
8631
|
+
*/
|
|
8632
|
+
async analyze(input) {
|
|
8633
|
+
if (this.service) return this.service.analyzeContent(input);
|
|
8634
|
+
return this.client.request(`${this.prefix}/analyze`, {
|
|
8635
|
+
method: "POST",
|
|
8636
|
+
body: JSON.stringify(input)
|
|
8637
|
+
});
|
|
8638
|
+
}
|
|
8639
|
+
};
|
|
8640
|
+
/**
|
|
8641
|
+
* Get or create an SeoClient adapter for a CMSClient.
|
|
8642
|
+
*/
|
|
8643
|
+
function getSeoClient(client, options) {
|
|
8644
|
+
return new SeoClient(client, options);
|
|
8645
|
+
}
|
|
8646
|
+
//#endregion
|
|
8647
|
+
//#region src/plugins/seo/index.ts
|
|
8648
|
+
/**
|
|
8649
|
+
* @azlib/cms - Built-in SEO & Metadata Plugin
|
|
8650
|
+
*/
|
|
8651
|
+
/**
|
|
8652
|
+
* Built-in SEO & Metadata plugin factory for @azlib/cms.
|
|
8653
|
+
* Equips the CMS engine with dynamic XML sitemaps, robots.txt generation,
|
|
8654
|
+
* OpenGraph/Twitter social cards, JSON-LD structured data, SEO readability scoring,
|
|
8655
|
+
* and declarative schema field extensions.
|
|
8656
|
+
*/
|
|
8657
|
+
const seoPlugin = definePlugin((options) => {
|
|
8658
|
+
const opts = options || {};
|
|
8659
|
+
const extendCollections = {};
|
|
8660
|
+
if (opts.autoExtendCollections) {
|
|
8661
|
+
const targets = Array.isArray(opts.autoExtendCollections) ? opts.autoExtendCollections : ["posts", "pages"];
|
|
8662
|
+
const seoFields = createSeoFieldDefinitions();
|
|
8663
|
+
for (const slug of targets) extendCollections[slug] = seoFields;
|
|
8664
|
+
}
|
|
8665
|
+
return {
|
|
8666
|
+
name: "seo",
|
|
8667
|
+
version: "1.0.0",
|
|
8668
|
+
description: "Universal SEO and metadata plugin supporting XML sitemaps, robots.txt, OpenGraph, JSON-LD, and content auditing",
|
|
8669
|
+
extendCollections: Object.keys(extendCollections).length > 0 ? extendCollections : void 0,
|
|
8670
|
+
setup(ctx) {
|
|
8671
|
+
const service = new SeoService(ctx.engine, opts);
|
|
8672
|
+
ctx.engine.__seoService = service;
|
|
8673
|
+
if (opts.enableRoutes !== false) registerSeoRoutes(ctx, service, opts);
|
|
8674
|
+
}
|
|
8675
|
+
};
|
|
8676
|
+
});
|
|
8677
|
+
//#endregion
|
|
8678
|
+
//#region src/plugins/search/adapters/memory-search-adapter.ts
|
|
8679
|
+
var MemorySearchAdapter = class {
|
|
8680
|
+
docs = /* @__PURE__ */ new Map();
|
|
8681
|
+
invertedIndex = /* @__PURE__ */ new Map();
|
|
8682
|
+
async index(documents) {
|
|
8683
|
+
for (const doc of documents) {
|
|
8684
|
+
const docKey = `${doc.collection}:${doc.id}`;
|
|
8685
|
+
if (this.docs.has(docKey)) await this.remove(doc.collection, doc.id);
|
|
8686
|
+
this.docs.set(docKey, doc);
|
|
8687
|
+
const fieldsToIndex = {
|
|
8688
|
+
title: doc.title || "",
|
|
8689
|
+
slug: doc.slug || "",
|
|
8690
|
+
excerpt: doc.excerpt || "",
|
|
8691
|
+
content: doc.content || ""
|
|
8692
|
+
};
|
|
8693
|
+
if (doc.data) {
|
|
8694
|
+
for (const [k, v] of Object.entries(doc.data)) if (typeof v === "string" && !fieldsToIndex[k]) fieldsToIndex[k] = v;
|
|
8695
|
+
}
|
|
8696
|
+
for (const [field, text] of Object.entries(fieldsToIndex)) {
|
|
8697
|
+
const tokens = this.tokenize(text);
|
|
8698
|
+
for (const token of tokens) {
|
|
8699
|
+
let postingMap = this.invertedIndex.get(token);
|
|
8700
|
+
if (!postingMap) {
|
|
8701
|
+
postingMap = /* @__PURE__ */ new Map();
|
|
8702
|
+
this.invertedIndex.set(token, postingMap);
|
|
8703
|
+
}
|
|
8704
|
+
let entry = postingMap.get(docKey);
|
|
8705
|
+
if (!entry) {
|
|
8706
|
+
entry = {
|
|
8707
|
+
docKey,
|
|
8708
|
+
fieldHits: {}
|
|
8709
|
+
};
|
|
8710
|
+
postingMap.set(docKey, entry);
|
|
8711
|
+
}
|
|
8712
|
+
entry.fieldHits[field] = (entry.fieldHits[field] || 0) + 1;
|
|
8713
|
+
}
|
|
8714
|
+
}
|
|
8715
|
+
}
|
|
8716
|
+
}
|
|
8717
|
+
async remove(collection, id) {
|
|
8718
|
+
const docKey = `${collection}:${id}`;
|
|
8719
|
+
if (!this.docs.has(docKey)) return;
|
|
8720
|
+
this.docs.delete(docKey);
|
|
8721
|
+
for (const [token, postingMap] of this.invertedIndex.entries()) {
|
|
8722
|
+
postingMap.delete(docKey);
|
|
8723
|
+
if (postingMap.size === 0) this.invertedIndex.delete(token);
|
|
8724
|
+
}
|
|
8725
|
+
}
|
|
8726
|
+
async clear() {
|
|
8727
|
+
this.docs.clear();
|
|
8728
|
+
this.invertedIndex.clear();
|
|
8729
|
+
}
|
|
8730
|
+
async search(query) {
|
|
8731
|
+
const startTime = performance.now();
|
|
8732
|
+
const queryTokens = this.tokenize(query.q);
|
|
8733
|
+
if (queryTokens.length === 0) return {
|
|
8734
|
+
items: [],
|
|
8735
|
+
total: 0,
|
|
8736
|
+
query: query.q,
|
|
8737
|
+
tookMs: 0
|
|
8738
|
+
};
|
|
8739
|
+
const docScores = /* @__PURE__ */ new Map();
|
|
8740
|
+
const collectionFilter = query.collections ? new Set(query.collections) : null;
|
|
8741
|
+
const fieldWeights = {
|
|
8742
|
+
title: 10,
|
|
8743
|
+
slug: 6,
|
|
8744
|
+
excerpt: 3,
|
|
8745
|
+
content: 1
|
|
8746
|
+
};
|
|
8747
|
+
for (const qToken of queryTokens) for (const [indexToken, postingMap] of this.invertedIndex.entries()) {
|
|
8748
|
+
const isExact = indexToken === qToken;
|
|
8749
|
+
const isPrefix = !isExact && indexToken.startsWith(qToken);
|
|
8750
|
+
if (!isExact && !isPrefix) continue;
|
|
8751
|
+
const tokenMultiplier = isExact ? 1 : .5;
|
|
8752
|
+
for (const [docKey, entry] of postingMap.entries()) {
|
|
8753
|
+
const doc = this.docs.get(docKey);
|
|
8754
|
+
if (!doc) continue;
|
|
8755
|
+
if (collectionFilter && !collectionFilter.has(doc.collection)) continue;
|
|
8756
|
+
if (query.status && doc.status && doc.status !== query.status) continue;
|
|
8757
|
+
let score = 0;
|
|
8758
|
+
for (const [field, hitCount] of Object.entries(entry.fieldHits)) {
|
|
8759
|
+
const weight = fieldWeights[field] || 1;
|
|
8760
|
+
score += hitCount * weight * tokenMultiplier;
|
|
8761
|
+
}
|
|
8762
|
+
if (doc.title.toLowerCase().includes(query.q.toLowerCase())) score += 15;
|
|
8763
|
+
const current = docScores.get(docKey) || 0;
|
|
8764
|
+
docScores.set(docKey, current + score * (doc.boost || 1));
|
|
8765
|
+
}
|
|
8766
|
+
}
|
|
8767
|
+
const sortedEntries = Array.from(docScores.entries()).sort((a, b) => b[1] - a[1]);
|
|
8768
|
+
const offset = query.offset || 0;
|
|
8769
|
+
const limit = query.limit || 20;
|
|
8770
|
+
const items = sortedEntries.slice(offset, offset + limit).map(([docKey, score]) => {
|
|
8771
|
+
const doc = this.docs.get(docKey);
|
|
8772
|
+
return {
|
|
8773
|
+
id: doc.id,
|
|
8774
|
+
collection: doc.collection,
|
|
8775
|
+
title: doc.title,
|
|
8776
|
+
slug: doc.slug,
|
|
8777
|
+
snippet: this.generateSnippet(doc, query.q),
|
|
8778
|
+
score: Number(score.toFixed(2)),
|
|
8779
|
+
terms: doc.terms
|
|
8780
|
+
};
|
|
8781
|
+
});
|
|
8782
|
+
const tookMs = Number((performance.now() - startTime).toFixed(2));
|
|
8783
|
+
return {
|
|
8784
|
+
items,
|
|
8785
|
+
total: sortedEntries.length,
|
|
8786
|
+
query: query.q,
|
|
8787
|
+
tookMs
|
|
8788
|
+
};
|
|
8789
|
+
}
|
|
8790
|
+
generateSnippet(doc, rawQuery) {
|
|
8791
|
+
const text = doc.excerpt || doc.content;
|
|
8792
|
+
if (!text) return void 0;
|
|
8793
|
+
const queryWords = this.tokenize(rawQuery);
|
|
8794
|
+
const lower = text.toLowerCase();
|
|
8795
|
+
let bestPos = -1;
|
|
8796
|
+
for (const w of queryWords) {
|
|
8797
|
+
const pos = lower.indexOf(w);
|
|
8798
|
+
if (pos !== -1 && (bestPos === -1 || pos < bestPos)) bestPos = pos;
|
|
8799
|
+
}
|
|
8800
|
+
if (bestPos === -1) return text.slice(0, 160) + (text.length > 160 ? "..." : "");
|
|
8801
|
+
const start = Math.max(0, bestPos - 60);
|
|
8802
|
+
const end = Math.min(text.length, bestPos + 100);
|
|
8803
|
+
let snippet = text.slice(start, end).trim();
|
|
8804
|
+
if (start > 0) snippet = "..." + snippet;
|
|
8805
|
+
if (end < text.length) snippet = snippet + "...";
|
|
8806
|
+
return snippet;
|
|
8807
|
+
}
|
|
8808
|
+
tokenize(text) {
|
|
8809
|
+
if (!text) return [];
|
|
8810
|
+
return text.toLowerCase().replace(/<[^>]*>?/gm, " ").replace(/[^\p{L}\p{N}\s-]/gu, " ").split(/\s+/).map((t) => t.trim()).filter((t) => t.length >= 2);
|
|
8811
|
+
}
|
|
8812
|
+
};
|
|
8813
|
+
//#endregion
|
|
8814
|
+
//#region src/plugins/search/service.ts
|
|
8815
|
+
var SearchService = class {
|
|
8816
|
+
engine;
|
|
8817
|
+
options;
|
|
8818
|
+
adapter;
|
|
8819
|
+
constructor(engine, options = {}) {
|
|
8820
|
+
this.engine = engine;
|
|
8821
|
+
this.options = options;
|
|
8822
|
+
this.adapter = options.adapter || new MemorySearchAdapter();
|
|
8823
|
+
if (options.enableAutoIndex !== false) this.attachHooks();
|
|
8824
|
+
}
|
|
8825
|
+
getAdapter() {
|
|
8826
|
+
return this.adapter;
|
|
8827
|
+
}
|
|
8828
|
+
/**
|
|
8829
|
+
* Search across indexed CMS collections.
|
|
8830
|
+
*/
|
|
8831
|
+
async search(query) {
|
|
8832
|
+
return this.adapter.search(query);
|
|
8833
|
+
}
|
|
8834
|
+
/**
|
|
8835
|
+
* Index or update a single content item.
|
|
8836
|
+
*/
|
|
8837
|
+
async indexItem(item) {
|
|
8838
|
+
if (!this.shouldIndex(item)) {
|
|
8839
|
+
await this.removeItem(item.collection, item.id);
|
|
8840
|
+
return;
|
|
8841
|
+
}
|
|
8842
|
+
const doc = this.toSearchDocument(item);
|
|
8843
|
+
await this.adapter.index([doc]);
|
|
8844
|
+
}
|
|
8845
|
+
/**
|
|
8846
|
+
* Remove a single content item from the search index.
|
|
8847
|
+
*/
|
|
8848
|
+
async removeItem(collection, id) {
|
|
8849
|
+
await this.adapter.remove(collection, id);
|
|
8850
|
+
}
|
|
8851
|
+
/**
|
|
8852
|
+
* Reindex all published items across configured CMS collections.
|
|
8853
|
+
*/
|
|
8854
|
+
async reindexAll() {
|
|
8855
|
+
await this.adapter.clear();
|
|
8856
|
+
const documents = [];
|
|
8857
|
+
const collectionSlugs = this.getTargetCollections();
|
|
8858
|
+
for (const slug of collectionSlugs) try {
|
|
8859
|
+
const result = await this.engine.collection(slug).find({
|
|
8860
|
+
status: "published",
|
|
8861
|
+
limit: 5e3
|
|
8862
|
+
});
|
|
8863
|
+
for (const item of result.items) if (this.shouldIndex(item)) documents.push(this.toSearchDocument(item));
|
|
8864
|
+
} catch {}
|
|
8865
|
+
if (documents.length > 0) await this.adapter.index(documents);
|
|
8866
|
+
return { indexedCount: documents.length };
|
|
8867
|
+
}
|
|
8868
|
+
shouldIndex(item) {
|
|
8869
|
+
if (item.collection.startsWith("cms_") || item.collection.startsWith("audit_")) return false;
|
|
8870
|
+
if (item.status !== "published") return false;
|
|
8871
|
+
if (item.data?.noIndex === true) return false;
|
|
8872
|
+
if (this.options.collections) if (Array.isArray(this.options.collections)) {
|
|
8873
|
+
if (!this.options.collections.includes(item.collection)) return false;
|
|
8874
|
+
} else {
|
|
8875
|
+
const rule = this.options.collections[item.collection];
|
|
8876
|
+
if (!rule) return false;
|
|
8877
|
+
if (rule.filter && !rule.filter(item)) return false;
|
|
8878
|
+
}
|
|
8879
|
+
return true;
|
|
8880
|
+
}
|
|
8881
|
+
toSearchDocument(item) {
|
|
8882
|
+
const data = item.data || {};
|
|
8883
|
+
const content = typeof data.content === "string" ? data.content : typeof data.description === "string" ? data.description : void 0;
|
|
8884
|
+
const excerpt = typeof data.excerpt === "string" ? data.excerpt : typeof data.seoDescription === "string" ? data.seoDescription : void 0;
|
|
8885
|
+
return {
|
|
8886
|
+
id: item.id,
|
|
8887
|
+
collection: item.collection,
|
|
8888
|
+
title: item.title || item.slug,
|
|
8889
|
+
slug: item.slug,
|
|
8890
|
+
content,
|
|
8891
|
+
excerpt,
|
|
8892
|
+
status: item.status,
|
|
8893
|
+
terms: item.terms,
|
|
8894
|
+
data
|
|
8895
|
+
};
|
|
8896
|
+
}
|
|
8897
|
+
getTargetCollections() {
|
|
8898
|
+
if (this.options.collections) return Array.isArray(this.options.collections) ? this.options.collections : Object.keys(this.options.collections);
|
|
8899
|
+
return this.engine.config.collections.map((c) => c.slug).filter((s) => !s.startsWith("cms_") && !s.startsWith("audit_"));
|
|
8900
|
+
}
|
|
8901
|
+
attachHooks() {
|
|
8902
|
+
this.engine.hooks.addAction("cms.content_created", async (...args) => {
|
|
8903
|
+
const item = args[0];
|
|
8904
|
+
if (item) await this.indexItem(item);
|
|
8905
|
+
});
|
|
8906
|
+
this.engine.hooks.addAction("cms.content_updated", async (...args) => {
|
|
8907
|
+
const item = args[0];
|
|
8908
|
+
if (item) await this.indexItem(item);
|
|
8909
|
+
});
|
|
8910
|
+
this.engine.hooks.addAction("cms.content_published", async (...args) => {
|
|
8911
|
+
const item = args[0];
|
|
8912
|
+
if (item) await this.indexItem(item);
|
|
8913
|
+
});
|
|
8914
|
+
this.engine.hooks.addAction("cms.content_deleted", async (...args) => {
|
|
8915
|
+
const item = args[0];
|
|
8916
|
+
if (item) await this.removeItem(item.collection, item.id);
|
|
8917
|
+
});
|
|
8918
|
+
}
|
|
8919
|
+
};
|
|
8920
|
+
/**
|
|
8921
|
+
* Retrieve the active SearchService instance associated with a CMSEngine.
|
|
8922
|
+
*/
|
|
8923
|
+
function getSearchService(engine, options) {
|
|
8924
|
+
if (engine.__searchService) return engine.__searchService;
|
|
8925
|
+
const service = new SearchService(engine, options);
|
|
8926
|
+
engine.__searchService = service;
|
|
8927
|
+
return service;
|
|
8928
|
+
}
|
|
8929
|
+
//#endregion
|
|
8930
|
+
//#region src/plugins/search/routes.ts
|
|
8931
|
+
function json$3(data, status = 200) {
|
|
8932
|
+
return new Response(JSON.stringify(data), {
|
|
8933
|
+
status,
|
|
8934
|
+
headers: {
|
|
8935
|
+
"Content-Type": "application/json",
|
|
8936
|
+
"Access-Control-Allow-Origin": "*"
|
|
8937
|
+
}
|
|
8938
|
+
});
|
|
8939
|
+
}
|
|
8940
|
+
function registerSearchRoutes(ctx, service, options = {}) {
|
|
8941
|
+
const prefix = (options.apiPrefix ?? "/api/search").replace(/\/+$/, "");
|
|
8942
|
+
ctx.registerRoute("GET", prefix, async (_req, { url }) => {
|
|
8943
|
+
try {
|
|
8944
|
+
const q = url.searchParams.get("q") || "";
|
|
8945
|
+
const collectionsParam = url.searchParams.get("collections");
|
|
8946
|
+
const searchQuery = {
|
|
8947
|
+
q,
|
|
8948
|
+
collections: collectionsParam ? collectionsParam.split(",").map((c) => c.trim()).filter(Boolean) : void 0,
|
|
8949
|
+
limit: url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit"), 10) : 20,
|
|
8950
|
+
offset: url.searchParams.get("offset") ? parseInt(url.searchParams.get("offset"), 10) : 0
|
|
8951
|
+
};
|
|
8952
|
+
return json$3(await service.search(searchQuery));
|
|
8953
|
+
} catch (err) {
|
|
8954
|
+
return json$3({ error: err.message || "Failed to perform search" }, 500);
|
|
8955
|
+
}
|
|
8956
|
+
});
|
|
8957
|
+
ctx.registerRoute("POST", `${prefix}/reindex`, async () => {
|
|
8958
|
+
try {
|
|
8959
|
+
return json$3({
|
|
8960
|
+
success: true,
|
|
8961
|
+
...await service.reindexAll()
|
|
8962
|
+
});
|
|
8963
|
+
} catch (err) {
|
|
8964
|
+
return json$3({ error: err.message || "Failed to reindex search" }, 500);
|
|
8965
|
+
}
|
|
8966
|
+
});
|
|
8967
|
+
}
|
|
8968
|
+
//#endregion
|
|
8969
|
+
//#region src/plugins/search/client.ts
|
|
8970
|
+
var SearchClient = class {
|
|
8971
|
+
client;
|
|
8972
|
+
options;
|
|
8973
|
+
service;
|
|
8974
|
+
prefix;
|
|
8975
|
+
constructor(client, options = {}) {
|
|
8976
|
+
this.client = client;
|
|
8977
|
+
this.options = options;
|
|
8978
|
+
this.prefix = (options.apiPrefix ?? "/api/search").replace(/\/+$/, "");
|
|
8979
|
+
const engine = client.getEngine();
|
|
8980
|
+
if (engine) this.service = getSearchService(engine, options);
|
|
8981
|
+
}
|
|
8982
|
+
/**
|
|
8983
|
+
* Search across indexed CMS collections.
|
|
8984
|
+
*/
|
|
8985
|
+
async search(query) {
|
|
8986
|
+
if (this.service) return this.service.search(query);
|
|
8987
|
+
const params = new URLSearchParams();
|
|
8988
|
+
params.set("q", query.q);
|
|
8989
|
+
if (query.collections && query.collections.length > 0) params.set("collections", query.collections.join(","));
|
|
8990
|
+
if (query.limit !== void 0) params.set("limit", String(query.limit));
|
|
8991
|
+
if (query.offset !== void 0) params.set("offset", String(query.offset));
|
|
8992
|
+
if (query.status) params.set("status", query.status);
|
|
8993
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
8994
|
+
return this.client.request(`${this.prefix}${q}`);
|
|
8995
|
+
}
|
|
8996
|
+
/**
|
|
8997
|
+
* Reindex all content in the search index.
|
|
8998
|
+
*/
|
|
8999
|
+
async reindex() {
|
|
9000
|
+
if (this.service) return {
|
|
9001
|
+
success: true,
|
|
9002
|
+
...await this.service.reindexAll()
|
|
9003
|
+
};
|
|
9004
|
+
return this.client.request(`${this.prefix}/reindex`, { method: "POST" });
|
|
9005
|
+
}
|
|
9006
|
+
};
|
|
9007
|
+
/**
|
|
9008
|
+
* Get or create a SearchClient adapter for a CMSClient.
|
|
9009
|
+
*/
|
|
9010
|
+
function getSearchClient(client, options) {
|
|
9011
|
+
return new SearchClient(client, options);
|
|
9012
|
+
}
|
|
9013
|
+
//#endregion
|
|
9014
|
+
//#region src/plugins/search/index.ts
|
|
9015
|
+
/**
|
|
9016
|
+
* @azlib/cms - Built-in Search Indexing & Discovery Plugin
|
|
9017
|
+
*/
|
|
9018
|
+
/**
|
|
9019
|
+
* Built-in Search Indexing & Discovery plugin factory for @azlib/cms.
|
|
9020
|
+
* Equips the CMS engine with fast inverted-index full-text search, field weighting,
|
|
9021
|
+
* fuzzy/prefix matching, snippet highlights, and automatic indexing via lifecycle hooks.
|
|
9022
|
+
*/
|
|
9023
|
+
const searchPlugin = definePlugin((options) => {
|
|
9024
|
+
const opts = options || {};
|
|
9025
|
+
return {
|
|
9026
|
+
name: "search",
|
|
9027
|
+
version: "1.0.0",
|
|
9028
|
+
description: "Universal full-text search indexing and discovery plugin with inverted index, field weighting, and auto-indexing",
|
|
9029
|
+
setup(ctx) {
|
|
9030
|
+
const service = new SearchService(ctx.engine, opts);
|
|
9031
|
+
ctx.engine.__searchService = service;
|
|
9032
|
+
if (opts.enableRoutes !== false) registerSearchRoutes(ctx, service, opts);
|
|
9033
|
+
}
|
|
9034
|
+
};
|
|
9035
|
+
});
|
|
9036
|
+
//#endregion
|
|
9037
|
+
//#region src/plugins/audit-log/service.ts
|
|
9038
|
+
var AuditLogService = class {
|
|
9039
|
+
engine;
|
|
9040
|
+
options;
|
|
9041
|
+
inMemoryLogs = [];
|
|
9042
|
+
maxEntries;
|
|
9043
|
+
constructor(engine, options = {}) {
|
|
9044
|
+
this.engine = engine;
|
|
9045
|
+
this.options = options;
|
|
9046
|
+
this.maxEntries = options.maxEntries ?? 1e4;
|
|
9047
|
+
this.attachHooks();
|
|
9048
|
+
}
|
|
9049
|
+
/**
|
|
9050
|
+
* Record an audit log entry.
|
|
9051
|
+
*/
|
|
9052
|
+
async record(entry) {
|
|
9053
|
+
const id = `audit_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
9054
|
+
const fullEntry = {
|
|
9055
|
+
...entry,
|
|
9056
|
+
id,
|
|
9057
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9058
|
+
};
|
|
9059
|
+
this.inMemoryLogs.unshift(fullEntry);
|
|
9060
|
+
if (this.inMemoryLogs.length > this.maxEntries) this.inMemoryLogs.pop();
|
|
9061
|
+
try {
|
|
9062
|
+
if (this.engine.hasCollection("cms_audit_logs")) await this.engine.collection("cms_audit_logs").create({
|
|
9063
|
+
title: `[${fullEntry.action.toUpperCase()}] ${fullEntry.entityType}:${fullEntry.entityId}`,
|
|
9064
|
+
data: {
|
|
9065
|
+
action: fullEntry.action,
|
|
9066
|
+
entityType: fullEntry.entityType,
|
|
9067
|
+
collectionSlug: fullEntry.collectionSlug,
|
|
9068
|
+
entityId: fullEntry.entityId,
|
|
9069
|
+
actorId: fullEntry.actorId,
|
|
9070
|
+
actorName: fullEntry.actorName,
|
|
9071
|
+
ip: fullEntry.ip,
|
|
9072
|
+
userAgent: fullEntry.userAgent,
|
|
9073
|
+
diff: fullEntry.diff,
|
|
9074
|
+
metadata: fullEntry.metadata
|
|
9075
|
+
}
|
|
9076
|
+
});
|
|
9077
|
+
} catch {}
|
|
9078
|
+
await this.engine.hooks.doAction("audit.logged", fullEntry);
|
|
9079
|
+
return fullEntry;
|
|
9080
|
+
}
|
|
9081
|
+
/**
|
|
9082
|
+
* Query audit logs with multi-attribute filtering and pagination.
|
|
9083
|
+
*/
|
|
9084
|
+
async query(query = {}) {
|
|
9085
|
+
let filtered = [...this.inMemoryLogs];
|
|
9086
|
+
if (query.action) filtered = filtered.filter((e) => e.action === query.action);
|
|
9087
|
+
if (query.entityType) filtered = filtered.filter((e) => e.entityType === query.entityType);
|
|
9088
|
+
if (query.collectionSlug) filtered = filtered.filter((e) => e.collectionSlug === query.collectionSlug);
|
|
9089
|
+
if (query.entityId) filtered = filtered.filter((e) => e.entityId === query.entityId);
|
|
9090
|
+
if (query.actorId) filtered = filtered.filter((e) => e.actorId === query.actorId);
|
|
9091
|
+
if (query.startDate) {
|
|
9092
|
+
const start = new Date(query.startDate).getTime();
|
|
9093
|
+
filtered = filtered.filter((e) => new Date(e.timestamp).getTime() >= start);
|
|
9094
|
+
}
|
|
9095
|
+
if (query.endDate) {
|
|
9096
|
+
const end = new Date(query.endDate).getTime();
|
|
9097
|
+
filtered = filtered.filter((e) => new Date(e.timestamp).getTime() <= end);
|
|
9098
|
+
}
|
|
9099
|
+
const total = filtered.length;
|
|
9100
|
+
const limit = query.limit ?? 50;
|
|
9101
|
+
const offset = query.offset ?? 0;
|
|
9102
|
+
const items = filtered.slice(offset, offset + limit);
|
|
9103
|
+
return {
|
|
9104
|
+
items,
|
|
9105
|
+
total,
|
|
9106
|
+
limit,
|
|
9107
|
+
offset,
|
|
9108
|
+
hasMore: offset + items.length < total
|
|
9109
|
+
};
|
|
9110
|
+
}
|
|
9111
|
+
/**
|
|
9112
|
+
* Get an audit log entry by its ID.
|
|
9113
|
+
*/
|
|
9114
|
+
async getById(id) {
|
|
9115
|
+
const memoryFound = this.inMemoryLogs.find((e) => e.id === id);
|
|
9116
|
+
if (memoryFound) return memoryFound;
|
|
9117
|
+
try {
|
|
9118
|
+
if (this.engine.hasCollection("cms_audit_logs")) {
|
|
9119
|
+
const item = await this.engine.collection("cms_audit_logs").findById(id);
|
|
9120
|
+
if (item) return {
|
|
9121
|
+
id: item.id,
|
|
9122
|
+
action: item.data?.action || "unknown",
|
|
9123
|
+
entityType: item.data?.entityType || "content",
|
|
9124
|
+
collectionSlug: item.data?.collectionSlug,
|
|
9125
|
+
entityId: item.data?.entityId || item.id,
|
|
9126
|
+
actorId: item.data?.actorId,
|
|
9127
|
+
actorName: item.data?.actorName,
|
|
9128
|
+
ip: item.data?.ip,
|
|
9129
|
+
userAgent: item.data?.userAgent,
|
|
9130
|
+
diff: item.data?.diff,
|
|
9131
|
+
timestamp: item.createdAt,
|
|
9132
|
+
metadata: item.data?.metadata
|
|
9133
|
+
};
|
|
9134
|
+
}
|
|
9135
|
+
} catch {}
|
|
9136
|
+
return null;
|
|
9137
|
+
}
|
|
9138
|
+
/**
|
|
9139
|
+
* Calculate a field-level diff between two states.
|
|
9140
|
+
*/
|
|
9141
|
+
calculateDiff(before, after) {
|
|
9142
|
+
const b = before || {};
|
|
9143
|
+
const a = after || {};
|
|
9144
|
+
const allKeys = Array.from(/* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(a)]));
|
|
9145
|
+
const changedFields = [];
|
|
9146
|
+
const beforeDiff = {};
|
|
9147
|
+
const afterDiff = {};
|
|
9148
|
+
for (const key of allKeys) {
|
|
9149
|
+
const valBefore = b[key];
|
|
9150
|
+
const valAfter = a[key];
|
|
9151
|
+
if (JSON.stringify(valBefore) !== JSON.stringify(valAfter)) {
|
|
9152
|
+
changedFields.push(key);
|
|
9153
|
+
if (valBefore !== void 0) beforeDiff[key] = valBefore;
|
|
9154
|
+
if (valAfter !== void 0) afterDiff[key] = valAfter;
|
|
9155
|
+
}
|
|
9156
|
+
}
|
|
9157
|
+
return {
|
|
9158
|
+
before: Object.keys(beforeDiff).length > 0 ? beforeDiff : void 0,
|
|
9159
|
+
after: Object.keys(afterDiff).length > 0 ? afterDiff : void 0,
|
|
9160
|
+
changedFields
|
|
9161
|
+
};
|
|
9162
|
+
}
|
|
9163
|
+
shouldAuditCollection(collectionSlug) {
|
|
9164
|
+
if (collectionSlug.startsWith("cms_") || collectionSlug.startsWith("audit_")) return false;
|
|
9165
|
+
if (this.options.collections && this.options.collections.length > 0) return this.options.collections.includes(collectionSlug);
|
|
9166
|
+
return true;
|
|
9167
|
+
}
|
|
9168
|
+
attachHooks() {
|
|
9169
|
+
this.engine.hooks.addAction("cms.content_created", async (...args) => {
|
|
9170
|
+
const item = args[0];
|
|
9171
|
+
if (!item || !this.shouldAuditCollection(item.collection)) return;
|
|
9172
|
+
await this.record({
|
|
9173
|
+
action: "create",
|
|
9174
|
+
entityType: "content",
|
|
9175
|
+
collectionSlug: item.collection,
|
|
9176
|
+
entityId: item.id,
|
|
9177
|
+
actorId: item.authorId,
|
|
9178
|
+
diff: {
|
|
9179
|
+
after: {
|
|
9180
|
+
title: item.title,
|
|
9181
|
+
slug: item.slug,
|
|
9182
|
+
status: item.status,
|
|
9183
|
+
...item.data
|
|
9184
|
+
},
|
|
9185
|
+
changedFields: [
|
|
9186
|
+
"title",
|
|
9187
|
+
"slug",
|
|
9188
|
+
"status",
|
|
9189
|
+
...Object.keys(item.data || {})
|
|
9190
|
+
]
|
|
9191
|
+
},
|
|
9192
|
+
metadata: { status: item.status }
|
|
9193
|
+
});
|
|
9194
|
+
});
|
|
9195
|
+
this.engine.hooks.addAction("cms.content_updated", async (...args) => {
|
|
9196
|
+
const item = args[0];
|
|
9197
|
+
if (!item || !this.shouldAuditCollection(item.collection)) return;
|
|
9198
|
+
const action = item.status === "published" ? "publish" : "update";
|
|
9199
|
+
await this.record({
|
|
9200
|
+
action,
|
|
9201
|
+
entityType: "content",
|
|
9202
|
+
collectionSlug: item.collection,
|
|
9203
|
+
entityId: item.id,
|
|
9204
|
+
actorId: item.authorId,
|
|
9205
|
+
diff: {
|
|
9206
|
+
after: {
|
|
9207
|
+
title: item.title,
|
|
9208
|
+
slug: item.slug,
|
|
9209
|
+
status: item.status,
|
|
9210
|
+
...item.data
|
|
9211
|
+
},
|
|
9212
|
+
changedFields: [
|
|
9213
|
+
"title",
|
|
9214
|
+
"slug",
|
|
9215
|
+
"status",
|
|
9216
|
+
...Object.keys(item.data || {})
|
|
9217
|
+
]
|
|
9218
|
+
},
|
|
9219
|
+
metadata: { status: item.status }
|
|
9220
|
+
});
|
|
9221
|
+
});
|
|
9222
|
+
this.engine.hooks.addAction("cms.content_deleted", async (...args) => {
|
|
9223
|
+
const item = args[0];
|
|
9224
|
+
if (!item || !this.shouldAuditCollection(item.collection)) return;
|
|
9225
|
+
await this.record({
|
|
9226
|
+
action: "delete",
|
|
9227
|
+
entityType: "content",
|
|
9228
|
+
collectionSlug: item.collection,
|
|
9229
|
+
entityId: item.id,
|
|
9230
|
+
actorId: item.authorId,
|
|
9231
|
+
metadata: {
|
|
9232
|
+
title: item.title,
|
|
9233
|
+
slug: item.slug
|
|
9234
|
+
}
|
|
9235
|
+
});
|
|
9236
|
+
});
|
|
9237
|
+
if (this.options.recordMedia !== false) {
|
|
9238
|
+
this.engine.hooks.addAction("cms.media_uploaded", async (...args) => {
|
|
9239
|
+
const media = args[0];
|
|
9240
|
+
if (!media) return;
|
|
9241
|
+
await this.record({
|
|
9242
|
+
action: "upload",
|
|
9243
|
+
entityType: "media",
|
|
9244
|
+
entityId: media.id,
|
|
9245
|
+
actorId: media.authorId,
|
|
9246
|
+
metadata: {
|
|
9247
|
+
filename: media.filename,
|
|
9248
|
+
mimeType: media.mimeType,
|
|
9249
|
+
sizeBytes: media.sizeBytes
|
|
9250
|
+
}
|
|
9251
|
+
});
|
|
9252
|
+
});
|
|
9253
|
+
this.engine.hooks.addAction("cms.media_deleted", async (...args) => {
|
|
9254
|
+
const media = args[0];
|
|
9255
|
+
if (!media) return;
|
|
9256
|
+
await this.record({
|
|
9257
|
+
action: "delete",
|
|
9258
|
+
entityType: "media",
|
|
9259
|
+
entityId: media.id,
|
|
9260
|
+
actorId: media.authorId,
|
|
9261
|
+
metadata: { filename: media.filename }
|
|
9262
|
+
});
|
|
9263
|
+
});
|
|
9264
|
+
}
|
|
9265
|
+
if (this.options.recordTaxonomies !== false) {
|
|
9266
|
+
this.engine.hooks.addAction("cms.term_created", async (...args) => {
|
|
9267
|
+
const term = args[0];
|
|
9268
|
+
if (!term) return;
|
|
9269
|
+
await this.record({
|
|
9270
|
+
action: "create",
|
|
9271
|
+
entityType: "taxonomy",
|
|
9272
|
+
collectionSlug: term.taxonomy,
|
|
9273
|
+
entityId: term.id,
|
|
9274
|
+
metadata: {
|
|
9275
|
+
name: term.name,
|
|
9276
|
+
slug: term.slug
|
|
9277
|
+
}
|
|
9278
|
+
});
|
|
9279
|
+
});
|
|
9280
|
+
this.engine.hooks.addAction("cms.term_deleted", async (...args) => {
|
|
9281
|
+
const term = args[0];
|
|
9282
|
+
if (!term) return;
|
|
9283
|
+
await this.record({
|
|
9284
|
+
action: "delete",
|
|
9285
|
+
entityType: "taxonomy",
|
|
9286
|
+
collectionSlug: term.taxonomy,
|
|
9287
|
+
entityId: term.id,
|
|
9288
|
+
metadata: {
|
|
9289
|
+
name: term.name,
|
|
9290
|
+
slug: term.slug
|
|
9291
|
+
}
|
|
9292
|
+
});
|
|
9293
|
+
});
|
|
9294
|
+
}
|
|
9295
|
+
if (this.options.recordOptions !== false) this.engine.hooks.addAction("cms.option_updated", async (...args) => {
|
|
9296
|
+
const key = args[0];
|
|
9297
|
+
const value = args[1];
|
|
9298
|
+
if (!key) return;
|
|
9299
|
+
await this.record({
|
|
9300
|
+
action: "option_change",
|
|
9301
|
+
entityType: "option",
|
|
9302
|
+
entityId: key,
|
|
9303
|
+
diff: {
|
|
9304
|
+
after: { [key]: value },
|
|
9305
|
+
changedFields: [key]
|
|
9306
|
+
}
|
|
9307
|
+
});
|
|
9308
|
+
});
|
|
9309
|
+
}
|
|
9310
|
+
};
|
|
9311
|
+
/**
|
|
9312
|
+
* Retrieve the active AuditLogService instance associated with a CMSEngine.
|
|
9313
|
+
*/
|
|
9314
|
+
function getAuditLogService(engine, options) {
|
|
9315
|
+
if (engine.__auditLogService) return engine.__auditLogService;
|
|
9316
|
+
const service = new AuditLogService(engine, options);
|
|
9317
|
+
engine.__auditLogService = service;
|
|
9318
|
+
return service;
|
|
9319
|
+
}
|
|
9320
|
+
//#endregion
|
|
9321
|
+
//#region src/plugins/audit-log/routes.ts
|
|
9322
|
+
function json$2(data, status = 200) {
|
|
9323
|
+
return new Response(JSON.stringify(data), {
|
|
9324
|
+
status,
|
|
9325
|
+
headers: {
|
|
9326
|
+
"Content-Type": "application/json",
|
|
9327
|
+
"Access-Control-Allow-Origin": "*"
|
|
9328
|
+
}
|
|
9329
|
+
});
|
|
9330
|
+
}
|
|
9331
|
+
function registerAuditLogRoutes(ctx, service, options = {}) {
|
|
9332
|
+
const prefix = (options.apiPrefix ?? "/api/audit-logs").replace(/\/+$/, "");
|
|
9333
|
+
ctx.registerRoute("GET", prefix, async (_req, { url }) => {
|
|
9334
|
+
try {
|
|
9335
|
+
const query = {
|
|
9336
|
+
action: url.searchParams.get("action") || void 0,
|
|
9337
|
+
entityType: url.searchParams.get("entityType") || void 0,
|
|
9338
|
+
collectionSlug: url.searchParams.get("collectionSlug") || void 0,
|
|
9339
|
+
entityId: url.searchParams.get("entityId") || void 0,
|
|
9340
|
+
actorId: url.searchParams.get("actorId") || void 0,
|
|
9341
|
+
startDate: url.searchParams.get("startDate") || void 0,
|
|
9342
|
+
endDate: url.searchParams.get("endDate") || void 0,
|
|
9343
|
+
limit: url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit"), 10) : void 0,
|
|
9344
|
+
offset: url.searchParams.get("offset") ? parseInt(url.searchParams.get("offset"), 10) : void 0
|
|
9345
|
+
};
|
|
9346
|
+
return json$2(await service.query(query));
|
|
9347
|
+
} catch (err) {
|
|
9348
|
+
return json$2({ error: err.message || "Failed to query audit logs" }, 500);
|
|
9349
|
+
}
|
|
9350
|
+
});
|
|
9351
|
+
ctx.registerRoute("GET", `${prefix}/:id`, async (_req, { params }) => {
|
|
9352
|
+
try {
|
|
9353
|
+
const entry = await service.getById(params.id);
|
|
9354
|
+
if (!entry) return json$2({ error: "Audit log entry not found" }, 404);
|
|
9355
|
+
return json$2(entry);
|
|
9356
|
+
} catch (err) {
|
|
9357
|
+
return json$2({ error: err.message || "Failed to retrieve audit log" }, 500);
|
|
9358
|
+
}
|
|
9359
|
+
});
|
|
9360
|
+
}
|
|
9361
|
+
//#endregion
|
|
9362
|
+
//#region src/plugins/audit-log/schemas.ts
|
|
9363
|
+
/**
|
|
9364
|
+
* @azlib/cms - Built-in Audit Log & Activity Trail Schemas
|
|
9365
|
+
*/
|
|
9366
|
+
function createAuditLogCollection(_options = {}) {
|
|
9367
|
+
return collection({
|
|
9368
|
+
label: "Audit Logs",
|
|
9369
|
+
slug: "cms_audit_logs",
|
|
9370
|
+
timestamps: true,
|
|
9371
|
+
revisions: false,
|
|
9372
|
+
fields: [
|
|
9373
|
+
fields.text({
|
|
9374
|
+
name: "action",
|
|
9375
|
+
label: "Action",
|
|
9376
|
+
required: true
|
|
9377
|
+
}),
|
|
9378
|
+
fields.text({
|
|
9379
|
+
name: "entityType",
|
|
9380
|
+
label: "Entity Type",
|
|
9381
|
+
required: true
|
|
9382
|
+
}),
|
|
9383
|
+
fields.text({
|
|
9384
|
+
name: "collectionSlug",
|
|
9385
|
+
label: "Collection Slug"
|
|
9386
|
+
}),
|
|
9387
|
+
fields.text({
|
|
9388
|
+
name: "entityId",
|
|
9389
|
+
label: "Entity ID",
|
|
9390
|
+
required: true
|
|
9391
|
+
}),
|
|
9392
|
+
fields.text({
|
|
9393
|
+
name: "actorId",
|
|
9394
|
+
label: "Actor / User ID"
|
|
9395
|
+
}),
|
|
9396
|
+
fields.text({
|
|
9397
|
+
name: "actorName",
|
|
9398
|
+
label: "Actor Name"
|
|
9399
|
+
}),
|
|
9400
|
+
fields.text({
|
|
9401
|
+
name: "ip",
|
|
9402
|
+
label: "IP Address"
|
|
9403
|
+
}),
|
|
9404
|
+
fields.text({
|
|
9405
|
+
name: "userAgent",
|
|
9406
|
+
label: "User Agent"
|
|
9407
|
+
}),
|
|
9408
|
+
fields.json({
|
|
9409
|
+
name: "diff",
|
|
9410
|
+
label: "Change Diff"
|
|
9411
|
+
}),
|
|
9412
|
+
fields.json({
|
|
9413
|
+
name: "metadata",
|
|
9414
|
+
label: "Event Metadata"
|
|
9415
|
+
})
|
|
9416
|
+
]
|
|
9417
|
+
});
|
|
9418
|
+
}
|
|
9419
|
+
//#endregion
|
|
9420
|
+
//#region src/plugins/audit-log/client.ts
|
|
9421
|
+
var AuditLogClient = class {
|
|
9422
|
+
client;
|
|
9423
|
+
options;
|
|
9424
|
+
service;
|
|
9425
|
+
prefix;
|
|
9426
|
+
constructor(client, options = {}) {
|
|
9427
|
+
this.client = client;
|
|
9428
|
+
this.options = options;
|
|
9429
|
+
this.prefix = (options.apiPrefix ?? "/api/audit-logs").replace(/\/+$/, "");
|
|
9430
|
+
const engine = client.getEngine();
|
|
9431
|
+
if (engine) this.service = getAuditLogService(engine, options);
|
|
9432
|
+
}
|
|
9433
|
+
/**
|
|
9434
|
+
* Query recorded audit logs with multi-field filters.
|
|
9435
|
+
*/
|
|
9436
|
+
async query(query = {}) {
|
|
9437
|
+
if (this.service) return this.service.query(query);
|
|
9438
|
+
const params = new URLSearchParams();
|
|
9439
|
+
if (query.action) params.set("action", query.action);
|
|
9440
|
+
if (query.entityType) params.set("entityType", query.entityType);
|
|
9441
|
+
if (query.collectionSlug) params.set("collectionSlug", query.collectionSlug);
|
|
9442
|
+
if (query.entityId) params.set("entityId", query.entityId);
|
|
9443
|
+
if (query.actorId) params.set("actorId", query.actorId);
|
|
9444
|
+
if (query.startDate) params.set("startDate", query.startDate);
|
|
9445
|
+
if (query.endDate) params.set("endDate", query.endDate);
|
|
9446
|
+
if (query.limit !== void 0) params.set("limit", String(query.limit));
|
|
9447
|
+
if (query.offset !== void 0) params.set("offset", String(query.offset));
|
|
9448
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
9449
|
+
return this.client.request(`${this.prefix}${q}`);
|
|
9450
|
+
}
|
|
9451
|
+
/**
|
|
9452
|
+
* Fetch a single audit log entry by ID.
|
|
9453
|
+
*/
|
|
9454
|
+
async get(id) {
|
|
9455
|
+
if (this.service) return this.service.getById(id);
|
|
9456
|
+
return this.client.request(`${this.prefix}/${encodeURIComponent(id)}`);
|
|
9457
|
+
}
|
|
9458
|
+
};
|
|
9459
|
+
/**
|
|
9460
|
+
* Get or create an AuditLogClient adapter for a CMSClient.
|
|
9461
|
+
*/
|
|
9462
|
+
function getAuditLogClient(client, options) {
|
|
9463
|
+
return new AuditLogClient(client, options);
|
|
9464
|
+
}
|
|
9465
|
+
//#endregion
|
|
9466
|
+
//#region src/plugins/audit-log/index.ts
|
|
9467
|
+
/**
|
|
9468
|
+
* @azlib/cms - Built-in Audit Log & Activity Trail Plugin
|
|
9469
|
+
*/
|
|
9470
|
+
/**
|
|
9471
|
+
* Built-in Audit Log & Activity Trail plugin factory for @azlib/cms.
|
|
9472
|
+
* Equips the CMS engine with immutable enterprise change logs, mutation auditing
|
|
9473
|
+
* across content, media, taxonomies, and options, field-level diffs, and query APIs.
|
|
9474
|
+
*/
|
|
9475
|
+
const auditLogPlugin = definePlugin((options) => {
|
|
9476
|
+
const opts = options || {};
|
|
9477
|
+
return {
|
|
9478
|
+
name: "audit-log",
|
|
9479
|
+
version: "1.0.0",
|
|
9480
|
+
description: "Universal enterprise audit logging and activity trail plugin with deep diff tracking and compliance query APIs",
|
|
9481
|
+
collections: [createAuditLogCollection(opts)],
|
|
9482
|
+
setup(ctx) {
|
|
9483
|
+
const service = new AuditLogService(ctx.engine, opts);
|
|
9484
|
+
ctx.engine.__auditLogService = service;
|
|
9485
|
+
if (opts.enableRoutes !== false) registerAuditLogRoutes(ctx, service, opts);
|
|
9486
|
+
}
|
|
9487
|
+
};
|
|
9488
|
+
});
|
|
9489
|
+
//#endregion
|
|
9490
|
+
//#region src/plugins/forms/service.ts
|
|
9491
|
+
var FormsService = class {
|
|
9492
|
+
engine;
|
|
9493
|
+
options;
|
|
9494
|
+
inMemoryForms = /* @__PURE__ */ new Map();
|
|
9495
|
+
inMemorySubmissions = [];
|
|
9496
|
+
honeypotField;
|
|
9497
|
+
constructor(engine, options = {}) {
|
|
9498
|
+
this.engine = engine;
|
|
9499
|
+
this.options = options;
|
|
9500
|
+
this.honeypotField = options.honeypotFieldName || "_az_hp";
|
|
9501
|
+
}
|
|
9502
|
+
/**
|
|
9503
|
+
* Register or update a form schema programmatically or dynamically.
|
|
9504
|
+
*/
|
|
9505
|
+
async registerForm(form) {
|
|
9506
|
+
this.inMemoryForms.set(form.slug, form);
|
|
9507
|
+
try {
|
|
9508
|
+
if (this.engine.hasCollection("cms_forms")) {
|
|
9509
|
+
const existing = await this.engine.collection("cms_forms").findBySlug(form.slug);
|
|
9510
|
+
if (existing) await this.engine.collection("cms_forms").update(existing.id, {
|
|
9511
|
+
title: form.title,
|
|
9512
|
+
data: {
|
|
9513
|
+
description: form.description,
|
|
9514
|
+
fields: form.fields,
|
|
9515
|
+
successMessage: form.successMessage,
|
|
9516
|
+
redirectUrl: form.redirectUrl,
|
|
9517
|
+
notifyEmails: form.notifyEmails,
|
|
9518
|
+
isActive: form.isActive ?? true
|
|
9519
|
+
}
|
|
9520
|
+
});
|
|
9521
|
+
else await this.engine.collection("cms_forms").create({
|
|
9522
|
+
title: form.title,
|
|
9523
|
+
slug: form.slug,
|
|
9524
|
+
data: {
|
|
9525
|
+
description: form.description,
|
|
9526
|
+
fields: form.fields,
|
|
9527
|
+
successMessage: form.successMessage,
|
|
9528
|
+
redirectUrl: form.redirectUrl,
|
|
9529
|
+
notifyEmails: form.notifyEmails,
|
|
9530
|
+
isActive: form.isActive ?? true
|
|
9531
|
+
}
|
|
9532
|
+
});
|
|
9533
|
+
}
|
|
9534
|
+
} catch {}
|
|
9535
|
+
return form;
|
|
9536
|
+
}
|
|
9537
|
+
/**
|
|
9538
|
+
* Retrieve a form definition by its slug.
|
|
9539
|
+
*/
|
|
9540
|
+
async getForm(slug) {
|
|
9541
|
+
if (this.inMemoryForms.has(slug)) return this.inMemoryForms.get(slug);
|
|
9542
|
+
try {
|
|
9543
|
+
if (this.engine.hasCollection("cms_forms")) {
|
|
9544
|
+
const item = await this.engine.collection("cms_forms").findBySlug(slug);
|
|
9545
|
+
if (item) {
|
|
9546
|
+
const form = {
|
|
9547
|
+
id: item.id,
|
|
9548
|
+
slug: item.slug,
|
|
9549
|
+
title: item.title || item.slug,
|
|
9550
|
+
description: item.data?.description,
|
|
9551
|
+
fields: item.data?.fields || [],
|
|
9552
|
+
successMessage: item.data?.successMessage,
|
|
9553
|
+
redirectUrl: item.data?.redirectUrl,
|
|
9554
|
+
notifyEmails: item.data?.notifyEmails,
|
|
9555
|
+
isActive: item.data?.isActive !== false
|
|
9556
|
+
};
|
|
9557
|
+
this.inMemoryForms.set(slug, form);
|
|
9558
|
+
return form;
|
|
9559
|
+
}
|
|
9560
|
+
}
|
|
9561
|
+
} catch {}
|
|
9562
|
+
return null;
|
|
9563
|
+
}
|
|
9564
|
+
/**
|
|
9565
|
+
* List all registered forms.
|
|
9566
|
+
*/
|
|
9567
|
+
async listForms() {
|
|
9568
|
+
try {
|
|
9569
|
+
if (this.engine.hasCollection("cms_forms")) {
|
|
9570
|
+
const result = await this.engine.collection("cms_forms").find({ limit: 100 });
|
|
9571
|
+
for (const item of result.items) if (!this.inMemoryForms.has(item.slug)) this.inMemoryForms.set(item.slug, {
|
|
9572
|
+
id: item.id,
|
|
9573
|
+
slug: item.slug,
|
|
9574
|
+
title: item.title || item.slug,
|
|
9575
|
+
description: item.data?.description,
|
|
9576
|
+
fields: item.data?.fields || [],
|
|
9577
|
+
successMessage: item.data?.successMessage,
|
|
9578
|
+
redirectUrl: item.data?.redirectUrl,
|
|
9579
|
+
notifyEmails: item.data?.notifyEmails,
|
|
9580
|
+
isActive: item.data?.isActive !== false
|
|
9581
|
+
});
|
|
9582
|
+
}
|
|
9583
|
+
} catch {}
|
|
9584
|
+
return Array.from(this.inMemoryForms.values());
|
|
9585
|
+
}
|
|
9586
|
+
/**
|
|
9587
|
+
* Validate a submission payload against the form's field definitions.
|
|
9588
|
+
*/
|
|
9589
|
+
validateSubmission(form, input) {
|
|
9590
|
+
const rawData = input.data || {};
|
|
9591
|
+
const errors = {};
|
|
9592
|
+
const sanitizedData = {};
|
|
9593
|
+
const hpValue = input.honeypotValue ?? rawData[this.honeypotField];
|
|
9594
|
+
if (this.options.enableAntiSpam !== false && hpValue && String(hpValue).trim() !== "") {
|
|
9595
|
+
errors["_spam"] = "Bot submission detected.";
|
|
9596
|
+
return {
|
|
9597
|
+
valid: false,
|
|
9598
|
+
errors,
|
|
9599
|
+
sanitizedData
|
|
9600
|
+
};
|
|
9601
|
+
}
|
|
9602
|
+
for (const rule of form.fields) {
|
|
9603
|
+
const val = rawData[rule.name];
|
|
9604
|
+
if (rule.required) {
|
|
9605
|
+
if (val === void 0 || val === null || typeof val === "string" && val.trim() === "") {
|
|
9606
|
+
errors[rule.name] = `${rule.label || rule.name} is required.`;
|
|
9607
|
+
continue;
|
|
9608
|
+
}
|
|
9609
|
+
}
|
|
9610
|
+
if (val === void 0 || val === null || val === "") {
|
|
9611
|
+
if (rule.defaultValue !== void 0) sanitizedData[rule.name] = rule.defaultValue;
|
|
9612
|
+
continue;
|
|
9613
|
+
}
|
|
9614
|
+
if (rule.type === "email") {
|
|
9615
|
+
if (typeof val !== "string" || !(0, _azlib_validator.isEmail)(val.trim())) {
|
|
9616
|
+
errors[rule.name] = `${rule.label || rule.name} must be a valid email address.`;
|
|
9617
|
+
continue;
|
|
9618
|
+
}
|
|
9619
|
+
sanitizedData[rule.name] = val.trim().toLowerCase();
|
|
9620
|
+
} else if (rule.type === "url") {
|
|
9621
|
+
if (typeof val !== "string" || !(0, _azlib_validator.isURL)(val.trim())) {
|
|
9622
|
+
errors[rule.name] = `${rule.label || rule.name} must be a valid URL.`;
|
|
9623
|
+
continue;
|
|
9624
|
+
}
|
|
9625
|
+
sanitizedData[rule.name] = val.trim();
|
|
9626
|
+
} else if (rule.type === "number") {
|
|
9627
|
+
const num = Number(val);
|
|
9628
|
+
if (isNaN(num)) {
|
|
9629
|
+
errors[rule.name] = `${rule.label || rule.name} must be a valid number.`;
|
|
9630
|
+
continue;
|
|
9631
|
+
}
|
|
9632
|
+
if (rule.min !== void 0 && num < rule.min) {
|
|
9633
|
+
errors[rule.name] = `${rule.label || rule.name} must be at least ${rule.min}.`;
|
|
9634
|
+
continue;
|
|
9635
|
+
}
|
|
9636
|
+
if (rule.max !== void 0 && num > rule.max) {
|
|
9637
|
+
errors[rule.name] = `${rule.label || rule.name} must be at most ${rule.max}.`;
|
|
9638
|
+
continue;
|
|
9639
|
+
}
|
|
9640
|
+
sanitizedData[rule.name] = num;
|
|
9641
|
+
} else if (rule.type === "checkbox") sanitizedData[rule.name] = Boolean(val);
|
|
9642
|
+
else {
|
|
9643
|
+
const strVal = String(val).trim();
|
|
9644
|
+
if (rule.minLength && strVal.length < rule.minLength) {
|
|
9645
|
+
errors[rule.name] = `${rule.label || rule.name} must be at least ${rule.minLength} characters.`;
|
|
9646
|
+
continue;
|
|
9647
|
+
}
|
|
9648
|
+
if (rule.maxLength && strVal.length > rule.maxLength) {
|
|
9649
|
+
errors[rule.name] = `${rule.label || rule.name} must be at most ${rule.maxLength} characters.`;
|
|
9650
|
+
continue;
|
|
9651
|
+
}
|
|
9652
|
+
if (rule.pattern) {
|
|
9653
|
+
if (!new RegExp(rule.pattern).test(strVal)) {
|
|
9654
|
+
errors[rule.name] = `${rule.label || rule.name} format is invalid.`;
|
|
9655
|
+
continue;
|
|
9656
|
+
}
|
|
9657
|
+
}
|
|
9658
|
+
sanitizedData[rule.name] = strVal;
|
|
9659
|
+
}
|
|
9660
|
+
}
|
|
9661
|
+
return {
|
|
9662
|
+
valid: Object.keys(errors).length === 0,
|
|
9663
|
+
errors,
|
|
9664
|
+
sanitizedData
|
|
9665
|
+
};
|
|
9666
|
+
}
|
|
9667
|
+
/**
|
|
9668
|
+
* Submit data to a form by its slug.
|
|
9669
|
+
*/
|
|
9670
|
+
async submit(formSlug, input, clientMeta) {
|
|
9671
|
+
const form = await this.getForm(formSlug);
|
|
9672
|
+
if (!form) throw new Error(`[FormsService] Form '${formSlug}' not found.`);
|
|
9673
|
+
if (form.isActive === false) throw new Error(`[FormsService] Form '${formSlug}' is currently disabled.`);
|
|
9674
|
+
const validation = this.validateSubmission(form, input);
|
|
9675
|
+
if (!validation.valid) {
|
|
9676
|
+
const errorMsg = Object.values(validation.errors).join("; ");
|
|
9677
|
+
const err = /* @__PURE__ */ new Error(`Form validation failed: ${errorMsg}`);
|
|
9678
|
+
err.validationErrors = validation.errors;
|
|
9679
|
+
throw err;
|
|
9680
|
+
}
|
|
9681
|
+
const id = `sub_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
9682
|
+
const submission = {
|
|
9683
|
+
id,
|
|
9684
|
+
formSlug,
|
|
9685
|
+
data: validation.sanitizedData,
|
|
9686
|
+
ip: clientMeta?.ip,
|
|
9687
|
+
userAgent: clientMeta?.userAgent,
|
|
9688
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9689
|
+
status: "unread"
|
|
9690
|
+
};
|
|
9691
|
+
this.inMemorySubmissions.unshift(submission);
|
|
9692
|
+
try {
|
|
9693
|
+
if (this.engine.hasCollection("cms_form_submissions")) await this.engine.collection("cms_form_submissions").create({
|
|
9694
|
+
title: `Submission: ${formSlug} (${id})`,
|
|
9695
|
+
data: {
|
|
9696
|
+
formSlug,
|
|
9697
|
+
data: submission.data,
|
|
9698
|
+
ip: submission.ip,
|
|
9699
|
+
userAgent: submission.userAgent,
|
|
9700
|
+
status: submission.status
|
|
9701
|
+
}
|
|
9702
|
+
});
|
|
9703
|
+
} catch {}
|
|
9704
|
+
await this.engine.hooks.doAction("forms.submitted", {
|
|
9705
|
+
form,
|
|
9706
|
+
submission
|
|
9707
|
+
});
|
|
9708
|
+
return submission;
|
|
9709
|
+
}
|
|
9710
|
+
/**
|
|
9711
|
+
* List submissions for a specific form.
|
|
9712
|
+
*/
|
|
9713
|
+
async listSubmissions(formSlug, options) {
|
|
9714
|
+
let list = this.inMemorySubmissions.filter((s) => s.formSlug === formSlug);
|
|
9715
|
+
if (options?.status) list = list.filter((s) => s.status === options.status);
|
|
9716
|
+
const total = list.length;
|
|
9717
|
+
const limit = options?.limit ?? 50;
|
|
9718
|
+
const offset = options?.offset ?? 0;
|
|
9719
|
+
const items = list.slice(offset, offset + limit);
|
|
9720
|
+
return {
|
|
9721
|
+
items,
|
|
9722
|
+
total,
|
|
9723
|
+
limit,
|
|
9724
|
+
offset,
|
|
9725
|
+
hasMore: offset + items.length < total
|
|
9726
|
+
};
|
|
9727
|
+
}
|
|
9728
|
+
/**
|
|
9729
|
+
* Update submission status (e.g. read, spam, archived).
|
|
9730
|
+
*/
|
|
9731
|
+
async updateSubmissionStatus(id, status) {
|
|
9732
|
+
const found = this.inMemorySubmissions.find((s) => s.id === id);
|
|
9733
|
+
if (found) {
|
|
9734
|
+
found.status = status;
|
|
9735
|
+
return found;
|
|
9736
|
+
}
|
|
9737
|
+
return null;
|
|
9738
|
+
}
|
|
9739
|
+
};
|
|
9740
|
+
/**
|
|
9741
|
+
* Retrieve the active FormsService instance associated with a CMSEngine.
|
|
9742
|
+
*/
|
|
9743
|
+
function getFormsService(engine, options) {
|
|
9744
|
+
if (engine.__formsService) return engine.__formsService;
|
|
9745
|
+
const service = new FormsService(engine, options);
|
|
9746
|
+
engine.__formsService = service;
|
|
9747
|
+
return service;
|
|
9748
|
+
}
|
|
9749
|
+
//#endregion
|
|
9750
|
+
//#region src/plugins/forms/routes.ts
|
|
9751
|
+
function json$1(data, status = 200) {
|
|
9752
|
+
return new Response(JSON.stringify(data), {
|
|
9753
|
+
status,
|
|
9754
|
+
headers: {
|
|
9755
|
+
"Content-Type": "application/json",
|
|
9756
|
+
"Access-Control-Allow-Origin": "*"
|
|
9757
|
+
}
|
|
9758
|
+
});
|
|
9759
|
+
}
|
|
9760
|
+
function registerFormsRoutes(ctx, service, options = {}) {
|
|
9761
|
+
const prefix = (options.apiPrefix ?? "/api/forms").replace(/\/+$/, "");
|
|
9762
|
+
ctx.registerRoute("GET", prefix, async () => {
|
|
9763
|
+
try {
|
|
9764
|
+
return json$1((await service.listForms()).map((f) => ({
|
|
9765
|
+
slug: f.slug,
|
|
9766
|
+
title: f.title,
|
|
9767
|
+
description: f.description,
|
|
9768
|
+
fields: f.fields,
|
|
9769
|
+
isActive: f.isActive
|
|
9770
|
+
})));
|
|
9771
|
+
} catch (err) {
|
|
9772
|
+
return json$1({ error: err.message || "Failed to list forms" }, 500);
|
|
9773
|
+
}
|
|
9774
|
+
});
|
|
9775
|
+
ctx.registerRoute("GET", `${prefix}/:slug`, async (_req, { params }) => {
|
|
9776
|
+
try {
|
|
9777
|
+
const form = await service.getForm(params.slug);
|
|
9778
|
+
if (!form) return json$1({ error: "Form not found" }, 404);
|
|
9779
|
+
return json$1({
|
|
9780
|
+
slug: form.slug,
|
|
9781
|
+
title: form.title,
|
|
9782
|
+
description: form.description,
|
|
9783
|
+
fields: form.fields,
|
|
9784
|
+
successMessage: form.successMessage,
|
|
9785
|
+
redirectUrl: form.redirectUrl,
|
|
9786
|
+
isActive: form.isActive
|
|
9787
|
+
});
|
|
9788
|
+
} catch (err) {
|
|
9789
|
+
return json$1({ error: err.message || "Failed to get form" }, 500);
|
|
9790
|
+
}
|
|
9791
|
+
});
|
|
9792
|
+
ctx.registerRoute("POST", `${prefix}/:slug/submit`, async (req, { params }) => {
|
|
9793
|
+
try {
|
|
9794
|
+
const body = await req.json();
|
|
9795
|
+
const ip = req.headers.get("x-forwarded-for") || void 0;
|
|
9796
|
+
const userAgent = req.headers.get("user-agent") || void 0;
|
|
9797
|
+
const input = body && typeof body.data === "object" ? body : {
|
|
9798
|
+
data: body,
|
|
9799
|
+
honeypotValue: body?._az_hp
|
|
9800
|
+
};
|
|
9801
|
+
const submission = await service.submit(params.slug, input, {
|
|
9802
|
+
ip,
|
|
9803
|
+
userAgent
|
|
9804
|
+
});
|
|
9805
|
+
const form = await service.getForm(params.slug);
|
|
9806
|
+
return json$1({
|
|
9807
|
+
success: true,
|
|
9808
|
+
message: form?.successMessage || "Form submitted successfully.",
|
|
9809
|
+
redirectUrl: form?.redirectUrl,
|
|
9810
|
+
submissionId: submission.id
|
|
9811
|
+
}, 201);
|
|
9812
|
+
} catch (err) {
|
|
9813
|
+
return json$1({
|
|
9814
|
+
error: err.message || "Form submission failed",
|
|
9815
|
+
validationErrors: err.validationErrors
|
|
9816
|
+
}, 400);
|
|
9817
|
+
}
|
|
9818
|
+
});
|
|
9819
|
+
ctx.registerRoute("GET", `${prefix}/:slug/submissions`, async (_req, { params, url }) => {
|
|
9820
|
+
try {
|
|
9821
|
+
const status = url.searchParams.get("status") || void 0;
|
|
9822
|
+
const limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit"), 10) : void 0;
|
|
9823
|
+
const offset = url.searchParams.get("offset") ? parseInt(url.searchParams.get("offset"), 10) : void 0;
|
|
9824
|
+
return json$1(await service.listSubmissions(params.slug, {
|
|
9825
|
+
status,
|
|
9826
|
+
limit,
|
|
9827
|
+
offset
|
|
9828
|
+
}));
|
|
9829
|
+
} catch (err) {
|
|
9830
|
+
return json$1({ error: err.message || "Failed to list submissions" }, 500);
|
|
9831
|
+
}
|
|
9832
|
+
});
|
|
9833
|
+
ctx.registerRoute("PUT", `${prefix}/submissions/:id/status`, async (req, { params }) => {
|
|
9834
|
+
try {
|
|
9835
|
+
const body = await req.json();
|
|
9836
|
+
const updated = await service.updateSubmissionStatus(params.id, body.status);
|
|
9837
|
+
if (!updated) return json$1({ error: "Submission not found" }, 404);
|
|
9838
|
+
return json$1(updated);
|
|
9839
|
+
} catch (err) {
|
|
9840
|
+
return json$1({ error: err.message || "Failed to update submission status" }, 400);
|
|
9841
|
+
}
|
|
9842
|
+
});
|
|
9843
|
+
}
|
|
9844
|
+
//#endregion
|
|
9845
|
+
//#region src/plugins/forms/schemas.ts
|
|
9846
|
+
/**
|
|
9847
|
+
* @azlib/cms - Built-in Forms & Submissions Declarative Content Schemas
|
|
9848
|
+
*/
|
|
9849
|
+
/**
|
|
9850
|
+
* Forms definition collection schema.
|
|
9851
|
+
*/
|
|
9852
|
+
function createFormsCollection(_options = {}) {
|
|
9853
|
+
return collection({
|
|
9854
|
+
label: "Forms",
|
|
9855
|
+
slug: "cms_forms",
|
|
9856
|
+
timestamps: true,
|
|
9857
|
+
revisions: false,
|
|
9858
|
+
fields: [
|
|
9859
|
+
fields.text({
|
|
9860
|
+
name: "title",
|
|
9861
|
+
label: "Form Title",
|
|
9862
|
+
required: true
|
|
9863
|
+
}),
|
|
9864
|
+
fields.slug({
|
|
9865
|
+
name: "slug",
|
|
9866
|
+
label: "Form Slug",
|
|
9867
|
+
from: "title",
|
|
9868
|
+
required: true,
|
|
9869
|
+
unique: true
|
|
9870
|
+
}),
|
|
9871
|
+
fields.text({
|
|
9872
|
+
name: "description",
|
|
9873
|
+
label: "Description"
|
|
9874
|
+
}),
|
|
9875
|
+
fields.json({
|
|
9876
|
+
name: "fields",
|
|
9877
|
+
label: "Form Field Rules",
|
|
9878
|
+
required: true
|
|
9879
|
+
}),
|
|
9880
|
+
fields.text({
|
|
9881
|
+
name: "successMessage",
|
|
9882
|
+
label: "Success Message",
|
|
9883
|
+
defaultValue: "Thank you! Your submission has been received."
|
|
9884
|
+
}),
|
|
9885
|
+
fields.url({
|
|
9886
|
+
name: "redirectUrl",
|
|
9887
|
+
label: "Redirect URL on Success"
|
|
9888
|
+
}),
|
|
9889
|
+
fields.array({
|
|
9890
|
+
name: "notifyEmails",
|
|
9891
|
+
label: "Notification Emails"
|
|
9892
|
+
}),
|
|
9893
|
+
fields.boolean({
|
|
9894
|
+
name: "isActive",
|
|
9895
|
+
label: "Active",
|
|
9896
|
+
defaultValue: true
|
|
9897
|
+
})
|
|
9898
|
+
]
|
|
9899
|
+
});
|
|
9900
|
+
}
|
|
9901
|
+
/**
|
|
9902
|
+
* Form submissions collection schema.
|
|
9903
|
+
*/
|
|
9904
|
+
function createSubmissionsCollection(_options = {}) {
|
|
9905
|
+
return collection({
|
|
9906
|
+
label: "Form Submissions",
|
|
9907
|
+
slug: "cms_form_submissions",
|
|
9908
|
+
timestamps: true,
|
|
9909
|
+
revisions: false,
|
|
9910
|
+
fields: [
|
|
9911
|
+
fields.text({
|
|
9912
|
+
name: "formSlug",
|
|
9913
|
+
label: "Form Slug",
|
|
9914
|
+
required: true
|
|
9915
|
+
}),
|
|
9916
|
+
fields.json({
|
|
9917
|
+
name: "data",
|
|
9918
|
+
label: "Submitted Fields",
|
|
9919
|
+
required: true
|
|
9920
|
+
}),
|
|
9921
|
+
fields.text({
|
|
9922
|
+
name: "ip",
|
|
9923
|
+
label: "IP Address"
|
|
9924
|
+
}),
|
|
9925
|
+
fields.text({
|
|
9926
|
+
name: "userAgent",
|
|
9927
|
+
label: "User Agent"
|
|
9928
|
+
}),
|
|
9929
|
+
fields.select({
|
|
9930
|
+
name: "status",
|
|
9931
|
+
label: "Submission Status",
|
|
9932
|
+
options: [
|
|
9933
|
+
"unread",
|
|
9934
|
+
"read",
|
|
9935
|
+
"spam",
|
|
9936
|
+
"archived"
|
|
9937
|
+
],
|
|
9938
|
+
defaultValue: "unread"
|
|
9939
|
+
})
|
|
9940
|
+
]
|
|
9941
|
+
});
|
|
9942
|
+
}
|
|
9943
|
+
//#endregion
|
|
9944
|
+
//#region src/plugins/forms/client.ts
|
|
9945
|
+
var FormsClient = class {
|
|
9946
|
+
client;
|
|
9947
|
+
options;
|
|
9948
|
+
service;
|
|
9949
|
+
prefix;
|
|
9950
|
+
constructor(client, options = {}) {
|
|
9951
|
+
this.client = client;
|
|
9952
|
+
this.options = options;
|
|
9953
|
+
this.prefix = (options.apiPrefix ?? "/api/forms").replace(/\/+$/, "");
|
|
9954
|
+
const engine = client.getEngine();
|
|
9955
|
+
if (engine) this.service = getFormsService(engine, options);
|
|
9956
|
+
}
|
|
9957
|
+
/**
|
|
9958
|
+
* List public forms.
|
|
9959
|
+
*/
|
|
9960
|
+
async listForms() {
|
|
9961
|
+
if (this.service) return this.service.listForms();
|
|
9962
|
+
return this.client.request(this.prefix);
|
|
9963
|
+
}
|
|
9964
|
+
/**
|
|
9965
|
+
* Get public form schema definition by slug.
|
|
9966
|
+
*/
|
|
9967
|
+
async getForm(slug) {
|
|
9968
|
+
if (this.service) return this.service.getForm(slug);
|
|
9969
|
+
return this.client.request(`${this.prefix}/${encodeURIComponent(slug)}`);
|
|
9970
|
+
}
|
|
9971
|
+
/**
|
|
9972
|
+
* Submit data to a form.
|
|
9973
|
+
*/
|
|
9974
|
+
async submit(slug, data, honeypotValue) {
|
|
9975
|
+
if (this.service) {
|
|
9976
|
+
const submission = await this.service.submit(slug, {
|
|
9977
|
+
data,
|
|
9978
|
+
honeypotValue
|
|
9979
|
+
});
|
|
9980
|
+
const form = await this.service.getForm(slug);
|
|
9981
|
+
return {
|
|
9982
|
+
success: true,
|
|
9983
|
+
message: form?.successMessage || "Form submitted successfully.",
|
|
9984
|
+
submissionId: submission.id,
|
|
9985
|
+
redirectUrl: form?.redirectUrl
|
|
9986
|
+
};
|
|
9987
|
+
}
|
|
9988
|
+
return this.client.request(`${this.prefix}/${encodeURIComponent(slug)}/submit`, {
|
|
9989
|
+
method: "POST",
|
|
9990
|
+
body: JSON.stringify({
|
|
9991
|
+
data,
|
|
9992
|
+
honeypotValue
|
|
9993
|
+
})
|
|
9994
|
+
});
|
|
9995
|
+
}
|
|
9996
|
+
/**
|
|
9997
|
+
* List submissions for a form.
|
|
9998
|
+
*/
|
|
9999
|
+
async listSubmissions(slug, options) {
|
|
10000
|
+
if (this.service) return this.service.listSubmissions(slug, options);
|
|
10001
|
+
const params = new URLSearchParams();
|
|
10002
|
+
if (options?.status) params.set("status", options.status);
|
|
10003
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
10004
|
+
if (options?.offset !== void 0) params.set("offset", String(options.offset));
|
|
10005
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
10006
|
+
return this.client.request(`${this.prefix}/${encodeURIComponent(slug)}/submissions${q}`);
|
|
10007
|
+
}
|
|
10008
|
+
};
|
|
10009
|
+
/**
|
|
10010
|
+
* Get or create a FormsClient adapter for a CMSClient.
|
|
10011
|
+
*/
|
|
10012
|
+
function getFormsClient(client, options) {
|
|
10013
|
+
return new FormsClient(client, options);
|
|
10014
|
+
}
|
|
10015
|
+
//#endregion
|
|
10016
|
+
//#region src/plugins/forms/index.ts
|
|
10017
|
+
/**
|
|
10018
|
+
* @azlib/cms - Built-in Forms & Submissions Plugin
|
|
10019
|
+
*/
|
|
10020
|
+
/**
|
|
10021
|
+
* Built-in Forms & Submissions plugin factory for @azlib/cms.
|
|
10022
|
+
* Equips the CMS engine with dynamic form creation, schema validation,
|
|
10023
|
+
* anti-spam honeypots, submission storage, and custom Web Standard REST routes.
|
|
10024
|
+
*/
|
|
10025
|
+
const formsPlugin = definePlugin((options) => {
|
|
10026
|
+
const opts = options || {};
|
|
10027
|
+
return {
|
|
10028
|
+
name: "forms",
|
|
10029
|
+
version: "1.0.0",
|
|
10030
|
+
description: "Universal dynamic forms and submissions plugin with schema validation and honeypot anti-spam",
|
|
10031
|
+
collections: [createFormsCollection(opts), createSubmissionsCollection(opts)],
|
|
10032
|
+
setup(ctx) {
|
|
10033
|
+
const service = new FormsService(ctx.engine, opts);
|
|
10034
|
+
ctx.engine.__formsService = service;
|
|
10035
|
+
if (opts.enableRoutes !== false) registerFormsRoutes(ctx, service, opts);
|
|
10036
|
+
}
|
|
10037
|
+
};
|
|
10038
|
+
});
|
|
10039
|
+
//#endregion
|
|
10040
|
+
//#region src/plugins/revalidation/service.ts
|
|
10041
|
+
var RevalidationService = class {
|
|
10042
|
+
engine;
|
|
10043
|
+
options;
|
|
10044
|
+
endpoints = [];
|
|
10045
|
+
history = [];
|
|
10046
|
+
tagPrefix;
|
|
10047
|
+
fetchFn;
|
|
10048
|
+
constructor(engine, options = {}) {
|
|
10049
|
+
this.engine = engine;
|
|
10050
|
+
this.options = options;
|
|
10051
|
+
this.endpoints = options.endpoints ? [...options.endpoints] : [];
|
|
10052
|
+
this.tagPrefix = options.tagPrefix || "cms";
|
|
10053
|
+
this.fetchFn = globalThis.fetch?.bind(globalThis);
|
|
10054
|
+
if (options.enableAutoRevalidate !== false) this.attachHooks();
|
|
10055
|
+
}
|
|
10056
|
+
addEndpoint(endpoint) {
|
|
10057
|
+
this.endpoints.push(endpoint);
|
|
10058
|
+
}
|
|
10059
|
+
getEndpoints() {
|
|
10060
|
+
return this.endpoints;
|
|
10061
|
+
}
|
|
10062
|
+
getHistory() {
|
|
10063
|
+
return this.history;
|
|
10064
|
+
}
|
|
10065
|
+
/**
|
|
10066
|
+
* Derive cache tags for a content item.
|
|
10067
|
+
*/
|
|
10068
|
+
deriveTags(collection, id, slug) {
|
|
10069
|
+
const tags = [`${this.tagPrefix}:${collection}`];
|
|
10070
|
+
if (id) tags.push(`${this.tagPrefix}:${collection}:${id}`);
|
|
10071
|
+
if (slug) tags.push(`${this.tagPrefix}:${collection}:${slug}`);
|
|
10072
|
+
return tags;
|
|
10073
|
+
}
|
|
10074
|
+
/**
|
|
10075
|
+
* Derive frontend URL paths affected by this content change.
|
|
10076
|
+
*/
|
|
10077
|
+
derivePaths(item) {
|
|
10078
|
+
if (this.options.collectionPathMap?.[item.collection]) {
|
|
10079
|
+
const mapped = this.options.collectionPathMap[item.collection](item);
|
|
10080
|
+
return Array.isArray(mapped) ? mapped : [mapped];
|
|
10081
|
+
}
|
|
10082
|
+
if (item.collection === "pages") return item.slug === "home" || item.slug === "index" ? ["/"] : [`/${item.slug}`];
|
|
10083
|
+
return [`/${item.collection}/${item.slug}`, `/${item.collection}`];
|
|
10084
|
+
}
|
|
10085
|
+
/**
|
|
10086
|
+
* Dispatch revalidation to all configured endpoints.
|
|
10087
|
+
*/
|
|
10088
|
+
async revalidate(input) {
|
|
10089
|
+
const paths = input.paths || [];
|
|
10090
|
+
const tags = input.tags || (input.collectionSlug ? this.deriveTags(input.collectionSlug, input.itemId) : []);
|
|
10091
|
+
const payload = {
|
|
10092
|
+
event: input.event || "manual",
|
|
10093
|
+
collectionSlug: input.collectionSlug,
|
|
10094
|
+
itemId: input.itemId,
|
|
10095
|
+
paths,
|
|
10096
|
+
tags,
|
|
10097
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10098
|
+
};
|
|
10099
|
+
const results = [];
|
|
10100
|
+
for (const ep of this.endpoints) {
|
|
10101
|
+
const startTime = performance.now();
|
|
10102
|
+
try {
|
|
10103
|
+
const bodyStr = JSON.stringify(payload);
|
|
10104
|
+
const headers = {
|
|
10105
|
+
"Content-Type": "application/json",
|
|
10106
|
+
"X-Azlib-Timestamp": payload.timestamp,
|
|
10107
|
+
...ep.headers || {}
|
|
10108
|
+
};
|
|
10109
|
+
if (ep.secret) headers["X-Azlib-Signature"] = await computeHmacSignature(bodyStr, ep.secret);
|
|
10110
|
+
let status = 200;
|
|
10111
|
+
let success = true;
|
|
10112
|
+
let errorMsg;
|
|
10113
|
+
if (this.fetchFn) try {
|
|
10114
|
+
const method = ep.method || "POST";
|
|
10115
|
+
let targetUrl = ep.url;
|
|
10116
|
+
if (method === "GET") {
|
|
10117
|
+
const url = new URL(ep.url);
|
|
10118
|
+
if (paths.length > 0) url.searchParams.set("paths", paths.join(","));
|
|
10119
|
+
if (tags.length > 0) url.searchParams.set("tags", tags.join(","));
|
|
10120
|
+
if (ep.secret) url.searchParams.set("secret", ep.secret);
|
|
10121
|
+
targetUrl = url.toString();
|
|
10122
|
+
}
|
|
10123
|
+
const response = await this.fetchFn(targetUrl, {
|
|
10124
|
+
method,
|
|
10125
|
+
headers,
|
|
10126
|
+
body: method === "POST" ? bodyStr : void 0
|
|
10127
|
+
});
|
|
10128
|
+
status = response.status;
|
|
10129
|
+
success = response.ok;
|
|
10130
|
+
if (!response.ok) errorMsg = `Endpoint returned status ${response.status}`;
|
|
10131
|
+
} catch (err) {
|
|
10132
|
+
status = "error";
|
|
10133
|
+
success = false;
|
|
10134
|
+
errorMsg = err.message || "Fetch failed";
|
|
10135
|
+
}
|
|
10136
|
+
const durationMs = Number((performance.now() - startTime).toFixed(2));
|
|
10137
|
+
const res = {
|
|
10138
|
+
endpoint: ep.url,
|
|
10139
|
+
status,
|
|
10140
|
+
success,
|
|
10141
|
+
durationMs,
|
|
10142
|
+
error: errorMsg
|
|
10143
|
+
};
|
|
10144
|
+
results.push(res);
|
|
10145
|
+
this.history.unshift(res);
|
|
10146
|
+
if (this.history.length > 500) this.history.pop();
|
|
10147
|
+
} catch (err) {
|
|
10148
|
+
const durationMs = Number((performance.now() - startTime).toFixed(2));
|
|
10149
|
+
const res = {
|
|
10150
|
+
endpoint: ep.url,
|
|
10151
|
+
status: "error",
|
|
10152
|
+
success: false,
|
|
10153
|
+
durationMs,
|
|
10154
|
+
error: err.message
|
|
10155
|
+
};
|
|
10156
|
+
results.push(res);
|
|
10157
|
+
this.history.unshift(res);
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
10160
|
+
await this.engine.hooks.doAction("revalidation.dispatched", {
|
|
10161
|
+
payload,
|
|
10162
|
+
results
|
|
10163
|
+
});
|
|
10164
|
+
return results;
|
|
10165
|
+
}
|
|
10166
|
+
attachHooks() {
|
|
10167
|
+
const handleContentEvent = async (event, item) => {
|
|
10168
|
+
if (item.collection.startsWith("cms_") || item.collection.startsWith("audit_")) return;
|
|
10169
|
+
const paths = this.derivePaths(item);
|
|
10170
|
+
const tags = this.deriveTags(item.collection, item.id, item.slug);
|
|
10171
|
+
await this.revalidate({
|
|
10172
|
+
event,
|
|
10173
|
+
collectionSlug: item.collection,
|
|
10174
|
+
itemId: item.id,
|
|
10175
|
+
paths,
|
|
10176
|
+
tags
|
|
10177
|
+
});
|
|
10178
|
+
};
|
|
10179
|
+
this.engine.hooks.addAction("cms.content_created", async (...args) => {
|
|
10180
|
+
const item = args[0];
|
|
10181
|
+
if (item) await handleContentEvent("create", item);
|
|
10182
|
+
});
|
|
10183
|
+
this.engine.hooks.addAction("cms.content_updated", async (...args) => {
|
|
10184
|
+
const item = args[0];
|
|
10185
|
+
if (item) await handleContentEvent("update", item);
|
|
10186
|
+
});
|
|
10187
|
+
this.engine.hooks.addAction("cms.content_published", async (...args) => {
|
|
10188
|
+
const item = args[0];
|
|
10189
|
+
if (item) await handleContentEvent("publish", item);
|
|
10190
|
+
});
|
|
10191
|
+
this.engine.hooks.addAction("cms.content_deleted", async (...args) => {
|
|
10192
|
+
const item = args[0];
|
|
10193
|
+
if (item) await handleContentEvent("delete", item);
|
|
10194
|
+
});
|
|
10195
|
+
}
|
|
10196
|
+
};
|
|
10197
|
+
/**
|
|
10198
|
+
* Retrieve the active RevalidationService instance associated with a CMSEngine.
|
|
10199
|
+
*/
|
|
10200
|
+
function getRevalidationService(engine, options) {
|
|
10201
|
+
if (engine.__revalidationService) return engine.__revalidationService;
|
|
10202
|
+
const service = new RevalidationService(engine, options);
|
|
10203
|
+
engine.__revalidationService = service;
|
|
10204
|
+
return service;
|
|
10205
|
+
}
|
|
10206
|
+
//#endregion
|
|
10207
|
+
//#region src/plugins/revalidation/routes.ts
|
|
10208
|
+
function json(data, status = 200) {
|
|
10209
|
+
return new Response(JSON.stringify(data), {
|
|
10210
|
+
status,
|
|
10211
|
+
headers: {
|
|
10212
|
+
"Content-Type": "application/json",
|
|
10213
|
+
"Access-Control-Allow-Origin": "*"
|
|
10214
|
+
}
|
|
10215
|
+
});
|
|
10216
|
+
}
|
|
10217
|
+
function registerRevalidationRoutes(ctx, service, options = {}) {
|
|
10218
|
+
const prefix = (options.apiPrefix ?? "/api/revalidate").replace(/\/+$/, "");
|
|
10219
|
+
ctx.registerRoute("POST", prefix, async (req) => {
|
|
10220
|
+
try {
|
|
10221
|
+
const body = await req.json();
|
|
10222
|
+
const results = await service.revalidate(body);
|
|
10223
|
+
return json({
|
|
10224
|
+
success: results.every((r) => r.success),
|
|
10225
|
+
results
|
|
10226
|
+
});
|
|
10227
|
+
} catch (err) {
|
|
10228
|
+
return json({ error: err.message || "Failed to trigger revalidation" }, 500);
|
|
10229
|
+
}
|
|
10230
|
+
});
|
|
10231
|
+
ctx.registerRoute("GET", `${prefix}/history`, async () => {
|
|
10232
|
+
try {
|
|
10233
|
+
return json(service.getHistory());
|
|
10234
|
+
} catch (err) {
|
|
10235
|
+
return json({ error: err.message || "Failed to fetch revalidation history" }, 500);
|
|
10236
|
+
}
|
|
10237
|
+
});
|
|
10238
|
+
}
|
|
10239
|
+
//#endregion
|
|
10240
|
+
//#region src/plugins/revalidation/client.ts
|
|
10241
|
+
var RevalidationClient = class {
|
|
10242
|
+
client;
|
|
10243
|
+
options;
|
|
10244
|
+
service;
|
|
10245
|
+
prefix;
|
|
10246
|
+
constructor(client, options = {}) {
|
|
10247
|
+
this.client = client;
|
|
10248
|
+
this.options = options;
|
|
10249
|
+
this.prefix = (options.apiPrefix ?? "/api/revalidate").replace(/\/+$/, "");
|
|
10250
|
+
const engine = client.getEngine();
|
|
10251
|
+
if (engine) this.service = getRevalidationService(engine, options);
|
|
10252
|
+
}
|
|
10253
|
+
/**
|
|
10254
|
+
* Manually trigger cache invalidation and revalidation for paths or tags.
|
|
10255
|
+
*/
|
|
10256
|
+
async revalidate(input) {
|
|
10257
|
+
if (this.service) {
|
|
10258
|
+
const results = await this.service.revalidate(input);
|
|
10259
|
+
return {
|
|
10260
|
+
success: results.every((r) => r.success),
|
|
10261
|
+
results
|
|
10262
|
+
};
|
|
10263
|
+
}
|
|
10264
|
+
return this.client.request(this.prefix, {
|
|
10265
|
+
method: "POST",
|
|
10266
|
+
body: JSON.stringify(input)
|
|
10267
|
+
});
|
|
10268
|
+
}
|
|
10269
|
+
/**
|
|
10270
|
+
* Fetch recent revalidation webhook delivery logs.
|
|
10271
|
+
*/
|
|
10272
|
+
async getHistory() {
|
|
10273
|
+
if (this.service) return [...this.service.getHistory()];
|
|
10274
|
+
return this.client.request(`${this.prefix}/history`);
|
|
10275
|
+
}
|
|
10276
|
+
};
|
|
10277
|
+
/**
|
|
10278
|
+
* Get or create a RevalidationClient adapter for a CMSClient.
|
|
10279
|
+
*/
|
|
10280
|
+
function getRevalidationClient(client, options) {
|
|
10281
|
+
return new RevalidationClient(client, options);
|
|
10282
|
+
}
|
|
10283
|
+
//#endregion
|
|
10284
|
+
//#region src/plugins/revalidation/index.ts
|
|
10285
|
+
/**
|
|
10286
|
+
* @azlib/cms - Built-in Frontend Cache & Revalidation Plugin
|
|
10287
|
+
*/
|
|
10288
|
+
/**
|
|
10289
|
+
* Built-in Frontend Cache & Revalidation plugin factory for @azlib/cms.
|
|
10290
|
+
* Automatically invalidates edge caches and triggers on-demand revalidation
|
|
10291
|
+
* (Next.js, Vercel, Cloudflare, Astro) with HMAC-signed webhooks and manual APIs.
|
|
10292
|
+
*/
|
|
10293
|
+
const revalidationPlugin = definePlugin((options) => {
|
|
10294
|
+
const opts = options || {};
|
|
10295
|
+
return {
|
|
10296
|
+
name: "revalidation",
|
|
10297
|
+
version: "1.0.0",
|
|
10298
|
+
description: "Universal frontend cache invalidation and ISR revalidation plugin with HMAC signatures",
|
|
10299
|
+
setup(ctx) {
|
|
10300
|
+
const service = new RevalidationService(ctx.engine, opts);
|
|
10301
|
+
ctx.engine.__revalidationService = service;
|
|
10302
|
+
if (opts.enableRoutes !== false) registerRevalidationRoutes(ctx, service, opts);
|
|
10303
|
+
}
|
|
10304
|
+
};
|
|
10305
|
+
});
|
|
10306
|
+
//#endregion
|
|
10307
|
+
exports.AuditLogClient = AuditLogClient;
|
|
10308
|
+
exports.AuditLogService = AuditLogService;
|
|
8165
10309
|
exports.AuthService = AuthService;
|
|
8166
10310
|
exports.CMSClient = CMSClient;
|
|
8167
10311
|
exports.CMSEngine = CMSEngine;
|
|
@@ -8172,27 +10316,38 @@ exports.DEFAULT_ROLE_CAPABILITIES = DEFAULT_ROLE_CAPABILITIES;
|
|
|
8172
10316
|
exports.DiskMediaStorageDriver = DiskMediaStorageDriver;
|
|
8173
10317
|
exports.EcommerceClient = EcommerceClient;
|
|
8174
10318
|
exports.EcommerceService = EcommerceService;
|
|
10319
|
+
exports.FormsClient = FormsClient;
|
|
10320
|
+
exports.FormsService = FormsService;
|
|
8175
10321
|
exports.HRMSClient = HRMSClient;
|
|
8176
10322
|
exports.HRMSService = HRMSService;
|
|
8177
10323
|
exports.HRMS_EMPLOYEE_TRANSFER_PRESET = HRMS_EMPLOYEE_TRANSFER_PRESET;
|
|
8178
10324
|
exports.HRMS_EMPLOYER_TRANSFER_PRESET = HRMS_EMPLOYER_TRANSFER_PRESET;
|
|
8179
10325
|
exports.HooksManager = HooksManager;
|
|
8180
10326
|
exports.MediaManager = MediaManager;
|
|
10327
|
+
exports.MemorySearchAdapter = MemorySearchAdapter;
|
|
8181
10328
|
exports.MemoryStorageAdapter = MemoryStorageAdapter;
|
|
8182
10329
|
exports.OptionsManager = OptionsManager;
|
|
8183
10330
|
exports.PersistenceStorageAdapter = PersistenceStorageAdapter;
|
|
8184
10331
|
exports.PreviewManager = PreviewManager;
|
|
8185
10332
|
exports.RBACManager = RBACManager;
|
|
10333
|
+
exports.RevalidationClient = RevalidationClient;
|
|
10334
|
+
exports.RevalidationService = RevalidationService;
|
|
8186
10335
|
exports.RevisionManager = RevisionManager;
|
|
10336
|
+
exports.SearchClient = SearchClient;
|
|
10337
|
+
exports.SearchService = SearchService;
|
|
10338
|
+
exports.SeoClient = SeoClient;
|
|
10339
|
+
exports.SeoService = SeoService;
|
|
8187
10340
|
exports.TaxonomyManager = TaxonomyManager;
|
|
8188
10341
|
exports.TransferClient = TransferClient;
|
|
8189
10342
|
exports.TransferService = TransferService;
|
|
8190
10343
|
exports.VALID_STATUS_TRANSITIONS = VALID_STATUS_TRANSITIONS;
|
|
8191
10344
|
exports.WebhookManager = WebhookManager;
|
|
8192
10345
|
exports.applyFieldTransform = applyFieldTransform;
|
|
10346
|
+
exports.auditLogPlugin = auditLogPlugin;
|
|
8193
10347
|
exports.collection = collection;
|
|
8194
10348
|
exports.computeHmacSignature = computeHmacSignature;
|
|
8195
10349
|
exports.createAttendanceCollection = createAttendanceCollection;
|
|
10350
|
+
exports.createAuditLogCollection = createAuditLogCollection;
|
|
8196
10351
|
exports.createCMSEngine = createCMSEngine;
|
|
8197
10352
|
exports.createCMSRouter = createCMSRouter;
|
|
8198
10353
|
exports.createCmsClient = createCmsClient;
|
|
@@ -8200,22 +10355,36 @@ exports.createDiscountCollection = createDiscountCollection;
|
|
|
8200
10355
|
exports.createEcommerceTaxonomies = createEcommerceTaxonomies;
|
|
8201
10356
|
exports.createEmployeeCollection = createEmployeeCollection;
|
|
8202
10357
|
exports.createEmployerCollection = createEmployerCollection;
|
|
10358
|
+
exports.createFormsCollection = createFormsCollection;
|
|
8203
10359
|
exports.createHRMSTaxonomies = createHRMSTaxonomies;
|
|
8204
10360
|
exports.createLeaveRequestCollection = createLeaveRequestCollection;
|
|
8205
10361
|
exports.createLeaveTypeCollection = createLeaveTypeCollection;
|
|
8206
10362
|
exports.createOrderCollection = createOrderCollection;
|
|
8207
10363
|
exports.createProductCollection = createProductCollection;
|
|
10364
|
+
exports.createSeoFieldDefinitions = createSeoFieldDefinitions;
|
|
10365
|
+
exports.createSubmissionsCollection = createSubmissionsCollection;
|
|
8208
10366
|
exports.defaultHooks = defaultHooks;
|
|
8209
10367
|
exports.defineConfig = defineConfig;
|
|
8210
10368
|
exports.definePlugin = definePlugin;
|
|
8211
10369
|
exports.detectFormat = detectFormat;
|
|
8212
10370
|
exports.ecommercePlugin = ecommercePlugin;
|
|
8213
10371
|
exports.fields = fields;
|
|
10372
|
+
exports.formsPlugin = formsPlugin;
|
|
8214
10373
|
exports.generateSuggestedMapping = generateSuggestedMapping;
|
|
10374
|
+
exports.getAuditLogClient = getAuditLogClient;
|
|
10375
|
+
exports.getAuditLogService = getAuditLogService;
|
|
8215
10376
|
exports.getEcommerceClient = getEcommerceClient;
|
|
8216
10377
|
exports.getEcommerceService = getEcommerceService;
|
|
10378
|
+
exports.getFormsClient = getFormsClient;
|
|
10379
|
+
exports.getFormsService = getFormsService;
|
|
8217
10380
|
exports.getHRMSClient = getHRMSClient;
|
|
8218
10381
|
exports.getHRMSService = getHRMSService;
|
|
10382
|
+
exports.getRevalidationClient = getRevalidationClient;
|
|
10383
|
+
exports.getRevalidationService = getRevalidationService;
|
|
10384
|
+
exports.getSearchClient = getSearchClient;
|
|
10385
|
+
exports.getSearchService = getSearchService;
|
|
10386
|
+
exports.getSeoClient = getSeoClient;
|
|
10387
|
+
exports.getSeoService = getSeoService;
|
|
8219
10388
|
exports.getTransferClient = getTransferClient;
|
|
8220
10389
|
exports.getTransferService = getTransferService;
|
|
8221
10390
|
exports.hashPassword = hashPassword;
|
|
@@ -8231,6 +10400,9 @@ exports.parseJsonSource = parseJsonSource;
|
|
|
8231
10400
|
exports.parseSource = parseSource;
|
|
8232
10401
|
exports.resolveLocalizedData = resolveLocalizedData;
|
|
8233
10402
|
exports.resolveUniqueSlug = resolveUniqueSlug;
|
|
10403
|
+
exports.revalidationPlugin = revalidationPlugin;
|
|
10404
|
+
exports.searchPlugin = searchPlugin;
|
|
10405
|
+
exports.seoPlugin = seoPlugin;
|
|
8234
10406
|
exports.serializeCsv = serializeCsv;
|
|
8235
10407
|
exports.serializeExcel = serializeExcel;
|
|
8236
10408
|
exports.serializeJson = serializeJson;
|