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