@azlib/cms 0.5.0 → 0.6.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/dist/index.cjs +1965 -114
- package/dist/index.d.cts +439 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +439 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1920 -115
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ExcelJS from "exceljs";
|
|
1
2
|
//#region src/content/schema.ts
|
|
2
3
|
const fields = {
|
|
3
4
|
text(options) {
|
|
@@ -2856,7 +2857,7 @@ var EcommerceService = class {
|
|
|
2856
2857
|
};
|
|
2857
2858
|
//#endregion
|
|
2858
2859
|
//#region src/plugins/ecommerce/routes.ts
|
|
2859
|
-
function json$
|
|
2860
|
+
function json$2(data, status = 200) {
|
|
2860
2861
|
return new Response(JSON.stringify(data), {
|
|
2861
2862
|
status,
|
|
2862
2863
|
headers: {
|
|
@@ -2865,11 +2866,11 @@ function json$1(data, status = 200) {
|
|
|
2865
2866
|
}
|
|
2866
2867
|
});
|
|
2867
2868
|
}
|
|
2868
|
-
function badRequest$
|
|
2869
|
-
return json$
|
|
2869
|
+
function badRequest$2(message) {
|
|
2870
|
+
return json$2({ error: message }, 400);
|
|
2870
2871
|
}
|
|
2871
|
-
function notFound$
|
|
2872
|
-
return json$
|
|
2872
|
+
function notFound$2(message) {
|
|
2873
|
+
return json$2({ error: message }, 404);
|
|
2873
2874
|
}
|
|
2874
2875
|
function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
2875
2876
|
const prefix = (options.apiPrefix ?? "/api/ecommerce").replace(/\/+$/, "");
|
|
@@ -2892,7 +2893,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
2892
2893
|
const offsetStr = url.searchParams.get("offset");
|
|
2893
2894
|
const limit = limitStr ? parseInt(limitStr, 10) : 20;
|
|
2894
2895
|
const offset = offsetStr ? parseInt(offsetStr, 10) : 0;
|
|
2895
|
-
return json$
|
|
2896
|
+
return json$2(await service.listProducts({
|
|
2896
2897
|
categorySlug,
|
|
2897
2898
|
categoryId,
|
|
2898
2899
|
tagSlug,
|
|
@@ -2908,7 +2909,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
2908
2909
|
offset
|
|
2909
2910
|
}));
|
|
2910
2911
|
} catch (err) {
|
|
2911
|
-
return badRequest$
|
|
2912
|
+
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
2912
2913
|
}
|
|
2913
2914
|
});
|
|
2914
2915
|
ctx.registerRoute("GET", `${prefix}/products/:id`, async (_req, { params, url }) => {
|
|
@@ -2920,32 +2921,32 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
2920
2921
|
product = await service.getProduct(id);
|
|
2921
2922
|
if (!product) product = await service.getProductBySlug(id);
|
|
2922
2923
|
}
|
|
2923
|
-
if (!product) return notFound$
|
|
2924
|
-
return json$
|
|
2924
|
+
if (!product) return notFound$2(`Product '${id}' not found`);
|
|
2925
|
+
return json$2(product);
|
|
2925
2926
|
});
|
|
2926
2927
|
ctx.registerRoute("POST", `${prefix}/products`, async (req) => {
|
|
2927
2928
|
try {
|
|
2928
2929
|
const body = await req.json();
|
|
2929
|
-
if (!body.title) return badRequest$
|
|
2930
|
-
if (body.price === void 0 || body.price < 0) return badRequest$
|
|
2931
|
-
return json$
|
|
2930
|
+
if (!body.title) return badRequest$2("Product 'title' is required.");
|
|
2931
|
+
if (body.price === void 0 || body.price < 0) return badRequest$2("Valid product 'price' is required.");
|
|
2932
|
+
return json$2(await service.createProduct(body), 201);
|
|
2932
2933
|
} catch (err) {
|
|
2933
|
-
return badRequest$
|
|
2934
|
+
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
2934
2935
|
}
|
|
2935
2936
|
});
|
|
2936
2937
|
ctx.registerRoute("PUT", `${prefix}/products/:id`, async (req, { params }) => {
|
|
2937
2938
|
try {
|
|
2938
2939
|
const body = await req.json();
|
|
2939
2940
|
const updated = await service.updateProduct(params.id, body);
|
|
2940
|
-
if (!updated) return notFound$
|
|
2941
|
-
return json$
|
|
2941
|
+
if (!updated) return notFound$2(`Product '${params.id}' not found.`);
|
|
2942
|
+
return json$2(updated);
|
|
2942
2943
|
} catch (err) {
|
|
2943
|
-
return badRequest$
|
|
2944
|
+
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
2944
2945
|
}
|
|
2945
2946
|
});
|
|
2946
2947
|
ctx.registerRoute("DELETE", `${prefix}/products/:id`, async (_req, { params }) => {
|
|
2947
|
-
if (!await service.deleteProduct(params.id)) return notFound$
|
|
2948
|
-
return json$
|
|
2948
|
+
if (!await service.deleteProduct(params.id)) return notFound$2(`Product '${params.id}' not found.`);
|
|
2949
|
+
return json$2({
|
|
2949
2950
|
success: true,
|
|
2950
2951
|
id: params.id
|
|
2951
2952
|
});
|
|
@@ -2953,8 +2954,8 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
2953
2954
|
ctx.registerRoute("POST", `${prefix}/products/:id/images`, async (req, { params }) => {
|
|
2954
2955
|
try {
|
|
2955
2956
|
const body = await req.json();
|
|
2956
|
-
if (!body.filename || !body.mimeType) return badRequest$
|
|
2957
|
-
return json$
|
|
2957
|
+
if (!body.filename || !body.mimeType) return badRequest$2("'filename' and 'mimeType' are required.");
|
|
2958
|
+
return json$2(await service.uploadProductImage(params.id, {
|
|
2958
2959
|
filename: body.filename,
|
|
2959
2960
|
mimeType: body.mimeType,
|
|
2960
2961
|
sizeBytes: body.sizeBytes ?? 0,
|
|
@@ -2966,68 +2967,68 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
2966
2967
|
isFeatured: body.isFeatured
|
|
2967
2968
|
}), 201);
|
|
2968
2969
|
} catch (err) {
|
|
2969
|
-
return badRequest$
|
|
2970
|
+
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
2970
2971
|
}
|
|
2971
2972
|
});
|
|
2972
2973
|
ctx.registerRoute("GET", `${prefix}/categories`, async (_req, { url }) => {
|
|
2973
2974
|
try {
|
|
2974
|
-
if (url.searchParams.get("tree") === "true") return json$
|
|
2975
|
+
if (url.searchParams.get("tree") === "true") return json$2(await service.getCategoryTree());
|
|
2975
2976
|
const parentId = url.searchParams.get("parentId");
|
|
2976
|
-
return json$
|
|
2977
|
+
return json$2(await service.getCategories({ parentId: parentId === "null" ? null : parentId ?? void 0 }));
|
|
2977
2978
|
} catch (err) {
|
|
2978
|
-
return badRequest$
|
|
2979
|
+
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
2979
2980
|
}
|
|
2980
2981
|
});
|
|
2981
2982
|
ctx.registerRoute("POST", `${prefix}/categories`, async (req) => {
|
|
2982
2983
|
try {
|
|
2983
2984
|
const body = await req.json();
|
|
2984
|
-
if (!body.name) return badRequest$
|
|
2985
|
-
return json$
|
|
2985
|
+
if (!body.name) return badRequest$2("Category 'name' is required.");
|
|
2986
|
+
return json$2(await service.createCategory(body), 201);
|
|
2986
2987
|
} catch (err) {
|
|
2987
|
-
return badRequest$
|
|
2988
|
+
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
2988
2989
|
}
|
|
2989
2990
|
});
|
|
2990
2991
|
if (options.enableDiscounts !== false) {
|
|
2991
2992
|
ctx.registerRoute("POST", `${prefix}/discounts`, async (req) => {
|
|
2992
2993
|
try {
|
|
2993
2994
|
const body = await req.json();
|
|
2994
|
-
if (!body.title || !body.code) return badRequest$
|
|
2995
|
-
if (body.value === void 0 || body.value < 0) return badRequest$
|
|
2996
|
-
return json$
|
|
2995
|
+
if (!body.title || !body.code) return badRequest$2("'title' and 'code' are required.");
|
|
2996
|
+
if (body.value === void 0 || body.value < 0) return badRequest$2("Valid discount 'value' is required.");
|
|
2997
|
+
return json$2(await service.createDiscount(body), 201);
|
|
2997
2998
|
} catch (err) {
|
|
2998
|
-
return badRequest$
|
|
2999
|
+
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
2999
3000
|
}
|
|
3000
3001
|
});
|
|
3001
3002
|
ctx.registerRoute("POST", `${prefix}/discounts/validate`, async (req) => {
|
|
3002
3003
|
try {
|
|
3003
3004
|
const body = await req.json();
|
|
3004
|
-
if (!body.code) return badRequest$
|
|
3005
|
+
if (!body.code) return badRequest$2("Discount 'code' is required.");
|
|
3005
3006
|
const subtotal = Number(body.subtotal ?? 0);
|
|
3006
3007
|
const productIds = Array.isArray(body.productIds) ? body.productIds : [];
|
|
3007
|
-
return json$
|
|
3008
|
+
return json$2(await service.validateDiscount(body.code, subtotal, productIds));
|
|
3008
3009
|
} catch (err) {
|
|
3009
|
-
return badRequest$
|
|
3010
|
+
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
3010
3011
|
}
|
|
3011
3012
|
});
|
|
3012
3013
|
}
|
|
3013
3014
|
ctx.registerRoute("POST", `${prefix}/cart/calculate`, async (req) => {
|
|
3014
3015
|
try {
|
|
3015
3016
|
const body = await req.json();
|
|
3016
|
-
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$
|
|
3017
|
-
return json$
|
|
3017
|
+
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$2("'items' array is required and must not be empty.");
|
|
3018
|
+
return json$2(await service.calculateCart(body));
|
|
3018
3019
|
} catch (err) {
|
|
3019
|
-
return badRequest$
|
|
3020
|
+
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
3020
3021
|
}
|
|
3021
3022
|
});
|
|
3022
3023
|
if (options.enableOrders !== false) {
|
|
3023
3024
|
ctx.registerRoute("POST", `${prefix}/orders`, async (req) => {
|
|
3024
3025
|
try {
|
|
3025
3026
|
const body = await req.json();
|
|
3026
|
-
if (!body.customerEmail) return badRequest$
|
|
3027
|
-
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$
|
|
3028
|
-
return json$
|
|
3027
|
+
if (!body.customerEmail) return badRequest$2("'customerEmail' is required.");
|
|
3028
|
+
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$2("'items' array is required.");
|
|
3029
|
+
return json$2(await service.createOrder(body), 201);
|
|
3029
3030
|
} catch (err) {
|
|
3030
|
-
return badRequest$
|
|
3031
|
+
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
3031
3032
|
}
|
|
3032
3033
|
});
|
|
3033
3034
|
ctx.registerRoute("GET", `${prefix}/orders/:id`, async (_req, { params, url }) => {
|
|
@@ -3039,18 +3040,18 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
3039
3040
|
order = await service.getOrder(id);
|
|
3040
3041
|
if (!order) order = await service.getOrderByNumber(id);
|
|
3041
3042
|
}
|
|
3042
|
-
if (!order) return notFound$
|
|
3043
|
-
return json$
|
|
3043
|
+
if (!order) return notFound$2(`Order '${id}' not found.`);
|
|
3044
|
+
return json$2(order);
|
|
3044
3045
|
});
|
|
3045
3046
|
ctx.registerRoute("PATCH", `${prefix}/orders/:id/status`, async (req, { params }) => {
|
|
3046
3047
|
try {
|
|
3047
3048
|
const body = await req.json();
|
|
3048
|
-
if (!body.status) return badRequest$
|
|
3049
|
+
if (!body.status) return badRequest$2("New 'status' is required.");
|
|
3049
3050
|
const updated = await service.updateOrderStatus(params.id, body.status, body.note);
|
|
3050
|
-
if (!updated) return notFound$
|
|
3051
|
-
return json$
|
|
3051
|
+
if (!updated) return notFound$2(`Order '${params.id}' not found.`);
|
|
3052
|
+
return json$2(updated);
|
|
3052
3053
|
} catch (err) {
|
|
3053
|
-
return badRequest$
|
|
3054
|
+
return badRequest$2(err instanceof Error ? err.message : String(err));
|
|
3054
3055
|
}
|
|
3055
3056
|
});
|
|
3056
3057
|
}
|
|
@@ -4284,7 +4285,7 @@ var HRMSService = class {
|
|
|
4284
4285
|
};
|
|
4285
4286
|
//#endregion
|
|
4286
4287
|
//#region src/plugins/hrms/routes.ts
|
|
4287
|
-
function json(data, status = 200) {
|
|
4288
|
+
function json$1(data, status = 200) {
|
|
4288
4289
|
return new Response(JSON.stringify(data), {
|
|
4289
4290
|
status,
|
|
4290
4291
|
headers: {
|
|
@@ -4293,11 +4294,11 @@ function json(data, status = 200) {
|
|
|
4293
4294
|
}
|
|
4294
4295
|
});
|
|
4295
4296
|
}
|
|
4296
|
-
function badRequest(message) {
|
|
4297
|
-
return json({ error: message }, 400);
|
|
4297
|
+
function badRequest$1(message) {
|
|
4298
|
+
return json$1({ error: message }, 400);
|
|
4298
4299
|
}
|
|
4299
|
-
function notFound(message) {
|
|
4300
|
-
return json({ error: message }, 404);
|
|
4300
|
+
function notFound$1(message) {
|
|
4301
|
+
return json$1({ error: message }, 404);
|
|
4301
4302
|
}
|
|
4302
4303
|
function registerHRMSRoutes(ctx, service, options = {}) {
|
|
4303
4304
|
const prefix = (options.apiPrefix ?? "/api/hrms").replace(/\/+$/, "");
|
|
@@ -4308,41 +4309,41 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
4308
4309
|
const limitStr = url.searchParams.get("limit");
|
|
4309
4310
|
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4310
4311
|
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4311
|
-
return json(await service.listEmployers({
|
|
4312
|
+
return json$1(await service.listEmployers({
|
|
4312
4313
|
status,
|
|
4313
4314
|
page,
|
|
4314
4315
|
limit
|
|
4315
4316
|
}));
|
|
4316
4317
|
} catch (err) {
|
|
4317
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4318
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4318
4319
|
}
|
|
4319
4320
|
});
|
|
4320
4321
|
ctx.registerRoute("POST", `${prefix}/employers`, async (req) => {
|
|
4321
4322
|
try {
|
|
4322
4323
|
const body = await req.json();
|
|
4323
|
-
if (!body.companyName) return badRequest("companyName is required.");
|
|
4324
|
-
return json(await service.createEmployer(body), 201);
|
|
4324
|
+
if (!body.companyName) return badRequest$1("companyName is required.");
|
|
4325
|
+
return json$1(await service.createEmployer(body), 201);
|
|
4325
4326
|
} catch (err) {
|
|
4326
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4327
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4327
4328
|
}
|
|
4328
4329
|
});
|
|
4329
4330
|
ctx.registerRoute("GET", `${prefix}/employers/:id`, async (_req, { params }) => {
|
|
4330
4331
|
try {
|
|
4331
4332
|
const employer = await service.getEmployer(params.id);
|
|
4332
|
-
if (!employer) return notFound(`Employer '${params.id}' not found.`);
|
|
4333
|
-
return json(employer);
|
|
4333
|
+
if (!employer) return notFound$1(`Employer '${params.id}' not found.`);
|
|
4334
|
+
return json$1(employer);
|
|
4334
4335
|
} catch (err) {
|
|
4335
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4336
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4336
4337
|
}
|
|
4337
4338
|
});
|
|
4338
4339
|
ctx.registerRoute("PUT", `${prefix}/employers/:id`, async (req, { params }) => {
|
|
4339
4340
|
try {
|
|
4340
4341
|
const body = await req.json();
|
|
4341
4342
|
const updated = await service.updateEmployer(params.id, body);
|
|
4342
|
-
if (!updated) return notFound(`Employer '${params.id}' not found.`);
|
|
4343
|
-
return json(updated);
|
|
4343
|
+
if (!updated) return notFound$1(`Employer '${params.id}' not found.`);
|
|
4344
|
+
return json$1(updated);
|
|
4344
4345
|
} catch (err) {
|
|
4345
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4346
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4346
4347
|
}
|
|
4347
4348
|
});
|
|
4348
4349
|
ctx.registerRoute("GET", `${prefix}/employees`, async (_req, { url }) => {
|
|
@@ -4356,7 +4357,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
4356
4357
|
const limitStr = url.searchParams.get("limit");
|
|
4357
4358
|
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4358
4359
|
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4359
|
-
return json(await service.listEmployees({
|
|
4360
|
+
return json$1(await service.listEmployees({
|
|
4360
4361
|
employerId,
|
|
4361
4362
|
department,
|
|
4362
4363
|
employmentType,
|
|
@@ -4366,84 +4367,84 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
4366
4367
|
limit
|
|
4367
4368
|
}));
|
|
4368
4369
|
} catch (err) {
|
|
4369
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4370
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4370
4371
|
}
|
|
4371
4372
|
});
|
|
4372
4373
|
ctx.registerRoute("POST", `${prefix}/employees`, async (req) => {
|
|
4373
4374
|
try {
|
|
4374
4375
|
const body = await req.json();
|
|
4375
|
-
if (!body.employerId) return badRequest("employerId is required.");
|
|
4376
|
-
if (!body.employeeNumber) return badRequest("employeeNumber is required.");
|
|
4377
|
-
if (!body.firstName || !body.lastName) return badRequest("firstName and lastName are required.");
|
|
4378
|
-
if (!body.email) return badRequest("email is required.");
|
|
4379
|
-
return json(await service.createEmployee(body), 201);
|
|
4376
|
+
if (!body.employerId) return badRequest$1("employerId is required.");
|
|
4377
|
+
if (!body.employeeNumber) return badRequest$1("employeeNumber is required.");
|
|
4378
|
+
if (!body.firstName || !body.lastName) return badRequest$1("firstName and lastName are required.");
|
|
4379
|
+
if (!body.email) return badRequest$1("email is required.");
|
|
4380
|
+
return json$1(await service.createEmployee(body), 201);
|
|
4380
4381
|
} catch (err) {
|
|
4381
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4382
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4382
4383
|
}
|
|
4383
4384
|
});
|
|
4384
4385
|
ctx.registerRoute("GET", `${prefix}/employees/:id`, async (_req, { params }) => {
|
|
4385
4386
|
try {
|
|
4386
4387
|
const employee = await service.getEmployee(params.id);
|
|
4387
|
-
if (!employee) return notFound(`Employee '${params.id}' not found.`);
|
|
4388
|
-
return json(employee);
|
|
4388
|
+
if (!employee) return notFound$1(`Employee '${params.id}' not found.`);
|
|
4389
|
+
return json$1(employee);
|
|
4389
4390
|
} catch (err) {
|
|
4390
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4391
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4391
4392
|
}
|
|
4392
4393
|
});
|
|
4393
4394
|
ctx.registerRoute("PUT", `${prefix}/employees/:id`, async (req, { params }) => {
|
|
4394
4395
|
try {
|
|
4395
4396
|
const body = await req.json();
|
|
4396
4397
|
const updated = await service.updateEmployee(params.id, body);
|
|
4397
|
-
if (!updated) return notFound(`Employee '${params.id}' not found.`);
|
|
4398
|
-
return json(updated);
|
|
4398
|
+
if (!updated) return notFound$1(`Employee '${params.id}' not found.`);
|
|
4399
|
+
return json$1(updated);
|
|
4399
4400
|
} catch (err) {
|
|
4400
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4401
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4401
4402
|
}
|
|
4402
4403
|
});
|
|
4403
4404
|
ctx.registerRoute("DELETE", `${prefix}/employees/:id`, async (_req, { params }) => {
|
|
4404
4405
|
try {
|
|
4405
|
-
if (!await service.deleteEmployee(params.id)) return notFound(`Employee '${params.id}' not found.`);
|
|
4406
|
-
return json({ success: true });
|
|
4406
|
+
if (!await service.deleteEmployee(params.id)) return notFound$1(`Employee '${params.id}' not found.`);
|
|
4407
|
+
return json$1({ success: true });
|
|
4407
4408
|
} catch (err) {
|
|
4408
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4409
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4409
4410
|
}
|
|
4410
4411
|
});
|
|
4411
4412
|
ctx.registerRoute("GET", `${prefix}/employees/:id/leave-balance`, async (_req, { params, url }) => {
|
|
4412
4413
|
try {
|
|
4413
4414
|
const yearStr = url.searchParams.get("year");
|
|
4414
4415
|
const year = yearStr ? parseInt(yearStr, 10) : void 0;
|
|
4415
|
-
return json(await service.calculateLeaveBalance(params.id, year));
|
|
4416
|
+
return json$1(await service.calculateLeaveBalance(params.id, year));
|
|
4416
4417
|
} catch (err) {
|
|
4417
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4418
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4418
4419
|
}
|
|
4419
4420
|
});
|
|
4420
4421
|
ctx.registerRoute("GET", `${prefix}/employees/:id/direct-reports`, async (_req, { params }) => {
|
|
4421
4422
|
try {
|
|
4422
4423
|
const reports = await service.getDirectReports(params.id);
|
|
4423
|
-
return json({
|
|
4424
|
+
return json$1({
|
|
4424
4425
|
items: reports,
|
|
4425
4426
|
total: reports.length
|
|
4426
4427
|
});
|
|
4427
4428
|
} catch (err) {
|
|
4428
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4429
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4429
4430
|
}
|
|
4430
4431
|
});
|
|
4431
4432
|
ctx.registerRoute("POST", `${prefix}/attendance/check-in`, async (req) => {
|
|
4432
4433
|
try {
|
|
4433
4434
|
const body = await req.json();
|
|
4434
|
-
if (!body.employeeId) return badRequest("employeeId is required.");
|
|
4435
|
-
return json(await service.checkIn(body), 201);
|
|
4435
|
+
if (!body.employeeId) return badRequest$1("employeeId is required.");
|
|
4436
|
+
return json$1(await service.checkIn(body), 201);
|
|
4436
4437
|
} catch (err) {
|
|
4437
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4438
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4438
4439
|
}
|
|
4439
4440
|
});
|
|
4440
4441
|
ctx.registerRoute("POST", `${prefix}/attendance/check-out`, async (req) => {
|
|
4441
4442
|
try {
|
|
4442
4443
|
const body = await req.json();
|
|
4443
|
-
if (!body.employeeId) return badRequest("employeeId is required.");
|
|
4444
|
-
return json(await service.checkOut(body));
|
|
4444
|
+
if (!body.employeeId) return badRequest$1("employeeId is required.");
|
|
4445
|
+
return json$1(await service.checkOut(body));
|
|
4445
4446
|
} catch (err) {
|
|
4446
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4447
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4447
4448
|
}
|
|
4448
4449
|
});
|
|
4449
4450
|
ctx.registerRoute("GET", `${prefix}/attendance`, async (_req, { url }) => {
|
|
@@ -4458,7 +4459,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
4458
4459
|
const limitStr = url.searchParams.get("limit");
|
|
4459
4460
|
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4460
4461
|
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4461
|
-
return json(await service.listAttendance({
|
|
4462
|
+
return json$1(await service.listAttendance({
|
|
4462
4463
|
employerId,
|
|
4463
4464
|
employeeId,
|
|
4464
4465
|
date,
|
|
@@ -4469,46 +4470,46 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
4469
4470
|
limit
|
|
4470
4471
|
}));
|
|
4471
4472
|
} catch (err) {
|
|
4472
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4473
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4473
4474
|
}
|
|
4474
4475
|
});
|
|
4475
4476
|
ctx.registerRoute("POST", `${prefix}/attendance/manual`, async (req) => {
|
|
4476
4477
|
try {
|
|
4477
4478
|
const body = await req.json();
|
|
4478
|
-
if (!body.employerId || !body.employeeId || !body.date || !body.checkInAt) return badRequest("employerId, employeeId, date, and checkInAt are required.");
|
|
4479
|
-
return json(await service.recordAttendanceManual(body), 201);
|
|
4479
|
+
if (!body.employerId || !body.employeeId || !body.date || !body.checkInAt) return badRequest$1("employerId, employeeId, date, and checkInAt are required.");
|
|
4480
|
+
return json$1(await service.recordAttendanceManual(body), 201);
|
|
4480
4481
|
} catch (err) {
|
|
4481
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4482
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4482
4483
|
}
|
|
4483
4484
|
});
|
|
4484
4485
|
ctx.registerRoute("GET", `${prefix}/leave-types`, async (_req, { url }) => {
|
|
4485
4486
|
try {
|
|
4486
4487
|
const employerId = url.searchParams.get("employerId") ?? void 0;
|
|
4487
4488
|
const types = await service.listLeaveTypes(employerId);
|
|
4488
|
-
return json({
|
|
4489
|
+
return json$1({
|
|
4489
4490
|
items: types,
|
|
4490
4491
|
total: types.length
|
|
4491
4492
|
});
|
|
4492
4493
|
} catch (err) {
|
|
4493
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4494
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4494
4495
|
}
|
|
4495
4496
|
});
|
|
4496
4497
|
ctx.registerRoute("POST", `${prefix}/leave-types`, async (req) => {
|
|
4497
4498
|
try {
|
|
4498
4499
|
const body = await req.json();
|
|
4499
|
-
if (!body.name || !body.code || body.daysAllowedPerYear === void 0) return badRequest("name, code, and daysAllowedPerYear are required.");
|
|
4500
|
-
return json(await service.createLeaveType(body), 201);
|
|
4500
|
+
if (!body.name || !body.code || body.daysAllowedPerYear === void 0) return badRequest$1("name, code, and daysAllowedPerYear are required.");
|
|
4501
|
+
return json$1(await service.createLeaveType(body), 201);
|
|
4501
4502
|
} catch (err) {
|
|
4502
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4503
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4503
4504
|
}
|
|
4504
4505
|
});
|
|
4505
4506
|
ctx.registerRoute("GET", `${prefix}/leave-types/:id`, async (_req, { params }) => {
|
|
4506
4507
|
try {
|
|
4507
4508
|
const leaveType = await service.getLeaveType(params.id);
|
|
4508
|
-
if (!leaveType) return notFound(`Leave type '${params.id}' not found.`);
|
|
4509
|
-
return json(leaveType);
|
|
4509
|
+
if (!leaveType) return notFound$1(`Leave type '${params.id}' not found.`);
|
|
4510
|
+
return json$1(leaveType);
|
|
4510
4511
|
} catch (err) {
|
|
4511
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4512
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4512
4513
|
}
|
|
4513
4514
|
});
|
|
4514
4515
|
ctx.registerRoute("GET", `${prefix}/leave-requests`, async (_req, { url }) => {
|
|
@@ -4523,7 +4524,7 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
4523
4524
|
const limitStr = url.searchParams.get("limit");
|
|
4524
4525
|
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4525
4526
|
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4526
|
-
return json(await service.listLeaveRequests({
|
|
4527
|
+
return json$1(await service.listLeaveRequests({
|
|
4527
4528
|
employerId,
|
|
4528
4529
|
employeeId,
|
|
4529
4530
|
leaveTypeId,
|
|
@@ -4533,47 +4534,47 @@ function registerHRMSRoutes(ctx, service, options = {}) {
|
|
|
4533
4534
|
limit
|
|
4534
4535
|
}));
|
|
4535
4536
|
} catch (err) {
|
|
4536
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4537
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4537
4538
|
}
|
|
4538
4539
|
});
|
|
4539
4540
|
ctx.registerRoute("POST", `${prefix}/leave-requests`, async (req) => {
|
|
4540
4541
|
try {
|
|
4541
4542
|
const body = await req.json();
|
|
4542
|
-
if (!body.employeeId || !body.leaveTypeId || !body.startDate || !body.endDate) return badRequest("employeeId, leaveTypeId, startDate, and endDate are required.");
|
|
4543
|
-
return json(await service.requestLeave(body), 201);
|
|
4543
|
+
if (!body.employeeId || !body.leaveTypeId || !body.startDate || !body.endDate) return badRequest$1("employeeId, leaveTypeId, startDate, and endDate are required.");
|
|
4544
|
+
return json$1(await service.requestLeave(body), 201);
|
|
4544
4545
|
} catch (err) {
|
|
4545
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4546
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4546
4547
|
}
|
|
4547
4548
|
});
|
|
4548
4549
|
ctx.registerRoute("POST", `${prefix}/leave-requests/:id/approve`, async (req, { params }) => {
|
|
4549
4550
|
try {
|
|
4550
4551
|
const approverId = (await req.json().catch(() => ({}))).approverId ?? "admin";
|
|
4551
|
-
return json(await service.approveLeave({
|
|
4552
|
+
return json$1(await service.approveLeave({
|
|
4552
4553
|
requestId: params.id,
|
|
4553
4554
|
approverId
|
|
4554
4555
|
}));
|
|
4555
4556
|
} catch (err) {
|
|
4556
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4557
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4557
4558
|
}
|
|
4558
4559
|
});
|
|
4559
4560
|
ctx.registerRoute("POST", `${prefix}/leave-requests/:id/reject`, async (req, { params }) => {
|
|
4560
4561
|
try {
|
|
4561
4562
|
const body = await req.json().catch(() => ({}));
|
|
4562
4563
|
const approverId = body.approverId ?? "admin";
|
|
4563
|
-
return json(await service.rejectLeave({
|
|
4564
|
+
return json$1(await service.rejectLeave({
|
|
4564
4565
|
requestId: params.id,
|
|
4565
4566
|
approverId,
|
|
4566
4567
|
reason: body.reason
|
|
4567
4568
|
}));
|
|
4568
4569
|
} catch (err) {
|
|
4569
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4570
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4570
4571
|
}
|
|
4571
4572
|
});
|
|
4572
4573
|
ctx.registerRoute("POST", `${prefix}/leave-requests/:id/cancel`, async (_req, { params }) => {
|
|
4573
4574
|
try {
|
|
4574
|
-
return json(await service.cancelLeave(params.id));
|
|
4575
|
+
return json$1(await service.cancelLeave(params.id));
|
|
4575
4576
|
} catch (err) {
|
|
4576
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4577
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
4577
4578
|
}
|
|
4578
4579
|
});
|
|
4579
4580
|
}
|
|
@@ -4825,6 +4826,1810 @@ function getHRMSService(engine, options) {
|
|
|
4825
4826
|
return service;
|
|
4826
4827
|
}
|
|
4827
4828
|
//#endregion
|
|
4828
|
-
|
|
4829
|
+
//#region src/plugins/transfer/parsers/json.ts
|
|
4830
|
+
/**
|
|
4831
|
+
* @azlib/cms - Universal JSON Data Parser
|
|
4832
|
+
*/
|
|
4833
|
+
function parseJsonSource(input) {
|
|
4834
|
+
if (Array.isArray(input)) return input.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item)));
|
|
4835
|
+
let text;
|
|
4836
|
+
if (typeof input === "string") text = input.trim();
|
|
4837
|
+
else if (input instanceof Uint8Array || Buffer.isBuffer(input)) text = new TextDecoder("utf-8").decode(input).trim();
|
|
4838
|
+
else if (input && typeof input === "object") {
|
|
4839
|
+
const obj = input;
|
|
4840
|
+
if (Array.isArray(obj.data)) return parseJsonSource(obj.data);
|
|
4841
|
+
if (Array.isArray(obj.items)) return parseJsonSource(obj.items);
|
|
4842
|
+
return [obj];
|
|
4843
|
+
} else throw new Error("[TransferParser] Invalid JSON input: expected string, buffer, or array.");
|
|
4844
|
+
if (text.charCodeAt(0) === 65279) text = text.slice(1);
|
|
4845
|
+
if (!text) return [];
|
|
4846
|
+
try {
|
|
4847
|
+
const parsed = JSON.parse(text);
|
|
4848
|
+
if (Array.isArray(parsed)) return parsed.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item)));
|
|
4849
|
+
if (parsed && typeof parsed === "object") {
|
|
4850
|
+
if (Array.isArray(parsed.data)) return parseJsonSource(parsed.data);
|
|
4851
|
+
if (Array.isArray(parsed.items)) return parseJsonSource(parsed.items);
|
|
4852
|
+
return [parsed];
|
|
4853
|
+
}
|
|
4854
|
+
return [];
|
|
4855
|
+
} catch {
|
|
4856
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
4857
|
+
const records = [];
|
|
4858
|
+
for (const line of lines) try {
|
|
4859
|
+
const item = JSON.parse(line);
|
|
4860
|
+
if (item && typeof item === "object" && !Array.isArray(item)) records.push(item);
|
|
4861
|
+
} catch (err) {
|
|
4862
|
+
throw new Error(`[TransferParser] Failed to parse JSON source: ${err instanceof Error ? err.message : String(err)}`);
|
|
4863
|
+
}
|
|
4864
|
+
return records;
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
//#endregion
|
|
4868
|
+
//#region src/plugins/transfer/parsers/csv.ts
|
|
4869
|
+
/**
|
|
4870
|
+
* @azlib/cms - Universal RFC 4180 CSV Data Parser
|
|
4871
|
+
*/
|
|
4872
|
+
function parseCsvSource(input) {
|
|
4873
|
+
let text;
|
|
4874
|
+
if (typeof input === "string") text = input;
|
|
4875
|
+
else if (input instanceof Uint8Array || Buffer.isBuffer(input)) text = new TextDecoder("utf-8").decode(input);
|
|
4876
|
+
else throw new Error("[TransferParser] Invalid CSV input: expected string or buffer.");
|
|
4877
|
+
if (text.charCodeAt(0) === 65279) text = text.slice(1);
|
|
4878
|
+
const rows = parseCsvRows(text);
|
|
4879
|
+
if (rows.length === 0) return [];
|
|
4880
|
+
const headers = rows[0].map((h, i) => {
|
|
4881
|
+
return h.trim() || `column_${i + 1}`;
|
|
4882
|
+
});
|
|
4883
|
+
const records = [];
|
|
4884
|
+
for (let r = 1; r < rows.length; r++) {
|
|
4885
|
+
const row = rows[r];
|
|
4886
|
+
if (row.length === 0 || row.length === 1 && row[0].trim() === "") continue;
|
|
4887
|
+
const record = {};
|
|
4888
|
+
for (let c = 0; c < headers.length; c++) {
|
|
4889
|
+
const header = headers[c];
|
|
4890
|
+
record[header] = coerceCsvValue(c < row.length ? row[c].trim() : "");
|
|
4891
|
+
}
|
|
4892
|
+
records.push(record);
|
|
4893
|
+
}
|
|
4894
|
+
return records;
|
|
4895
|
+
}
|
|
4896
|
+
/**
|
|
4897
|
+
* Tokenize CSV into 2D array of string cells following RFC 4180.
|
|
4898
|
+
*/
|
|
4899
|
+
function parseCsvRows(text) {
|
|
4900
|
+
const rows = [];
|
|
4901
|
+
let currentRow = [];
|
|
4902
|
+
let currentCell = "";
|
|
4903
|
+
let insideQuotes = false;
|
|
4904
|
+
let i = 0;
|
|
4905
|
+
const len = text.length;
|
|
4906
|
+
while (i < len) {
|
|
4907
|
+
const char = text[i];
|
|
4908
|
+
if (insideQuotes) if (char === "\"") if (i + 1 < len && text[i + 1] === "\"") {
|
|
4909
|
+
currentCell += "\"";
|
|
4910
|
+
i += 2;
|
|
4911
|
+
continue;
|
|
4912
|
+
} else {
|
|
4913
|
+
insideQuotes = false;
|
|
4914
|
+
i++;
|
|
4915
|
+
continue;
|
|
4916
|
+
}
|
|
4917
|
+
else {
|
|
4918
|
+
currentCell += char;
|
|
4919
|
+
i++;
|
|
4920
|
+
continue;
|
|
4921
|
+
}
|
|
4922
|
+
if (char === "\"") {
|
|
4923
|
+
insideQuotes = true;
|
|
4924
|
+
i++;
|
|
4925
|
+
continue;
|
|
4926
|
+
}
|
|
4927
|
+
if (char === ",") {
|
|
4928
|
+
currentRow.push(currentCell);
|
|
4929
|
+
currentCell = "";
|
|
4930
|
+
i++;
|
|
4931
|
+
continue;
|
|
4932
|
+
}
|
|
4933
|
+
if (char === "\r") {
|
|
4934
|
+
if (i + 1 < len && text[i + 1] === "\n") i++;
|
|
4935
|
+
currentRow.push(currentCell);
|
|
4936
|
+
rows.push(currentRow);
|
|
4937
|
+
currentRow = [];
|
|
4938
|
+
currentCell = "";
|
|
4939
|
+
i++;
|
|
4940
|
+
continue;
|
|
4941
|
+
}
|
|
4942
|
+
if (char === "\n") {
|
|
4943
|
+
currentRow.push(currentCell);
|
|
4944
|
+
rows.push(currentRow);
|
|
4945
|
+
currentRow = [];
|
|
4946
|
+
currentCell = "";
|
|
4947
|
+
i++;
|
|
4948
|
+
continue;
|
|
4949
|
+
}
|
|
4950
|
+
currentCell += char;
|
|
4951
|
+
i++;
|
|
4952
|
+
}
|
|
4953
|
+
if (currentCell.length > 0 || currentRow.length > 0) {
|
|
4954
|
+
currentRow.push(currentCell);
|
|
4955
|
+
rows.push(currentRow);
|
|
4956
|
+
}
|
|
4957
|
+
return rows;
|
|
4958
|
+
}
|
|
4959
|
+
/**
|
|
4960
|
+
* Coerce obvious numeric / boolean CSV strings to primitives when lossless.
|
|
4961
|
+
*/
|
|
4962
|
+
function coerceCsvValue(val) {
|
|
4963
|
+
if (val === "") return "";
|
|
4964
|
+
if (val.toLowerCase() === "true") return true;
|
|
4965
|
+
if (val.toLowerCase() === "false") return false;
|
|
4966
|
+
if (val.toLowerCase() === "null") return null;
|
|
4967
|
+
if (/^-?\d+(\.\d+)?$/.test(val)) {
|
|
4968
|
+
if (val.length > 1 && val.startsWith("0") && !val.startsWith("0.")) return val;
|
|
4969
|
+
const num = Number(val);
|
|
4970
|
+
if (!Number.isNaN(num)) return num;
|
|
4971
|
+
}
|
|
4972
|
+
return val;
|
|
4973
|
+
}
|
|
4974
|
+
//#endregion
|
|
4975
|
+
//#region src/plugins/transfer/parsers/excel.ts
|
|
4976
|
+
/**
|
|
4977
|
+
* @azlib/cms - Universal Excel (.xlsx) Data Parser using ExcelJS
|
|
4978
|
+
*/
|
|
4979
|
+
async function parseExcelSource(input, options = {}) {
|
|
4980
|
+
const workbook = new ExcelJS.Workbook();
|
|
4981
|
+
let buffer;
|
|
4982
|
+
if (Buffer.isBuffer(input)) buffer = input;
|
|
4983
|
+
else if (input instanceof Uint8Array) buffer = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
4984
|
+
else if (input instanceof ArrayBuffer) buffer = Buffer.from(input);
|
|
4985
|
+
else throw new Error("[TransferParser] Invalid Excel input: expected Buffer, Uint8Array, or ArrayBuffer.");
|
|
4986
|
+
await workbook.xlsx.load(buffer);
|
|
4987
|
+
const sheetNames = workbook.worksheets.map((s) => s.name);
|
|
4988
|
+
if (sheetNames.length === 0) return {
|
|
4989
|
+
sheets: [],
|
|
4990
|
+
selectedSheet: "",
|
|
4991
|
+
records: []
|
|
4992
|
+
};
|
|
4993
|
+
let worksheet = options.sheetName ? workbook.getWorksheet(options.sheetName) : void 0;
|
|
4994
|
+
if (!worksheet) worksheet = workbook.worksheets[0];
|
|
4995
|
+
const selectedSheet = worksheet.name;
|
|
4996
|
+
const records = [];
|
|
4997
|
+
const headerRow = worksheet.getRow(1);
|
|
4998
|
+
const headers = [];
|
|
4999
|
+
headerRow.eachCell({ includeEmpty: false }, (cell, colNumber) => {
|
|
5000
|
+
let headerText = String(cell.value ?? "").trim();
|
|
5001
|
+
if (!headerText) headerText = `column_${colNumber}`;
|
|
5002
|
+
headers[colNumber] = headerText;
|
|
5003
|
+
});
|
|
5004
|
+
if (headers.length === 0) return {
|
|
5005
|
+
sheets: sheetNames,
|
|
5006
|
+
selectedSheet,
|
|
5007
|
+
records: []
|
|
5008
|
+
};
|
|
5009
|
+
const rowCount = worksheet.rowCount;
|
|
5010
|
+
for (let r = 2; r <= rowCount; r++) {
|
|
5011
|
+
const row = worksheet.getRow(r);
|
|
5012
|
+
let hasValues = false;
|
|
5013
|
+
const record = {};
|
|
5014
|
+
for (let c = 1; c < headers.length; c++) {
|
|
5015
|
+
const header = headers[c];
|
|
5016
|
+
if (!header) continue;
|
|
5017
|
+
const val = extractCellValue(row.getCell(c).value);
|
|
5018
|
+
if (val !== void 0 && val !== null && val !== "") hasValues = true;
|
|
5019
|
+
record[header] = val;
|
|
5020
|
+
}
|
|
5021
|
+
if (hasValues) records.push(record);
|
|
5022
|
+
}
|
|
5023
|
+
return {
|
|
5024
|
+
sheets: sheetNames,
|
|
5025
|
+
selectedSheet,
|
|
5026
|
+
records
|
|
5027
|
+
};
|
|
5028
|
+
}
|
|
5029
|
+
/**
|
|
5030
|
+
* Normalizes ExcelJS cell values (handles formulas, dates, rich text, hyperlinks).
|
|
5031
|
+
*/
|
|
5032
|
+
function extractCellValue(val) {
|
|
5033
|
+
if (val === void 0 || val === null) return null;
|
|
5034
|
+
if (val instanceof Date) return val.toISOString();
|
|
5035
|
+
if (typeof val === "object") {
|
|
5036
|
+
if ("formula" in val) return extractCellValue(val.result);
|
|
5037
|
+
if ("text" in val && "hyperlink" in val) return val.text;
|
|
5038
|
+
if ("richText" in val && Array.isArray(val.richText)) return val.richText.map((t) => t.text).join("");
|
|
5039
|
+
if ("error" in val) return null;
|
|
5040
|
+
}
|
|
5041
|
+
return val;
|
|
5042
|
+
}
|
|
5043
|
+
//#endregion
|
|
5044
|
+
//#region src/plugins/transfer/parsers/index.ts
|
|
5045
|
+
/**
|
|
5046
|
+
* Detect the format of an input payload if not explicitly provided.
|
|
5047
|
+
*/
|
|
5048
|
+
function detectFormat(input, fileName) {
|
|
5049
|
+
if (Array.isArray(input)) return "json";
|
|
5050
|
+
if (fileName) {
|
|
5051
|
+
const lower = fileName.toLowerCase();
|
|
5052
|
+
if (lower.endsWith(".xlsx") || lower.endsWith(".xls")) return "excel";
|
|
5053
|
+
if (lower.endsWith(".csv")) return "csv";
|
|
5054
|
+
if (lower.endsWith(".json") || lower.endsWith(".ndjson")) return "json";
|
|
5055
|
+
}
|
|
5056
|
+
if (input instanceof Uint8Array || Buffer.isBuffer(input)) {
|
|
5057
|
+
if (input.length >= 4 && input[0] === 80 && input[1] === 75 && input[2] === 3 && input[3] === 4) return "excel";
|
|
5058
|
+
try {
|
|
5059
|
+
const head = new TextDecoder("utf-8").decode(input.slice(0, 100)).trim();
|
|
5060
|
+
if (head.startsWith("{") || head.startsWith("[")) return "json";
|
|
5061
|
+
if (head.includes(",") && head.includes("\n")) return "csv";
|
|
5062
|
+
} catch {}
|
|
5063
|
+
}
|
|
5064
|
+
if (typeof input === "string") {
|
|
5065
|
+
const trimmed = input.trim();
|
|
5066
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
|
|
5067
|
+
if (trimmed.includes(",") || trimmed.includes("\n")) return "csv";
|
|
5068
|
+
}
|
|
5069
|
+
return "json";
|
|
5070
|
+
}
|
|
5071
|
+
/**
|
|
5072
|
+
* Universal source parser that handles JSON, CSV, and Excel (.xlsx).
|
|
5073
|
+
*/
|
|
5074
|
+
async function parseSource(input, options = {}) {
|
|
5075
|
+
switch (options.format || detectFormat(input, options.fileName)) {
|
|
5076
|
+
case "excel": {
|
|
5077
|
+
const excelResult = await parseExcelSource(input, { sheetName: options.sheetName });
|
|
5078
|
+
return {
|
|
5079
|
+
format: "excel",
|
|
5080
|
+
sheets: excelResult.sheets,
|
|
5081
|
+
selectedSheet: excelResult.selectedSheet,
|
|
5082
|
+
records: excelResult.records
|
|
5083
|
+
};
|
|
5084
|
+
}
|
|
5085
|
+
case "csv": return {
|
|
5086
|
+
format: "csv",
|
|
5087
|
+
records: parseCsvSource(input)
|
|
5088
|
+
};
|
|
5089
|
+
default: return {
|
|
5090
|
+
format: "json",
|
|
5091
|
+
records: parseJsonSource(input)
|
|
5092
|
+
};
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
//#endregion
|
|
5096
|
+
//#region src/plugins/transfer/serializers/json.ts
|
|
5097
|
+
function serializeJson(records, options = {}) {
|
|
5098
|
+
const indent = options.pretty !== false ? 2 : void 0;
|
|
5099
|
+
return JSON.stringify(records, null, indent);
|
|
5100
|
+
}
|
|
5101
|
+
//#endregion
|
|
5102
|
+
//#region src/plugins/transfer/serializers/csv.ts
|
|
5103
|
+
function serializeCsv(records, options = {}) {
|
|
5104
|
+
if (records.length === 0) return "";
|
|
5105
|
+
let columns = options.columns;
|
|
5106
|
+
if (!columns || columns.length === 0) {
|
|
5107
|
+
const keys = /* @__PURE__ */ new Set();
|
|
5108
|
+
for (const record of records) for (const key of Object.keys(record)) keys.add(key);
|
|
5109
|
+
columns = Array.from(keys).map((k) => ({
|
|
5110
|
+
key: k,
|
|
5111
|
+
header: k
|
|
5112
|
+
}));
|
|
5113
|
+
}
|
|
5114
|
+
const lines = [];
|
|
5115
|
+
lines.push(columns.map((c) => escapeCsvCell(c.header)).join(","));
|
|
5116
|
+
for (const record of records) {
|
|
5117
|
+
const rowCells = columns.map((col) => {
|
|
5118
|
+
const val = record[col.key];
|
|
5119
|
+
return escapeCsvCell(formatCsvValue(val));
|
|
5120
|
+
});
|
|
5121
|
+
lines.push(rowCells.join(","));
|
|
5122
|
+
}
|
|
5123
|
+
return lines.join("\r\n");
|
|
5124
|
+
}
|
|
5125
|
+
function formatCsvValue(val) {
|
|
5126
|
+
if (val === void 0 || val === null) return "";
|
|
5127
|
+
if (typeof val === "object") {
|
|
5128
|
+
if (val instanceof Date) return val.toISOString();
|
|
5129
|
+
return JSON.stringify(val);
|
|
5130
|
+
}
|
|
5131
|
+
return String(val);
|
|
5132
|
+
}
|
|
5133
|
+
function escapeCsvCell(cell) {
|
|
5134
|
+
if (cell.includes(",") || cell.includes("\"") || cell.includes("\n") || cell.includes("\r")) return `"${cell.replace(/"/g, "\"\"")}"`;
|
|
5135
|
+
return cell;
|
|
5136
|
+
}
|
|
5137
|
+
//#endregion
|
|
5138
|
+
//#region src/plugins/transfer/serializers/excel.ts
|
|
5139
|
+
/**
|
|
5140
|
+
* @azlib/cms - Universal Excel (.xlsx) Serializer using ExcelJS
|
|
5141
|
+
*/
|
|
5142
|
+
async function serializeExcel(records, options = {}) {
|
|
5143
|
+
const workbook = new ExcelJS.Workbook();
|
|
5144
|
+
const sheetName = (options.sheetName || "Export").replace(/[:\\/?*\[\]]/g, "_");
|
|
5145
|
+
const worksheet = workbook.addWorksheet(sheetName.slice(0, 31));
|
|
5146
|
+
let columns = options.columns;
|
|
5147
|
+
if (!columns || columns.length === 0) {
|
|
5148
|
+
const keys = /* @__PURE__ */ new Set();
|
|
5149
|
+
for (const record of records) for (const key of Object.keys(record)) keys.add(key);
|
|
5150
|
+
columns = Array.from(keys).map((k) => ({
|
|
5151
|
+
key: k,
|
|
5152
|
+
header: formatHeaderLabel(k)
|
|
5153
|
+
}));
|
|
5154
|
+
}
|
|
5155
|
+
worksheet.columns = columns.map((col) => {
|
|
5156
|
+
let maxLen = col.header.length;
|
|
5157
|
+
for (let i = 0; i < Math.min(records.length, 100); i++) {
|
|
5158
|
+
const val = records[i][col.key];
|
|
5159
|
+
if (val !== void 0 && val !== null) {
|
|
5160
|
+
const str = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
5161
|
+
if (str.length > maxLen) maxLen = Math.min(str.length, 50);
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
return {
|
|
5165
|
+
header: col.header,
|
|
5166
|
+
key: col.key,
|
|
5167
|
+
width: Math.max(maxLen + 4, col.width ?? 12)
|
|
5168
|
+
};
|
|
5169
|
+
});
|
|
5170
|
+
const headerRow = worksheet.getRow(1);
|
|
5171
|
+
headerRow.height = 28;
|
|
5172
|
+
headerRow.eachCell((cell) => {
|
|
5173
|
+
cell.font = {
|
|
5174
|
+
bold: true,
|
|
5175
|
+
color: { argb: "FF1E293B" },
|
|
5176
|
+
size: 11
|
|
5177
|
+
};
|
|
5178
|
+
cell.fill = {
|
|
5179
|
+
type: "pattern",
|
|
5180
|
+
pattern: "solid",
|
|
5181
|
+
fgColor: { argb: "FFF1F5F9" }
|
|
5182
|
+
};
|
|
5183
|
+
cell.alignment = {
|
|
5184
|
+
vertical: "middle",
|
|
5185
|
+
horizontal: "center",
|
|
5186
|
+
wrapText: true
|
|
5187
|
+
};
|
|
5188
|
+
cell.border = {
|
|
5189
|
+
top: {
|
|
5190
|
+
style: "thin",
|
|
5191
|
+
color: { argb: "FFE2E8F0" }
|
|
5192
|
+
},
|
|
5193
|
+
bottom: {
|
|
5194
|
+
style: "medium",
|
|
5195
|
+
color: { argb: "FFCBD5E1" }
|
|
5196
|
+
},
|
|
5197
|
+
left: {
|
|
5198
|
+
style: "thin",
|
|
5199
|
+
color: { argb: "FFE2E8F0" }
|
|
5200
|
+
},
|
|
5201
|
+
right: {
|
|
5202
|
+
style: "thin",
|
|
5203
|
+
color: { argb: "FFE2E8F0" }
|
|
5204
|
+
}
|
|
5205
|
+
};
|
|
5206
|
+
});
|
|
5207
|
+
for (const record of records) {
|
|
5208
|
+
const rowValues = {};
|
|
5209
|
+
for (const col of columns) {
|
|
5210
|
+
const val = record[col.key];
|
|
5211
|
+
rowValues[col.key] = formatExcelCellValue(val);
|
|
5212
|
+
}
|
|
5213
|
+
const row = worksheet.addRow(rowValues);
|
|
5214
|
+
row.height = 20;
|
|
5215
|
+
row.eachCell((cell) => {
|
|
5216
|
+
cell.alignment = { vertical: "middle" };
|
|
5217
|
+
cell.border = {
|
|
5218
|
+
top: {
|
|
5219
|
+
style: "thin",
|
|
5220
|
+
color: { argb: "FFF1F5F9" }
|
|
5221
|
+
},
|
|
5222
|
+
bottom: {
|
|
5223
|
+
style: "thin",
|
|
5224
|
+
color: { argb: "FFF1F5F9" }
|
|
5225
|
+
},
|
|
5226
|
+
left: {
|
|
5227
|
+
style: "thin",
|
|
5228
|
+
color: { argb: "FFF1F5F9" }
|
|
5229
|
+
},
|
|
5230
|
+
right: {
|
|
5231
|
+
style: "thin",
|
|
5232
|
+
color: { argb: "FFF1F5F9" }
|
|
5233
|
+
}
|
|
5234
|
+
};
|
|
5235
|
+
});
|
|
5236
|
+
}
|
|
5237
|
+
const arrayBuffer = await workbook.xlsx.writeBuffer();
|
|
5238
|
+
return new Uint8Array(arrayBuffer);
|
|
5239
|
+
}
|
|
5240
|
+
function formatHeaderLabel(key) {
|
|
5241
|
+
return key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
5242
|
+
}
|
|
5243
|
+
function formatExcelCellValue(val) {
|
|
5244
|
+
if (val === void 0 || val === null) return "";
|
|
5245
|
+
if (typeof val === "number" || typeof val === "boolean") return val;
|
|
5246
|
+
if (val instanceof Date) return val;
|
|
5247
|
+
if (typeof val === "object") return JSON.stringify(val);
|
|
5248
|
+
return String(val);
|
|
5249
|
+
}
|
|
5250
|
+
//#endregion
|
|
5251
|
+
//#region src/plugins/transfer/serializers/index.ts
|
|
5252
|
+
async function serializeSource(records, options = {}) {
|
|
5253
|
+
const format = options.format || "json";
|
|
5254
|
+
const baseName = options.fileName || "export";
|
|
5255
|
+
switch (format) {
|
|
5256
|
+
case "excel": return {
|
|
5257
|
+
format: "excel",
|
|
5258
|
+
data: await serializeExcel(records, {
|
|
5259
|
+
sheetName: options.sheetName,
|
|
5260
|
+
columns: options.columns
|
|
5261
|
+
}),
|
|
5262
|
+
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
5263
|
+
fileName: baseName.endsWith(".xlsx") ? baseName : `${baseName}.xlsx`,
|
|
5264
|
+
totalRecords: records.length
|
|
5265
|
+
};
|
|
5266
|
+
case "csv": return {
|
|
5267
|
+
format: "csv",
|
|
5268
|
+
data: serializeCsv(records, { columns: options.columns }),
|
|
5269
|
+
mimeType: "text/csv; charset=utf-8",
|
|
5270
|
+
fileName: baseName.endsWith(".csv") ? baseName : `${baseName}.csv`,
|
|
5271
|
+
totalRecords: records.length
|
|
5272
|
+
};
|
|
5273
|
+
default: return {
|
|
5274
|
+
format: "json",
|
|
5275
|
+
data: serializeJson(records, { pretty: true }),
|
|
5276
|
+
mimeType: "application/json; charset=utf-8",
|
|
5277
|
+
fileName: baseName.endsWith(".json") ? baseName : `${baseName}.json`,
|
|
5278
|
+
totalRecords: records.length
|
|
5279
|
+
};
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
//#endregion
|
|
5283
|
+
//#region src/plugins/transfer/inspect.ts
|
|
5284
|
+
/**
|
|
5285
|
+
* Inspect an uploaded source file or payload to discover its schema,
|
|
5286
|
+
* infer column types, and generate auto-mapping suggestions against a target collection.
|
|
5287
|
+
*/
|
|
5288
|
+
async function inspectSource(input, options = {}) {
|
|
5289
|
+
const parsed = await parseSource(input, {
|
|
5290
|
+
format: options.format,
|
|
5291
|
+
fileName: options.fileName,
|
|
5292
|
+
sheetName: options.sheetName
|
|
5293
|
+
});
|
|
5294
|
+
const records = parsed.records;
|
|
5295
|
+
const totalRows = records.length;
|
|
5296
|
+
const previewLimit = options.previewLimit ?? 5;
|
|
5297
|
+
const previewRows = records.slice(0, previewLimit);
|
|
5298
|
+
const fieldKeys = /* @__PURE__ */ new Set();
|
|
5299
|
+
for (const row of records) for (const key of Object.keys(row)) fieldKeys.add(key);
|
|
5300
|
+
const sourceFields = [];
|
|
5301
|
+
for (const key of fieldKeys) {
|
|
5302
|
+
let nullCount = 0;
|
|
5303
|
+
const samples = [];
|
|
5304
|
+
const detectedTypes = [];
|
|
5305
|
+
for (const row of records) {
|
|
5306
|
+
const val = row[key];
|
|
5307
|
+
if (val === void 0 || val === null || val === "") nullCount++;
|
|
5308
|
+
else {
|
|
5309
|
+
if (samples.length < 5) samples.push(val);
|
|
5310
|
+
detectedTypes.push(inferValueType(val));
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
sourceFields.push({
|
|
5314
|
+
name: key,
|
|
5315
|
+
inferredType: resolveDominantType(detectedTypes),
|
|
5316
|
+
sampleValues: samples,
|
|
5317
|
+
nullCount,
|
|
5318
|
+
totalCount: totalRows
|
|
5319
|
+
});
|
|
5320
|
+
}
|
|
5321
|
+
let targetSchema;
|
|
5322
|
+
let suggestedMapping;
|
|
5323
|
+
if (options.collectionConfig) {
|
|
5324
|
+
targetSchema = summarizeCollection(options.collectionConfig);
|
|
5325
|
+
suggestedMapping = generateSuggestedMapping(sourceFields.map((f) => f.name), targetSchema);
|
|
5326
|
+
}
|
|
5327
|
+
return {
|
|
5328
|
+
format: parsed.format,
|
|
5329
|
+
sheets: parsed.sheets,
|
|
5330
|
+
selectedSheet: parsed.selectedSheet,
|
|
5331
|
+
totalRows,
|
|
5332
|
+
sourceFields,
|
|
5333
|
+
targetSchema,
|
|
5334
|
+
suggestedMapping,
|
|
5335
|
+
previewRows
|
|
5336
|
+
};
|
|
5337
|
+
}
|
|
5338
|
+
/**
|
|
5339
|
+
* Summarize a CMS CollectionConfig into consumer-friendly schema details.
|
|
5340
|
+
*/
|
|
5341
|
+
function summarizeCollection(config) {
|
|
5342
|
+
return {
|
|
5343
|
+
slug: config.slug,
|
|
5344
|
+
label: config.label,
|
|
5345
|
+
singularLabel: config.singularLabel || config.label,
|
|
5346
|
+
description: config.description,
|
|
5347
|
+
taxonomies: config.taxonomies || [],
|
|
5348
|
+
fields: config.fields.map((f) => ({
|
|
5349
|
+
name: f.name,
|
|
5350
|
+
label: f.label || f.name,
|
|
5351
|
+
type: f.type,
|
|
5352
|
+
required: Boolean(f.required),
|
|
5353
|
+
unique: Boolean(f.unique),
|
|
5354
|
+
description: f.description,
|
|
5355
|
+
defaultValue: f.defaultValue,
|
|
5356
|
+
targetCollection: f.targetCollection,
|
|
5357
|
+
options: f.options
|
|
5358
|
+
}))
|
|
5359
|
+
};
|
|
5360
|
+
}
|
|
5361
|
+
/**
|
|
5362
|
+
* Generates intelligent auto-mapping suggestions matching source headers to CMS fields.
|
|
5363
|
+
*/
|
|
5364
|
+
function generateSuggestedMapping(sourceHeaders, targetSchema) {
|
|
5365
|
+
const suggestions = [];
|
|
5366
|
+
const matchedTargets = /* @__PURE__ */ new Set();
|
|
5367
|
+
for (const sourceHeader of sourceHeaders) {
|
|
5368
|
+
let bestMatch = null;
|
|
5369
|
+
for (const targetField of targetSchema.fields) {
|
|
5370
|
+
if (matchedTargets.has(targetField.name)) continue;
|
|
5371
|
+
const score = calculateMatchConfidence(sourceHeader, targetField);
|
|
5372
|
+
if (score >= .6 && (!bestMatch || score > bestMatch.confidence)) bestMatch = {
|
|
5373
|
+
field: targetField.name,
|
|
5374
|
+
confidence: score
|
|
5375
|
+
};
|
|
5376
|
+
}
|
|
5377
|
+
if (bestMatch) {
|
|
5378
|
+
matchedTargets.add(bestMatch.field);
|
|
5379
|
+
suggestions.push({
|
|
5380
|
+
sourceField: sourceHeader,
|
|
5381
|
+
targetField: bestMatch.field,
|
|
5382
|
+
confidence: Number(bestMatch.confidence.toFixed(2))
|
|
5383
|
+
});
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
return suggestions;
|
|
5387
|
+
}
|
|
5388
|
+
function normalize(str) {
|
|
5389
|
+
return str.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
5390
|
+
}
|
|
5391
|
+
/**
|
|
5392
|
+
* Common field synonyms dictionary for intuitive fuzzy matching.
|
|
5393
|
+
*/
|
|
5394
|
+
const SYNONYMS = {
|
|
5395
|
+
employeeNumber: [
|
|
5396
|
+
"empid",
|
|
5397
|
+
"employeeid",
|
|
5398
|
+
"empnumber",
|
|
5399
|
+
"staffid",
|
|
5400
|
+
"badgenumber",
|
|
5401
|
+
"staffno",
|
|
5402
|
+
"empno"
|
|
5403
|
+
],
|
|
5404
|
+
companyName: [
|
|
5405
|
+
"company",
|
|
5406
|
+
"organization",
|
|
5407
|
+
"employer",
|
|
5408
|
+
"businessname",
|
|
5409
|
+
"employername",
|
|
5410
|
+
"firm"
|
|
5411
|
+
],
|
|
5412
|
+
firstName: [
|
|
5413
|
+
"fname",
|
|
5414
|
+
"givenname",
|
|
5415
|
+
"first"
|
|
5416
|
+
],
|
|
5417
|
+
lastName: [
|
|
5418
|
+
"lname",
|
|
5419
|
+
"surname",
|
|
5420
|
+
"familyname",
|
|
5421
|
+
"last"
|
|
5422
|
+
],
|
|
5423
|
+
email: [
|
|
5424
|
+
"mail",
|
|
5425
|
+
"workemail",
|
|
5426
|
+
"primaryemail",
|
|
5427
|
+
"emailaddress"
|
|
5428
|
+
],
|
|
5429
|
+
phone: [
|
|
5430
|
+
"mobile",
|
|
5431
|
+
"cell",
|
|
5432
|
+
"telephone",
|
|
5433
|
+
"phonenumber",
|
|
5434
|
+
"contactnumber",
|
|
5435
|
+
"tel"
|
|
5436
|
+
],
|
|
5437
|
+
employerId: [
|
|
5438
|
+
"employer",
|
|
5439
|
+
"companyid",
|
|
5440
|
+
"company",
|
|
5441
|
+
"organizationid",
|
|
5442
|
+
"orgid"
|
|
5443
|
+
],
|
|
5444
|
+
hireDate: [
|
|
5445
|
+
"startdate",
|
|
5446
|
+
"joiningdate",
|
|
5447
|
+
"hired",
|
|
5448
|
+
"dateofjoining"
|
|
5449
|
+
],
|
|
5450
|
+
jobTitle: [
|
|
5451
|
+
"title",
|
|
5452
|
+
"position",
|
|
5453
|
+
"role",
|
|
5454
|
+
"designation"
|
|
5455
|
+
],
|
|
5456
|
+
employmentType: [
|
|
5457
|
+
"contracttype",
|
|
5458
|
+
"worktype",
|
|
5459
|
+
"jobtype"
|
|
5460
|
+
],
|
|
5461
|
+
salary: [
|
|
5462
|
+
"compensation",
|
|
5463
|
+
"wage",
|
|
5464
|
+
"pay",
|
|
5465
|
+
"rate",
|
|
5466
|
+
"annualsalary"
|
|
5467
|
+
],
|
|
5468
|
+
taxId: [
|
|
5469
|
+
"ein",
|
|
5470
|
+
"vat",
|
|
5471
|
+
"taxnumber",
|
|
5472
|
+
"tin"
|
|
5473
|
+
],
|
|
5474
|
+
status: ["state", "active"]
|
|
5475
|
+
};
|
|
5476
|
+
function calculateMatchConfidence(sourceHeader, targetField) {
|
|
5477
|
+
const normSource = normalize(sourceHeader);
|
|
5478
|
+
const normTargetName = normalize(targetField.name);
|
|
5479
|
+
const normTargetLabel = normalize(targetField.label);
|
|
5480
|
+
if (sourceHeader === targetField.name) return 1;
|
|
5481
|
+
if (normSource === normTargetName) return .95;
|
|
5482
|
+
if (sourceHeader.toLowerCase() === targetField.label.toLowerCase()) return .92;
|
|
5483
|
+
if (normSource === normTargetLabel) return .9;
|
|
5484
|
+
const synonyms = SYNONYMS[targetField.name];
|
|
5485
|
+
if (synonyms && synonyms.includes(normSource)) return .85;
|
|
5486
|
+
if (normSource.includes(normTargetName) || normTargetName.includes(normSource)) return .7;
|
|
5487
|
+
if (normSource.includes(normTargetLabel) || normTargetLabel.includes(normSource)) return .65;
|
|
5488
|
+
return 0;
|
|
5489
|
+
}
|
|
5490
|
+
function inferValueType(val) {
|
|
5491
|
+
if (typeof val === "boolean") return "boolean";
|
|
5492
|
+
if (typeof val === "number") return "number";
|
|
5493
|
+
if (Array.isArray(val)) return "array";
|
|
5494
|
+
if (val && typeof val === "object") return "object";
|
|
5495
|
+
if (typeof val === "string") {
|
|
5496
|
+
const s = val.trim();
|
|
5497
|
+
if (s.toLowerCase() === "true" || s.toLowerCase() === "false") return "boolean";
|
|
5498
|
+
if (/^-?\d+(\.\d+)?$/.test(s) && !/^0\d+/.test(s)) return "number";
|
|
5499
|
+
if (/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(s)) {
|
|
5500
|
+
const parsed = Date.parse(s);
|
|
5501
|
+
if (!Number.isNaN(parsed)) return "date";
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
return "string";
|
|
5505
|
+
}
|
|
5506
|
+
function resolveDominantType(types) {
|
|
5507
|
+
if (types.length === 0) return "string";
|
|
5508
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5509
|
+
for (const t of types) counts.set(t, (counts.get(t) || 0) + 1);
|
|
5510
|
+
let dominant = "string";
|
|
5511
|
+
let maxCount = 0;
|
|
5512
|
+
for (const [t, count] of counts) if (count > maxCount) {
|
|
5513
|
+
maxCount = count;
|
|
5514
|
+
dominant = t;
|
|
5515
|
+
}
|
|
5516
|
+
return dominant;
|
|
5517
|
+
}
|
|
5518
|
+
//#endregion
|
|
5519
|
+
//#region src/plugins/transfer/mapping.ts
|
|
5520
|
+
/**
|
|
5521
|
+
* Apply a mapping definition to transform an external raw record into a CMS content payload.
|
|
5522
|
+
*/
|
|
5523
|
+
function mapSourceRecord(rawRecord, mapping) {
|
|
5524
|
+
const mappedData = {};
|
|
5525
|
+
const mappedTargetFields = /* @__PURE__ */ new Set();
|
|
5526
|
+
for (const rule of mapping.fields) {
|
|
5527
|
+
mappedTargetFields.add(rule.targetField);
|
|
5528
|
+
const sourceVal = rawRecord[rule.sourceField];
|
|
5529
|
+
const transformed = applyFieldTransform(rule, sourceVal, rawRecord);
|
|
5530
|
+
if (transformed !== void 0) mappedData[rule.targetField] = transformed;
|
|
5531
|
+
}
|
|
5532
|
+
if (mapping.options?.ignoreUnmappedFields === false) {
|
|
5533
|
+
for (const [key, val] of Object.entries(rawRecord)) if (!mapping.fields.some((r) => r.sourceField === key)) mappedData[key] = val;
|
|
5534
|
+
}
|
|
5535
|
+
let title = typeof mappedData.title === "string" ? mappedData.title : void 0;
|
|
5536
|
+
if (!title) {
|
|
5537
|
+
if (typeof mappedData.companyName === "string") title = mappedData.companyName;
|
|
5538
|
+
else if (typeof mappedData.name === "string") title = mappedData.name;
|
|
5539
|
+
else if (typeof mappedData.firstName === "string" || typeof mappedData.lastName === "string") {
|
|
5540
|
+
const parts = [mappedData.firstName, mappedData.lastName].filter(Boolean);
|
|
5541
|
+
if (parts.length > 0) title = parts.join(" ");
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
let slug = typeof mappedData.slug === "string" ? mappedData.slug : void 0;
|
|
5545
|
+
if (!slug && mapping.options?.autoGenerateSlug !== false && title) slug = slugify(title);
|
|
5546
|
+
let status = mapping.options?.defaultStatus;
|
|
5547
|
+
if (mapping.options?.statusField && mappedData[mapping.options.statusField]) status = String(mappedData[mapping.options.statusField]);
|
|
5548
|
+
return {
|
|
5549
|
+
data: mappedData,
|
|
5550
|
+
title,
|
|
5551
|
+
slug,
|
|
5552
|
+
status
|
|
5553
|
+
};
|
|
5554
|
+
}
|
|
5555
|
+
/**
|
|
5556
|
+
* Applies transform preset or custom function to a field value.
|
|
5557
|
+
*/
|
|
5558
|
+
function applyFieldTransform(rule, value, rawRecord) {
|
|
5559
|
+
let val = value;
|
|
5560
|
+
if (typeof rule.transform === "function") try {
|
|
5561
|
+
val = rule.transform(val, rawRecord);
|
|
5562
|
+
} catch (err) {
|
|
5563
|
+
throw new Error(`Transform error on field '${rule.sourceField}': ${err instanceof Error ? err.message : String(err)}`);
|
|
5564
|
+
}
|
|
5565
|
+
else if (rule.transform && typeof rule.transform === "string") val = applyPresetTransform(rule.transform, val);
|
|
5566
|
+
if (val === void 0 || val === null || val === "") {
|
|
5567
|
+
if (rule.defaultValue !== void 0) val = rule.defaultValue;
|
|
5568
|
+
}
|
|
5569
|
+
return val;
|
|
5570
|
+
}
|
|
5571
|
+
function applyPresetTransform(preset, val) {
|
|
5572
|
+
if (val === void 0 || val === null) return val;
|
|
5573
|
+
switch (preset) {
|
|
5574
|
+
case "trim": return typeof val === "string" ? val.trim() : val;
|
|
5575
|
+
case "lowercase": return typeof val === "string" ? val.toLowerCase() : val;
|
|
5576
|
+
case "uppercase": return typeof val === "string" ? val.toUpperCase() : val;
|
|
5577
|
+
case "number": {
|
|
5578
|
+
if (typeof val === "number") return val;
|
|
5579
|
+
const parsed = Number(val);
|
|
5580
|
+
return Number.isNaN(parsed) ? val : parsed;
|
|
5581
|
+
}
|
|
5582
|
+
case "boolean": {
|
|
5583
|
+
if (typeof val === "boolean") return val;
|
|
5584
|
+
const s = String(val).toLowerCase().trim();
|
|
5585
|
+
if (s === "true" || s === "1" || s === "yes") return true;
|
|
5586
|
+
if (s === "false" || s === "0" || s === "no") return false;
|
|
5587
|
+
return Boolean(val);
|
|
5588
|
+
}
|
|
5589
|
+
case "date": {
|
|
5590
|
+
if (val instanceof Date) return val.toISOString();
|
|
5591
|
+
const s = String(val).trim();
|
|
5592
|
+
const parsed = Date.parse(s);
|
|
5593
|
+
return Number.isNaN(parsed) ? s : new Date(parsed).toISOString();
|
|
5594
|
+
}
|
|
5595
|
+
case "json":
|
|
5596
|
+
if (typeof val === "object") return val;
|
|
5597
|
+
try {
|
|
5598
|
+
return JSON.parse(String(val));
|
|
5599
|
+
} catch {
|
|
5600
|
+
return val;
|
|
5601
|
+
}
|
|
5602
|
+
case "slug": return slugify(String(val));
|
|
5603
|
+
case "split_comma":
|
|
5604
|
+
if (Array.isArray(val)) return val;
|
|
5605
|
+
return String(val).split(",").map((s) => s.trim()).filter(Boolean);
|
|
5606
|
+
default: return val;
|
|
5607
|
+
}
|
|
5608
|
+
}
|
|
5609
|
+
/**
|
|
5610
|
+
* Reverse mapping for export: transforms a CMS ContentItem into an external export record.
|
|
5611
|
+
*/
|
|
5612
|
+
function mapCmsItemForExport(item, mapping, options = {}) {
|
|
5613
|
+
const exportRecord = {};
|
|
5614
|
+
if (mapping && mapping.fields.length > 0) {
|
|
5615
|
+
for (const rule of mapping.fields) {
|
|
5616
|
+
const val = item.data[rule.targetField] !== void 0 ? item.data[rule.targetField] : item[rule.targetField];
|
|
5617
|
+
exportRecord[rule.sourceField] = val !== void 0 ? val : rule.defaultValue ?? "";
|
|
5618
|
+
}
|
|
5619
|
+
return exportRecord;
|
|
5620
|
+
}
|
|
5621
|
+
if (options.includeId !== false) exportRecord.id = item.id;
|
|
5622
|
+
if (item.title) exportRecord.title = item.title;
|
|
5623
|
+
if (item.slug) exportRecord.slug = item.slug;
|
|
5624
|
+
if (item.status) exportRecord.status = item.status;
|
|
5625
|
+
for (const [k, v] of Object.entries(item.data)) exportRecord[k] = v;
|
|
5626
|
+
if (options.includeTimestamps) {
|
|
5627
|
+
exportRecord.createdAt = item.createdAt;
|
|
5628
|
+
exportRecord.updatedAt = item.updatedAt;
|
|
5629
|
+
}
|
|
5630
|
+
return exportRecord;
|
|
5631
|
+
}
|
|
5632
|
+
//#endregion
|
|
5633
|
+
//#region src/plugins/transfer/validator.ts
|
|
5634
|
+
/**
|
|
5635
|
+
* Validates a batch of source records against a target CMS collection configuration
|
|
5636
|
+
* and live database constraints.
|
|
5637
|
+
*/
|
|
5638
|
+
async function validateTransferBatch(rawRecords, mapping, collectionConfig, options = {}) {
|
|
5639
|
+
const errors = [];
|
|
5640
|
+
const previewRows = [];
|
|
5641
|
+
const previewLimit = options.previewLimit ?? 10;
|
|
5642
|
+
const onDuplicate = options.onDuplicate ?? mapping.options?.onDuplicate ?? "error";
|
|
5643
|
+
const uniqueFields = collectionConfig.fields.filter((f) => Boolean(f.unique) || f.name === "slug");
|
|
5644
|
+
const relationshipFields = collectionConfig.fields.filter((f) => f.type === "relationship" && Boolean(f.targetCollection));
|
|
5645
|
+
const seenUniqueValues = /* @__PURE__ */ new Map();
|
|
5646
|
+
for (const uf of uniqueFields) seenUniqueValues.set(uf.name, /* @__PURE__ */ new Map());
|
|
5647
|
+
let validCount = 0;
|
|
5648
|
+
for (let i = 0; i < rawRecords.length; i++) {
|
|
5649
|
+
const rowNumber = i + 1;
|
|
5650
|
+
const raw = rawRecords[i];
|
|
5651
|
+
const rowErrors = [];
|
|
5652
|
+
let mappedResult;
|
|
5653
|
+
try {
|
|
5654
|
+
mappedResult = mapSourceRecord(raw, mapping);
|
|
5655
|
+
} catch (err) {
|
|
5656
|
+
const errDetail = {
|
|
5657
|
+
rowNumber,
|
|
5658
|
+
code: "TRANSFORM_ERROR",
|
|
5659
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
5660
|
+
};
|
|
5661
|
+
rowErrors.push(errDetail);
|
|
5662
|
+
errors.push(errDetail);
|
|
5663
|
+
if (previewRows.length < previewLimit) previewRows.push({
|
|
5664
|
+
rowNumber,
|
|
5665
|
+
raw,
|
|
5666
|
+
mapped: {},
|
|
5667
|
+
valid: false,
|
|
5668
|
+
errors: rowErrors
|
|
5669
|
+
});
|
|
5670
|
+
continue;
|
|
5671
|
+
}
|
|
5672
|
+
const data = mappedResult.data;
|
|
5673
|
+
for (const field of collectionConfig.fields) validateField(field, data, rowNumber, rowErrors);
|
|
5674
|
+
for (const rule of mapping.fields) if (rule.required && !collectionConfig.fields.some((f) => f.name === rule.targetField)) {
|
|
5675
|
+
const val = data[rule.targetField];
|
|
5676
|
+
if (val === void 0 || val === null || val === "") rowErrors.push({
|
|
5677
|
+
rowNumber,
|
|
5678
|
+
field: rule.targetField,
|
|
5679
|
+
value: val,
|
|
5680
|
+
code: "REQUIRED_FIELD_MISSING",
|
|
5681
|
+
reason: `Field '${rule.targetField}' is required by mapping definition.`
|
|
5682
|
+
});
|
|
5683
|
+
}
|
|
5684
|
+
for (const uf of uniqueFields) {
|
|
5685
|
+
const val = data[uf.name] ?? (uf.name === "slug" ? mappedResult.slug : void 0);
|
|
5686
|
+
if (val !== void 0 && val !== null && val !== "") {
|
|
5687
|
+
const seenMap = seenUniqueValues.get(uf.name);
|
|
5688
|
+
if (seenMap.has(val)) {
|
|
5689
|
+
const prevRow = seenMap.get(val);
|
|
5690
|
+
rowErrors.push({
|
|
5691
|
+
rowNumber,
|
|
5692
|
+
field: uf.name,
|
|
5693
|
+
value: val,
|
|
5694
|
+
code: "CONSTRAINT_UNIQUE_VIOLATION",
|
|
5695
|
+
reason: `${uf.label || uf.name} value '${val}' is duplicated in row ${prevRow} and row ${rowNumber}.`
|
|
5696
|
+
});
|
|
5697
|
+
} else seenMap.set(val, rowNumber);
|
|
5698
|
+
if (options.storage && onDuplicate === "error") {
|
|
5699
|
+
if (await checkExistingUnique(options.storage, collectionConfig.slug, uf.name, val)) rowErrors.push({
|
|
5700
|
+
rowNumber,
|
|
5701
|
+
field: uf.name,
|
|
5702
|
+
value: val,
|
|
5703
|
+
code: "CONSTRAINT_UNIQUE_VIOLATION",
|
|
5704
|
+
reason: `${uf.label || uf.name} value '${val}' already exists in database.`
|
|
5705
|
+
});
|
|
5706
|
+
}
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
if (options.storage) for (const rf of relationshipFields) {
|
|
5710
|
+
const targetColl = rf.targetCollection;
|
|
5711
|
+
const refVal = data[rf.name];
|
|
5712
|
+
if (refVal !== void 0 && refVal !== null && refVal !== "") {
|
|
5713
|
+
if (!await checkRelationshipExists(options.storage, targetColl, refVal)) rowErrors.push({
|
|
5714
|
+
rowNumber,
|
|
5715
|
+
field: rf.name,
|
|
5716
|
+
value: refVal,
|
|
5717
|
+
code: "CONSTRAINT_FOREIGN_KEY_VIOLATION",
|
|
5718
|
+
reason: `Referenced ${rf.label || rf.name} with identifier '${refVal}' does not exist in collection '${targetColl}'.`
|
|
5719
|
+
});
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
if (rowErrors.length === 0) validCount++;
|
|
5723
|
+
else errors.push(...rowErrors);
|
|
5724
|
+
if (previewRows.length < previewLimit) previewRows.push({
|
|
5725
|
+
rowNumber,
|
|
5726
|
+
raw,
|
|
5727
|
+
mapped: data,
|
|
5728
|
+
valid: rowErrors.length === 0,
|
|
5729
|
+
errors: rowErrors.length > 0 ? rowErrors : void 0
|
|
5730
|
+
});
|
|
5731
|
+
}
|
|
5732
|
+
return {
|
|
5733
|
+
valid: errors.length === 0,
|
|
5734
|
+
totalRows: rawRecords.length,
|
|
5735
|
+
validCount,
|
|
5736
|
+
errorCount: errors.length,
|
|
5737
|
+
errors,
|
|
5738
|
+
previewRows
|
|
5739
|
+
};
|
|
5740
|
+
}
|
|
5741
|
+
/**
|
|
5742
|
+
* Validates a single field against its FieldDefinition.
|
|
5743
|
+
*/
|
|
5744
|
+
function validateField(field, data, rowNumber, errors) {
|
|
5745
|
+
const val = data[field.name];
|
|
5746
|
+
const label = field.label || field.name;
|
|
5747
|
+
if (field.required && (val === void 0 || val === null || val === "")) {
|
|
5748
|
+
errors.push({
|
|
5749
|
+
rowNumber,
|
|
5750
|
+
field: field.name,
|
|
5751
|
+
value: val,
|
|
5752
|
+
code: "REQUIRED_FIELD_MISSING",
|
|
5753
|
+
reason: `${label} is required.`
|
|
5754
|
+
});
|
|
5755
|
+
return;
|
|
5756
|
+
}
|
|
5757
|
+
if (val === void 0 || val === null || val === "") return;
|
|
5758
|
+
switch (field.type) {
|
|
5759
|
+
case "number": {
|
|
5760
|
+
const num = typeof val === "number" ? val : Number(val);
|
|
5761
|
+
if (Number.isNaN(num)) errors.push({
|
|
5762
|
+
rowNumber,
|
|
5763
|
+
field: field.name,
|
|
5764
|
+
value: val,
|
|
5765
|
+
code: "INVALID_DATA_TYPE",
|
|
5766
|
+
reason: `${label} must be a valid number, received '${val}'.`
|
|
5767
|
+
});
|
|
5768
|
+
else {
|
|
5769
|
+
data[field.name] = num;
|
|
5770
|
+
const min = field.min;
|
|
5771
|
+
const max = field.max;
|
|
5772
|
+
if (min !== void 0 && num < min) errors.push({
|
|
5773
|
+
rowNumber,
|
|
5774
|
+
field: field.name,
|
|
5775
|
+
value: val,
|
|
5776
|
+
code: "VALIDATION_RULE_FAILED",
|
|
5777
|
+
reason: `${label} must be greater than or equal to ${min}.`
|
|
5778
|
+
});
|
|
5779
|
+
if (max !== void 0 && num > max) errors.push({
|
|
5780
|
+
rowNumber,
|
|
5781
|
+
field: field.name,
|
|
5782
|
+
value: val,
|
|
5783
|
+
code: "VALIDATION_RULE_FAILED",
|
|
5784
|
+
reason: `${label} must be less than or equal to ${max}.`
|
|
5785
|
+
});
|
|
5786
|
+
}
|
|
5787
|
+
break;
|
|
5788
|
+
}
|
|
5789
|
+
case "boolean":
|
|
5790
|
+
if (typeof val !== "boolean") {
|
|
5791
|
+
const s = String(val).toLowerCase().trim();
|
|
5792
|
+
if (s === "true" || s === "1" || s === "yes") data[field.name] = true;
|
|
5793
|
+
else if (s === "false" || s === "0" || s === "no") data[field.name] = false;
|
|
5794
|
+
else errors.push({
|
|
5795
|
+
rowNumber,
|
|
5796
|
+
field: field.name,
|
|
5797
|
+
value: val,
|
|
5798
|
+
code: "INVALID_DATA_TYPE",
|
|
5799
|
+
reason: `${label} must be a boolean (true/false), received '${val}'.`
|
|
5800
|
+
});
|
|
5801
|
+
}
|
|
5802
|
+
break;
|
|
5803
|
+
case "date": {
|
|
5804
|
+
const parsed = Date.parse(String(val));
|
|
5805
|
+
if (Number.isNaN(parsed)) errors.push({
|
|
5806
|
+
rowNumber,
|
|
5807
|
+
field: field.name,
|
|
5808
|
+
value: val,
|
|
5809
|
+
code: "INVALID_DATA_TYPE",
|
|
5810
|
+
reason: `${label} must be a valid date, received '${val}'.`
|
|
5811
|
+
});
|
|
5812
|
+
break;
|
|
5813
|
+
}
|
|
5814
|
+
case "select": {
|
|
5815
|
+
const options = field.options;
|
|
5816
|
+
if (options && options.length > 0) {
|
|
5817
|
+
const allowedValues = options.map((opt) => typeof opt === "object" ? String(opt.value) : String(opt));
|
|
5818
|
+
if (!allowedValues.includes(String(val))) errors.push({
|
|
5819
|
+
rowNumber,
|
|
5820
|
+
field: field.name,
|
|
5821
|
+
value: val,
|
|
5822
|
+
code: "INVALID_SELECT_OPTION",
|
|
5823
|
+
reason: `${label} value '${val}' is not a valid option (allowed: ${allowedValues.join(", ")}).`
|
|
5824
|
+
});
|
|
5825
|
+
}
|
|
5826
|
+
break;
|
|
5827
|
+
}
|
|
5828
|
+
case "json":
|
|
5829
|
+
if (typeof val === "string") try {
|
|
5830
|
+
data[field.name] = JSON.parse(val);
|
|
5831
|
+
} catch {
|
|
5832
|
+
errors.push({
|
|
5833
|
+
rowNumber,
|
|
5834
|
+
field: field.name,
|
|
5835
|
+
value: val,
|
|
5836
|
+
code: "INVALID_DATA_TYPE",
|
|
5837
|
+
reason: `${label} must be a valid JSON structure.`
|
|
5838
|
+
});
|
|
5839
|
+
}
|
|
5840
|
+
break;
|
|
5841
|
+
}
|
|
5842
|
+
if (typeof field.validate === "function") try {
|
|
5843
|
+
const res = field.validate(val, data);
|
|
5844
|
+
if (typeof res === "string") errors.push({
|
|
5845
|
+
rowNumber,
|
|
5846
|
+
field: field.name,
|
|
5847
|
+
value: val,
|
|
5848
|
+
code: "VALIDATION_RULE_FAILED",
|
|
5849
|
+
reason: res
|
|
5850
|
+
});
|
|
5851
|
+
else if (res === false) errors.push({
|
|
5852
|
+
rowNumber,
|
|
5853
|
+
field: field.name,
|
|
5854
|
+
value: val,
|
|
5855
|
+
code: "VALIDATION_RULE_FAILED",
|
|
5856
|
+
reason: `${label} failed custom validation constraint.`
|
|
5857
|
+
});
|
|
5858
|
+
} catch (err) {
|
|
5859
|
+
errors.push({
|
|
5860
|
+
rowNumber,
|
|
5861
|
+
field: field.name,
|
|
5862
|
+
value: val,
|
|
5863
|
+
code: "VALIDATION_RULE_FAILED",
|
|
5864
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
5865
|
+
});
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
async function checkExistingUnique(storage, collectionSlug, fieldName, value) {
|
|
5869
|
+
if (fieldName === "slug") return await storage.getContentBySlug(collectionSlug, String(value)) !== null;
|
|
5870
|
+
return (await storage.findContent(collectionSlug, {
|
|
5871
|
+
where: { [fieldName]: value },
|
|
5872
|
+
limit: 1
|
|
5873
|
+
})).items.length > 0;
|
|
5874
|
+
}
|
|
5875
|
+
async function checkRelationshipExists(storage, targetCollection, refId) {
|
|
5876
|
+
const idStr = String(refId);
|
|
5877
|
+
if (await storage.getContent(targetCollection, idStr) !== null) return true;
|
|
5878
|
+
return await storage.getContentBySlug(targetCollection, idStr) !== null;
|
|
5879
|
+
}
|
|
5880
|
+
//#endregion
|
|
5881
|
+
//#region src/plugins/transfer/presets/hrms.ts
|
|
5882
|
+
/**
|
|
5883
|
+
* Preconfigured mapping definition for importing and exporting HRMS Employers.
|
|
5884
|
+
*/
|
|
5885
|
+
const HRMS_EMPLOYER_TRANSFER_PRESET = {
|
|
5886
|
+
id: "hrms_employers_preset",
|
|
5887
|
+
name: "HRMS Employers Standard Mapping",
|
|
5888
|
+
collectionSlug: "employers",
|
|
5889
|
+
fields: [
|
|
5890
|
+
{
|
|
5891
|
+
sourceField: "Company Name",
|
|
5892
|
+
targetField: "companyName",
|
|
5893
|
+
required: true
|
|
5894
|
+
},
|
|
5895
|
+
{
|
|
5896
|
+
sourceField: "Legal Name",
|
|
5897
|
+
targetField: "legalName"
|
|
5898
|
+
},
|
|
5899
|
+
{
|
|
5900
|
+
sourceField: "Tax ID",
|
|
5901
|
+
targetField: "taxId"
|
|
5902
|
+
},
|
|
5903
|
+
{
|
|
5904
|
+
sourceField: "Primary Email",
|
|
5905
|
+
targetField: "email",
|
|
5906
|
+
transform: "trim"
|
|
5907
|
+
},
|
|
5908
|
+
{
|
|
5909
|
+
sourceField: "Phone",
|
|
5910
|
+
targetField: "phone",
|
|
5911
|
+
transform: "trim"
|
|
5912
|
+
},
|
|
5913
|
+
{
|
|
5914
|
+
sourceField: "Website",
|
|
5915
|
+
targetField: "website"
|
|
5916
|
+
},
|
|
5917
|
+
{
|
|
5918
|
+
sourceField: "Timezone",
|
|
5919
|
+
targetField: "timezone",
|
|
5920
|
+
defaultValue: "UTC"
|
|
5921
|
+
},
|
|
5922
|
+
{
|
|
5923
|
+
sourceField: "Status",
|
|
5924
|
+
targetField: "status",
|
|
5925
|
+
defaultValue: "active"
|
|
5926
|
+
}
|
|
5927
|
+
],
|
|
5928
|
+
options: {
|
|
5929
|
+
autoGenerateSlug: true,
|
|
5930
|
+
defaultStatus: "published",
|
|
5931
|
+
onDuplicate: "update",
|
|
5932
|
+
uniqueIdentifierField: "taxId"
|
|
5933
|
+
}
|
|
5934
|
+
};
|
|
5935
|
+
/**
|
|
5936
|
+
* Preconfigured mapping definition for importing and exporting HRMS Employees.
|
|
5937
|
+
*/
|
|
5938
|
+
const HRMS_EMPLOYEE_TRANSFER_PRESET = {
|
|
5939
|
+
id: "hrms_employees_preset",
|
|
5940
|
+
name: "HRMS Employees Standard Mapping",
|
|
5941
|
+
collectionSlug: "employees",
|
|
5942
|
+
fields: [
|
|
5943
|
+
{
|
|
5944
|
+
sourceField: "Employer ID",
|
|
5945
|
+
targetField: "employerId",
|
|
5946
|
+
required: true
|
|
5947
|
+
},
|
|
5948
|
+
{
|
|
5949
|
+
sourceField: "Employee Number",
|
|
5950
|
+
targetField: "employeeNumber",
|
|
5951
|
+
required: true,
|
|
5952
|
+
transform: "trim"
|
|
5953
|
+
},
|
|
5954
|
+
{
|
|
5955
|
+
sourceField: "First Name",
|
|
5956
|
+
targetField: "firstName",
|
|
5957
|
+
required: true,
|
|
5958
|
+
transform: "trim"
|
|
5959
|
+
},
|
|
5960
|
+
{
|
|
5961
|
+
sourceField: "Last Name",
|
|
5962
|
+
targetField: "lastName",
|
|
5963
|
+
required: true,
|
|
5964
|
+
transform: "trim"
|
|
5965
|
+
},
|
|
5966
|
+
{
|
|
5967
|
+
sourceField: "Work Email",
|
|
5968
|
+
targetField: "email",
|
|
5969
|
+
required: true,
|
|
5970
|
+
transform: "lowercase"
|
|
5971
|
+
},
|
|
5972
|
+
{
|
|
5973
|
+
sourceField: "Phone",
|
|
5974
|
+
targetField: "phone",
|
|
5975
|
+
transform: "trim"
|
|
5976
|
+
},
|
|
5977
|
+
{
|
|
5978
|
+
sourceField: "Job Title",
|
|
5979
|
+
targetField: "jobTitle"
|
|
5980
|
+
},
|
|
5981
|
+
{
|
|
5982
|
+
sourceField: "Employment Type",
|
|
5983
|
+
targetField: "employmentType",
|
|
5984
|
+
defaultValue: "full_time"
|
|
5985
|
+
},
|
|
5986
|
+
{
|
|
5987
|
+
sourceField: "Hire Date",
|
|
5988
|
+
targetField: "hireDate",
|
|
5989
|
+
required: true,
|
|
5990
|
+
transform: "date"
|
|
5991
|
+
},
|
|
5992
|
+
{
|
|
5993
|
+
sourceField: "Status",
|
|
5994
|
+
targetField: "status",
|
|
5995
|
+
defaultValue: "active"
|
|
5996
|
+
}
|
|
5997
|
+
],
|
|
5998
|
+
options: {
|
|
5999
|
+
autoGenerateSlug: true,
|
|
6000
|
+
defaultStatus: "published",
|
|
6001
|
+
onDuplicate: "error",
|
|
6002
|
+
uniqueIdentifierField: "employeeNumber"
|
|
6003
|
+
}
|
|
6004
|
+
};
|
|
6005
|
+
//#endregion
|
|
6006
|
+
//#region src/plugins/transfer/service.ts
|
|
6007
|
+
var TransferService = class {
|
|
6008
|
+
engine;
|
|
6009
|
+
options;
|
|
6010
|
+
presets = /* @__PURE__ */ new Map();
|
|
6011
|
+
constructor(engine, options = {}) {
|
|
6012
|
+
this.engine = engine;
|
|
6013
|
+
this.options = options;
|
|
6014
|
+
this.registerPreset(HRMS_EMPLOYER_TRANSFER_PRESET);
|
|
6015
|
+
this.registerPreset(HRMS_EMPLOYEE_TRANSFER_PRESET);
|
|
6016
|
+
}
|
|
6017
|
+
/**
|
|
6018
|
+
* List all registered collections across all active plugins in the engine.
|
|
6019
|
+
*/
|
|
6020
|
+
listCollections() {
|
|
6021
|
+
return this.engine.getCollections().map(summarizeCollection);
|
|
6022
|
+
}
|
|
6023
|
+
/**
|
|
6024
|
+
* Get the collection schema summary for a specific collection slug.
|
|
6025
|
+
*/
|
|
6026
|
+
getCollectionSchema(collectionSlug) {
|
|
6027
|
+
return summarizeCollection(this.getCollectionConfigOrThrow(collectionSlug));
|
|
6028
|
+
}
|
|
6029
|
+
/**
|
|
6030
|
+
* Register a reusable data mapping preset.
|
|
6031
|
+
*/
|
|
6032
|
+
registerPreset(preset) {
|
|
6033
|
+
const id = preset.id || `${preset.collectionSlug}_${preset.name || "preset"}`;
|
|
6034
|
+
this.presets.set(id, {
|
|
6035
|
+
...preset,
|
|
6036
|
+
id
|
|
6037
|
+
});
|
|
6038
|
+
}
|
|
6039
|
+
/**
|
|
6040
|
+
* Get a registered preset by ID.
|
|
6041
|
+
*/
|
|
6042
|
+
getPreset(id) {
|
|
6043
|
+
return this.presets.get(id);
|
|
6044
|
+
}
|
|
6045
|
+
/**
|
|
6046
|
+
* Get all registered presets, optionally filtered by target collection.
|
|
6047
|
+
*/
|
|
6048
|
+
getPresets(collectionSlug) {
|
|
6049
|
+
const all = Array.from(this.presets.values());
|
|
6050
|
+
if (collectionSlug) return all.filter((p) => p.collectionSlug === collectionSlug);
|
|
6051
|
+
return all;
|
|
6052
|
+
}
|
|
6053
|
+
/**
|
|
6054
|
+
* Generate a 1:1 default mapping template for any collection.
|
|
6055
|
+
*/
|
|
6056
|
+
getMappingTemplate(collectionSlug) {
|
|
6057
|
+
const config = this.getCollectionConfigOrThrow(collectionSlug);
|
|
6058
|
+
return {
|
|
6059
|
+
id: `${collectionSlug}_default_template`,
|
|
6060
|
+
name: `${config.label} Default Template`,
|
|
6061
|
+
collectionSlug,
|
|
6062
|
+
fields: config.fields.map((f) => ({
|
|
6063
|
+
sourceField: f.name,
|
|
6064
|
+
targetField: f.name,
|
|
6065
|
+
required: Boolean(f.required),
|
|
6066
|
+
defaultValue: f.defaultValue
|
|
6067
|
+
})),
|
|
6068
|
+
options: {
|
|
6069
|
+
autoGenerateSlug: true,
|
|
6070
|
+
defaultStatus: config.draftable ? "draft" : "published",
|
|
6071
|
+
onDuplicate: "error"
|
|
6072
|
+
}
|
|
6073
|
+
};
|
|
6074
|
+
}
|
|
6075
|
+
/**
|
|
6076
|
+
* Inspect any source payload or file (JSON, Excel, CSV) to extract headers,
|
|
6077
|
+
* infer data types, and generate auto-mapping suggestions against a target collection.
|
|
6078
|
+
*/
|
|
6079
|
+
async inspectSource(input, options = {}) {
|
|
6080
|
+
const config = options.collectionSlug ? this.engine.getCollectionConfig(options.collectionSlug) : void 0;
|
|
6081
|
+
return inspectSource(input, {
|
|
6082
|
+
...options,
|
|
6083
|
+
collectionConfig: config
|
|
6084
|
+
});
|
|
6085
|
+
}
|
|
6086
|
+
/**
|
|
6087
|
+
* Dry-run validation of a source batch against a target collection's schema
|
|
6088
|
+
* and live database constraints without committing any changes.
|
|
6089
|
+
*/
|
|
6090
|
+
async validateImport(collectionSlug, input, mapping, options = {}) {
|
|
6091
|
+
const config = this.getCollectionConfigOrThrow(collectionSlug);
|
|
6092
|
+
const parsed = await parseSource(input, {
|
|
6093
|
+
format: options.format,
|
|
6094
|
+
fileName: options.fileName,
|
|
6095
|
+
sheetName: options.sheetName
|
|
6096
|
+
});
|
|
6097
|
+
const activeMapping = mapping || this.getMappingTemplate(collectionSlug);
|
|
6098
|
+
return validateTransferBatch(parsed.records, activeMapping, config, {
|
|
6099
|
+
storage: this.engine.storage,
|
|
6100
|
+
onDuplicate: options.onDuplicate,
|
|
6101
|
+
previewLimit: options.previewLimit
|
|
6102
|
+
});
|
|
6103
|
+
}
|
|
6104
|
+
/**
|
|
6105
|
+
* Execute an import batch from raw data or an external file (JSON, Excel, CSV)
|
|
6106
|
+
* into a target CMS collection.
|
|
6107
|
+
*/
|
|
6108
|
+
async importData(collectionSlug, input, mapping, options = {}) {
|
|
6109
|
+
const config = this.getCollectionConfigOrThrow(collectionSlug);
|
|
6110
|
+
const parsed = await parseSource(input, {
|
|
6111
|
+
format: options.format,
|
|
6112
|
+
fileName: options.fileName,
|
|
6113
|
+
sheetName: options.sheetName
|
|
6114
|
+
});
|
|
6115
|
+
const activeMapping = mapping || this.getMappingTemplate(collectionSlug);
|
|
6116
|
+
const rawRecords = parsed.records;
|
|
6117
|
+
const onDuplicate = options.onDuplicate ?? activeMapping.options?.onDuplicate ?? "error";
|
|
6118
|
+
const abortOnError = options.abortOnError ?? activeMapping.options?.abortOnError ?? false;
|
|
6119
|
+
const validation = await validateTransferBatch(rawRecords, activeMapping, config, {
|
|
6120
|
+
storage: this.engine.storage,
|
|
6121
|
+
onDuplicate
|
|
6122
|
+
});
|
|
6123
|
+
if (options.dryRun) return {
|
|
6124
|
+
success: validation.valid,
|
|
6125
|
+
collectionSlug,
|
|
6126
|
+
totalRows: rawRecords.length,
|
|
6127
|
+
importedCount: validation.validCount,
|
|
6128
|
+
updatedCount: 0,
|
|
6129
|
+
skippedCount: 0,
|
|
6130
|
+
failedCount: validation.errorCount,
|
|
6131
|
+
createdIds: [],
|
|
6132
|
+
updatedIds: [],
|
|
6133
|
+
errors: validation.errors
|
|
6134
|
+
};
|
|
6135
|
+
if (abortOnError && !validation.valid) return {
|
|
6136
|
+
success: false,
|
|
6137
|
+
collectionSlug,
|
|
6138
|
+
totalRows: rawRecords.length,
|
|
6139
|
+
importedCount: 0,
|
|
6140
|
+
updatedCount: 0,
|
|
6141
|
+
skippedCount: 0,
|
|
6142
|
+
failedCount: validation.errorCount,
|
|
6143
|
+
createdIds: [],
|
|
6144
|
+
updatedIds: [],
|
|
6145
|
+
errors: validation.errors
|
|
6146
|
+
};
|
|
6147
|
+
await this.engine.hooks.doAction("cms.transfer_import_started", {
|
|
6148
|
+
collectionSlug,
|
|
6149
|
+
totalRows: rawRecords.length
|
|
6150
|
+
});
|
|
6151
|
+
const createdIds = [];
|
|
6152
|
+
const updatedIds = [];
|
|
6153
|
+
const errors = [...validation.errors];
|
|
6154
|
+
let importedCount = 0;
|
|
6155
|
+
let updatedCount = 0;
|
|
6156
|
+
let skippedCount = 0;
|
|
6157
|
+
const rowErrorsMap = /* @__PURE__ */ new Map();
|
|
6158
|
+
for (const err of validation.errors) {
|
|
6159
|
+
const list = rowErrorsMap.get(err.rowNumber) || [];
|
|
6160
|
+
list.push(err);
|
|
6161
|
+
rowErrorsMap.set(err.rowNumber, list);
|
|
6162
|
+
}
|
|
6163
|
+
const collService = this.engine.collection(collectionSlug);
|
|
6164
|
+
for (let i = 0; i < rawRecords.length; i++) {
|
|
6165
|
+
const rowNumber = i + 1;
|
|
6166
|
+
const rowErrors = rowErrorsMap.get(rowNumber);
|
|
6167
|
+
if (rowErrors && rowErrors.length > 0) {
|
|
6168
|
+
if (rowErrors.some((e) => e.code !== "CONSTRAINT_UNIQUE_VIOLATION" || onDuplicate === "error")) continue;
|
|
6169
|
+
}
|
|
6170
|
+
const raw = rawRecords[i];
|
|
6171
|
+
let mapped;
|
|
6172
|
+
try {
|
|
6173
|
+
mapped = mapSourceRecord(raw, activeMapping);
|
|
6174
|
+
} catch (err) {
|
|
6175
|
+
errors.push({
|
|
6176
|
+
rowNumber,
|
|
6177
|
+
code: "TRANSFORM_ERROR",
|
|
6178
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
6179
|
+
});
|
|
6180
|
+
continue;
|
|
6181
|
+
}
|
|
6182
|
+
mapped = await this.engine.hooks.applyFilters("cms.transfer_before_import_row", mapped, {
|
|
6183
|
+
collectionSlug,
|
|
6184
|
+
rowNumber,
|
|
6185
|
+
raw
|
|
6186
|
+
});
|
|
6187
|
+
const identifierField = options.uniqueIdentifierField || activeMapping.options?.uniqueIdentifierField || this.detectUniqueField(config);
|
|
6188
|
+
let existingItem = null;
|
|
6189
|
+
if (identifierField && mapped.data[identifierField] !== void 0) {
|
|
6190
|
+
const val = mapped.data[identifierField];
|
|
6191
|
+
if (identifierField === "slug") existingItem = await this.engine.storage.getContentBySlug(collectionSlug, String(val));
|
|
6192
|
+
else existingItem = (await this.engine.storage.findContent(collectionSlug, {
|
|
6193
|
+
where: { [identifierField]: val },
|
|
6194
|
+
limit: 1
|
|
6195
|
+
})).items[0] || null;
|
|
6196
|
+
}
|
|
6197
|
+
if (existingItem) {
|
|
6198
|
+
if (onDuplicate === "skip") {
|
|
6199
|
+
skippedCount++;
|
|
6200
|
+
continue;
|
|
6201
|
+
}
|
|
6202
|
+
if (onDuplicate === "update") {
|
|
6203
|
+
try {
|
|
6204
|
+
const updated = await collService.update(existingItem.id, {
|
|
6205
|
+
title: mapped.title ?? existingItem.title,
|
|
6206
|
+
slug: mapped.slug ?? existingItem.slug,
|
|
6207
|
+
status: mapped.status ?? existingItem.status,
|
|
6208
|
+
data: {
|
|
6209
|
+
...existingItem.data,
|
|
6210
|
+
...mapped.data
|
|
6211
|
+
}
|
|
6212
|
+
}, options.authorId, options.revisionNote || "Import transfer update");
|
|
6213
|
+
if (updated) {
|
|
6214
|
+
updatedIds.push(updated.id);
|
|
6215
|
+
updatedCount++;
|
|
6216
|
+
await this.engine.hooks.doAction("cms.transfer_row_imported", {
|
|
6217
|
+
collectionSlug,
|
|
6218
|
+
action: "update",
|
|
6219
|
+
item: updated
|
|
6220
|
+
});
|
|
6221
|
+
}
|
|
6222
|
+
} catch (err) {
|
|
6223
|
+
errors.push({
|
|
6224
|
+
rowNumber,
|
|
6225
|
+
code: "VALIDATION_RULE_FAILED",
|
|
6226
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
6227
|
+
});
|
|
6228
|
+
}
|
|
6229
|
+
continue;
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
try {
|
|
6233
|
+
const created = await collService.create({
|
|
6234
|
+
title: mapped.title,
|
|
6235
|
+
slug: mapped.slug,
|
|
6236
|
+
status: mapped.status,
|
|
6237
|
+
data: mapped.data
|
|
6238
|
+
}, options.authorId);
|
|
6239
|
+
createdIds.push(created.id);
|
|
6240
|
+
importedCount++;
|
|
6241
|
+
await this.engine.hooks.doAction("cms.transfer_row_imported", {
|
|
6242
|
+
collectionSlug,
|
|
6243
|
+
action: "create",
|
|
6244
|
+
item: created
|
|
6245
|
+
});
|
|
6246
|
+
} catch (err) {
|
|
6247
|
+
errors.push({
|
|
6248
|
+
rowNumber,
|
|
6249
|
+
code: "VALIDATION_RULE_FAILED",
|
|
6250
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
6251
|
+
});
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
const result = {
|
|
6255
|
+
success: errors.length === 0,
|
|
6256
|
+
collectionSlug,
|
|
6257
|
+
totalRows: rawRecords.length,
|
|
6258
|
+
importedCount,
|
|
6259
|
+
updatedCount,
|
|
6260
|
+
skippedCount,
|
|
6261
|
+
failedCount: rawRecords.length - (importedCount + updatedCount + skippedCount),
|
|
6262
|
+
createdIds,
|
|
6263
|
+
updatedIds,
|
|
6264
|
+
errors
|
|
6265
|
+
};
|
|
6266
|
+
await this.engine.hooks.doAction("cms.transfer_import_completed", result);
|
|
6267
|
+
await this.engine.hooks.doAction(`cms.${collectionSlug}_transferred`, result);
|
|
6268
|
+
return result;
|
|
6269
|
+
}
|
|
6270
|
+
/**
|
|
6271
|
+
* Export CMS collection records to JSON, Excel (.xlsx), or CSV.
|
|
6272
|
+
*/
|
|
6273
|
+
async exportData(collectionSlug, options = {}) {
|
|
6274
|
+
const config = this.getCollectionConfigOrThrow(collectionSlug);
|
|
6275
|
+
const collService = this.engine.collection(collectionSlug);
|
|
6276
|
+
const query = options.query || { limit: 1e4 };
|
|
6277
|
+
const items = (await collService.find(query)).items;
|
|
6278
|
+
const exportRecords = [];
|
|
6279
|
+
for (const item of items) {
|
|
6280
|
+
let mappedRow = mapCmsItemForExport(item, options.mapping, {
|
|
6281
|
+
includeId: options.includeId,
|
|
6282
|
+
includeTimestamps: options.includeTimestamps
|
|
6283
|
+
});
|
|
6284
|
+
mappedRow = await this.engine.hooks.applyFilters("cms.transfer_before_export_row", mappedRow, {
|
|
6285
|
+
collectionSlug,
|
|
6286
|
+
item
|
|
6287
|
+
});
|
|
6288
|
+
exportRecords.push(mappedRow);
|
|
6289
|
+
}
|
|
6290
|
+
return serializeSource(exportRecords, {
|
|
6291
|
+
format: options.format || "json",
|
|
6292
|
+
fileName: options.fileName || `${collectionSlug}_export_${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
6293
|
+
sheetName: options.sheetName || config.label
|
|
6294
|
+
});
|
|
6295
|
+
}
|
|
6296
|
+
getCollectionConfigOrThrow(collectionSlug) {
|
|
6297
|
+
const config = this.engine.getCollectionConfig(collectionSlug);
|
|
6298
|
+
if (!config) throw new Error(`[TransferService] Collection '${collectionSlug}' is not registered in the CMS engine.`);
|
|
6299
|
+
return config;
|
|
6300
|
+
}
|
|
6301
|
+
detectUniqueField(config) {
|
|
6302
|
+
const unique = config.fields.find((f) => Boolean(f.unique));
|
|
6303
|
+
if (unique) return unique.name;
|
|
6304
|
+
const slugField = config.fields.find((f) => f.type === "slug" || f.name === "slug");
|
|
6305
|
+
if (slugField) return slugField.name;
|
|
6306
|
+
}
|
|
6307
|
+
};
|
|
6308
|
+
//#endregion
|
|
6309
|
+
//#region src/plugins/transfer/routes.ts
|
|
6310
|
+
function json(data, status = 200, headers = {}) {
|
|
6311
|
+
return new Response(JSON.stringify(data), {
|
|
6312
|
+
status,
|
|
6313
|
+
headers: {
|
|
6314
|
+
"Content-Type": "application/json",
|
|
6315
|
+
"Access-Control-Allow-Origin": "*",
|
|
6316
|
+
...headers
|
|
6317
|
+
}
|
|
6318
|
+
});
|
|
6319
|
+
}
|
|
6320
|
+
function badRequest(message) {
|
|
6321
|
+
return json({ error: message }, 400);
|
|
6322
|
+
}
|
|
6323
|
+
function notFound(message) {
|
|
6324
|
+
return json({ error: message }, 404);
|
|
6325
|
+
}
|
|
6326
|
+
function registerTransferRoutes(ctx, service, options = {}) {
|
|
6327
|
+
const prefix = (options.apiPrefix ?? "/api/transfer").replace(/\/+$/, "");
|
|
6328
|
+
ctx.registerRoute("GET", `${prefix}/collections`, async () => {
|
|
6329
|
+
try {
|
|
6330
|
+
return json({ collections: service.listCollections() });
|
|
6331
|
+
} catch (err) {
|
|
6332
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
6333
|
+
}
|
|
6334
|
+
});
|
|
6335
|
+
ctx.registerRoute("GET", `${prefix}/presets`, async (_req, { url }) => {
|
|
6336
|
+
try {
|
|
6337
|
+
const collectionSlug = url.searchParams.get("collection") || void 0;
|
|
6338
|
+
return json({ presets: service.getPresets(collectionSlug) });
|
|
6339
|
+
} catch (err) {
|
|
6340
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
6341
|
+
}
|
|
6342
|
+
});
|
|
6343
|
+
ctx.registerRoute("POST", `${prefix}/presets`, async (req) => {
|
|
6344
|
+
try {
|
|
6345
|
+
const body = await req.json();
|
|
6346
|
+
if (!body.collectionSlug || !Array.isArray(body.fields)) return badRequest("collectionSlug and fields array are required.");
|
|
6347
|
+
service.registerPreset(body);
|
|
6348
|
+
return json({
|
|
6349
|
+
success: true,
|
|
6350
|
+
preset: body
|
|
6351
|
+
}, 201);
|
|
6352
|
+
} catch (err) {
|
|
6353
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
6354
|
+
}
|
|
6355
|
+
});
|
|
6356
|
+
ctx.registerRoute("GET", `${prefix}/:collection/schema`, async (_req, { params }) => {
|
|
6357
|
+
try {
|
|
6358
|
+
return json({
|
|
6359
|
+
schema: service.getCollectionSchema(params.collection),
|
|
6360
|
+
template: service.getMappingTemplate(params.collection)
|
|
6361
|
+
});
|
|
6362
|
+
} catch (err) {
|
|
6363
|
+
return notFound(err instanceof Error ? err.message : String(err));
|
|
6364
|
+
}
|
|
6365
|
+
});
|
|
6366
|
+
ctx.registerRoute("POST", `${prefix}/inspect`, async (req) => {
|
|
6367
|
+
try {
|
|
6368
|
+
const payload = await extractPayload(req);
|
|
6369
|
+
return json(await service.inspectSource(payload.data, {
|
|
6370
|
+
collectionSlug: payload.collectionSlug,
|
|
6371
|
+
format: payload.format,
|
|
6372
|
+
fileName: payload.fileName,
|
|
6373
|
+
sheetName: payload.sheetName
|
|
6374
|
+
}));
|
|
6375
|
+
} catch (err) {
|
|
6376
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
6377
|
+
}
|
|
6378
|
+
});
|
|
6379
|
+
ctx.registerRoute("POST", `${prefix}/:collection/preview`, async (req, { params }) => {
|
|
6380
|
+
try {
|
|
6381
|
+
const payload = await extractPayload(req);
|
|
6382
|
+
return json(await service.validateImport(params.collection, payload.data, payload.mapping, {
|
|
6383
|
+
format: payload.format,
|
|
6384
|
+
fileName: payload.fileName,
|
|
6385
|
+
sheetName: payload.sheetName,
|
|
6386
|
+
onDuplicate: payload.onDuplicate
|
|
6387
|
+
}));
|
|
6388
|
+
} catch (err) {
|
|
6389
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
6390
|
+
}
|
|
6391
|
+
});
|
|
6392
|
+
ctx.registerRoute("POST", `${prefix}/:collection/import`, async (req, { params }) => {
|
|
6393
|
+
try {
|
|
6394
|
+
const payload = await extractPayload(req);
|
|
6395
|
+
const result = await service.importData(params.collection, payload.data, payload.mapping, {
|
|
6396
|
+
format: payload.format,
|
|
6397
|
+
fileName: payload.fileName,
|
|
6398
|
+
sheetName: payload.sheetName,
|
|
6399
|
+
onDuplicate: payload.onDuplicate,
|
|
6400
|
+
uniqueIdentifierField: payload.uniqueIdentifierField,
|
|
6401
|
+
abortOnError: payload.abortOnError,
|
|
6402
|
+
authorId: payload.authorId,
|
|
6403
|
+
revisionNote: payload.revisionNote
|
|
6404
|
+
});
|
|
6405
|
+
return json(result, result.success ? 200 : 207);
|
|
6406
|
+
} catch (err) {
|
|
6407
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
6408
|
+
}
|
|
6409
|
+
});
|
|
6410
|
+
ctx.registerRoute("POST", `${prefix}/:collection/export`, async (req, { params }) => {
|
|
6411
|
+
try {
|
|
6412
|
+
const body = await req.json();
|
|
6413
|
+
const result = await service.exportData(params.collection, {
|
|
6414
|
+
format: body.format,
|
|
6415
|
+
mapping: body.mapping,
|
|
6416
|
+
query: body.query,
|
|
6417
|
+
fileName: body.fileName,
|
|
6418
|
+
sheetName: body.sheetName,
|
|
6419
|
+
includeId: body.includeId,
|
|
6420
|
+
includeTimestamps: body.includeTimestamps
|
|
6421
|
+
});
|
|
6422
|
+
return new Response(result.data, {
|
|
6423
|
+
status: 200,
|
|
6424
|
+
headers: {
|
|
6425
|
+
"Content-Type": result.mimeType,
|
|
6426
|
+
"Content-Disposition": `attachment; filename="${result.fileName}"`,
|
|
6427
|
+
"Access-Control-Allow-Origin": "*"
|
|
6428
|
+
}
|
|
6429
|
+
});
|
|
6430
|
+
} catch (err) {
|
|
6431
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
6432
|
+
}
|
|
6433
|
+
});
|
|
6434
|
+
ctx.registerRoute("GET", `${prefix}/:collection/export`, async (_req, { params, url }) => {
|
|
6435
|
+
try {
|
|
6436
|
+
const format = url.searchParams.get("format") || "json";
|
|
6437
|
+
const fileName = url.searchParams.get("fileName") || void 0;
|
|
6438
|
+
const status = url.searchParams.get("status") || void 0;
|
|
6439
|
+
const search = url.searchParams.get("search") || void 0;
|
|
6440
|
+
const result = await service.exportData(params.collection, {
|
|
6441
|
+
format,
|
|
6442
|
+
fileName,
|
|
6443
|
+
query: {
|
|
6444
|
+
status,
|
|
6445
|
+
search
|
|
6446
|
+
}
|
|
6447
|
+
});
|
|
6448
|
+
return new Response(result.data, {
|
|
6449
|
+
status: 200,
|
|
6450
|
+
headers: {
|
|
6451
|
+
"Content-Type": result.mimeType,
|
|
6452
|
+
"Content-Disposition": `attachment; filename="${result.fileName}"`,
|
|
6453
|
+
"Access-Control-Allow-Origin": "*"
|
|
6454
|
+
}
|
|
6455
|
+
});
|
|
6456
|
+
} catch (err) {
|
|
6457
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
6458
|
+
}
|
|
6459
|
+
});
|
|
6460
|
+
}
|
|
6461
|
+
/**
|
|
6462
|
+
* Helper to extract payload and options from JSON or Multipart requests.
|
|
6463
|
+
*/
|
|
6464
|
+
async function extractPayload(req) {
|
|
6465
|
+
if ((req.headers.get("content-type") || "").includes("multipart/form-data")) {
|
|
6466
|
+
const formData = await req.formData();
|
|
6467
|
+
const file = formData.get("file");
|
|
6468
|
+
let data = null;
|
|
6469
|
+
let fileName;
|
|
6470
|
+
if (file && typeof file === "object" && "arrayBuffer" in file) {
|
|
6471
|
+
data = new Uint8Array(await file.arrayBuffer());
|
|
6472
|
+
fileName = file.name;
|
|
6473
|
+
}
|
|
6474
|
+
const mappingRaw = formData.get("mapping");
|
|
6475
|
+
const mapping = mappingRaw ? JSON.parse(String(mappingRaw)) : void 0;
|
|
6476
|
+
return {
|
|
6477
|
+
data,
|
|
6478
|
+
collectionSlug: formData.get("collectionSlug") || void 0,
|
|
6479
|
+
format: formData.get("format") || void 0,
|
|
6480
|
+
fileName: formData.get("fileName") || fileName,
|
|
6481
|
+
sheetName: formData.get("sheetName") || void 0,
|
|
6482
|
+
mapping,
|
|
6483
|
+
onDuplicate: formData.get("onDuplicate") || void 0,
|
|
6484
|
+
uniqueIdentifierField: formData.get("uniqueIdentifierField") || void 0,
|
|
6485
|
+
abortOnError: formData.get("abortOnError") === "true",
|
|
6486
|
+
authorId: formData.get("authorId") || void 0,
|
|
6487
|
+
revisionNote: formData.get("revisionNote") || void 0
|
|
6488
|
+
};
|
|
6489
|
+
}
|
|
6490
|
+
const body = await req.json();
|
|
6491
|
+
let data = body.data;
|
|
6492
|
+
if (typeof body.base64 === "string") data = Buffer.from(body.base64, "base64");
|
|
6493
|
+
return {
|
|
6494
|
+
data: data !== void 0 ? data : body,
|
|
6495
|
+
collectionSlug: body.collectionSlug,
|
|
6496
|
+
format: body.format,
|
|
6497
|
+
fileName: body.fileName,
|
|
6498
|
+
sheetName: body.sheetName,
|
|
6499
|
+
mapping: body.mapping,
|
|
6500
|
+
onDuplicate: body.onDuplicate,
|
|
6501
|
+
uniqueIdentifierField: body.uniqueIdentifierField,
|
|
6502
|
+
abortOnError: body.abortOnError,
|
|
6503
|
+
authorId: body.authorId,
|
|
6504
|
+
revisionNote: body.revisionNote
|
|
6505
|
+
};
|
|
6506
|
+
}
|
|
6507
|
+
//#endregion
|
|
6508
|
+
//#region src/plugins/transfer/client.ts
|
|
6509
|
+
var TransferClient = class {
|
|
6510
|
+
client;
|
|
6511
|
+
options;
|
|
6512
|
+
service;
|
|
6513
|
+
prefix;
|
|
6514
|
+
constructor(client, options = {}) {
|
|
6515
|
+
this.client = client;
|
|
6516
|
+
this.options = options;
|
|
6517
|
+
this.prefix = (options.apiPrefix ?? "/api/transfer").replace(/\/+$/, "");
|
|
6518
|
+
const engine = client.getEngine();
|
|
6519
|
+
if (engine) this.service = new TransferService(engine, options);
|
|
6520
|
+
}
|
|
6521
|
+
/**
|
|
6522
|
+
* List all registered collections across all active plugins.
|
|
6523
|
+
*/
|
|
6524
|
+
async listCollections() {
|
|
6525
|
+
if (this.service) return this.service.listCollections();
|
|
6526
|
+
return (await this.client.request(`${this.prefix}/collections`)).collections;
|
|
6527
|
+
}
|
|
6528
|
+
/**
|
|
6529
|
+
* Get the collection schema and default mapping template.
|
|
6530
|
+
*/
|
|
6531
|
+
async getSchema(collectionSlug) {
|
|
6532
|
+
if (this.service) return {
|
|
6533
|
+
schema: this.service.getCollectionSchema(collectionSlug),
|
|
6534
|
+
template: this.service.getMappingTemplate(collectionSlug)
|
|
6535
|
+
};
|
|
6536
|
+
return this.client.request(`${this.prefix}/${collectionSlug}/schema`);
|
|
6537
|
+
}
|
|
6538
|
+
/**
|
|
6539
|
+
* Inspect a source file or payload.
|
|
6540
|
+
*/
|
|
6541
|
+
async inspect(data, options = {}) {
|
|
6542
|
+
if (this.service) return this.service.inspectSource(data, options);
|
|
6543
|
+
return this.client.request(`${this.prefix}/inspect`, {
|
|
6544
|
+
method: "POST",
|
|
6545
|
+
body: JSON.stringify({
|
|
6546
|
+
data,
|
|
6547
|
+
...options
|
|
6548
|
+
})
|
|
6549
|
+
});
|
|
6550
|
+
}
|
|
6551
|
+
/**
|
|
6552
|
+
* Dry-run validation of a source batch against a target collection and database constraints.
|
|
6553
|
+
*/
|
|
6554
|
+
async preview(collectionSlug, data, mapping, options = {}) {
|
|
6555
|
+
if (this.service) return this.service.validateImport(collectionSlug, data, mapping, options);
|
|
6556
|
+
return this.client.request(`${this.prefix}/${collectionSlug}/preview`, {
|
|
6557
|
+
method: "POST",
|
|
6558
|
+
body: JSON.stringify({
|
|
6559
|
+
data,
|
|
6560
|
+
mapping,
|
|
6561
|
+
...options
|
|
6562
|
+
})
|
|
6563
|
+
});
|
|
6564
|
+
}
|
|
6565
|
+
/**
|
|
6566
|
+
* Execute an import batch into a target collection.
|
|
6567
|
+
*/
|
|
6568
|
+
async import(collectionSlug, data, mapping, options = {}) {
|
|
6569
|
+
if (this.service) return this.service.importData(collectionSlug, data, mapping, options);
|
|
6570
|
+
return this.client.request(`${this.prefix}/${collectionSlug}/import`, {
|
|
6571
|
+
method: "POST",
|
|
6572
|
+
body: JSON.stringify({
|
|
6573
|
+
data,
|
|
6574
|
+
mapping,
|
|
6575
|
+
...options
|
|
6576
|
+
})
|
|
6577
|
+
});
|
|
6578
|
+
}
|
|
6579
|
+
/**
|
|
6580
|
+
* Export collection records to JSON, Excel, or CSV.
|
|
6581
|
+
*/
|
|
6582
|
+
async export(collectionSlug, options = {}) {
|
|
6583
|
+
if (this.service) return this.service.exportData(collectionSlug, options);
|
|
6584
|
+
return await this.client.request(`${this.prefix}/${collectionSlug}/export`, {
|
|
6585
|
+
method: "POST",
|
|
6586
|
+
body: JSON.stringify(options)
|
|
6587
|
+
});
|
|
6588
|
+
}
|
|
6589
|
+
};
|
|
6590
|
+
/**
|
|
6591
|
+
* Access or instantiate the TransferClient associated with a CMSClient instance.
|
|
6592
|
+
*/
|
|
6593
|
+
function getTransferClient(client, options) {
|
|
6594
|
+
if (client.__transferClient) return client.__transferClient;
|
|
6595
|
+
const tc = new TransferClient(client, options);
|
|
6596
|
+
client.__transferClient = tc;
|
|
6597
|
+
return tc;
|
|
6598
|
+
}
|
|
6599
|
+
//#endregion
|
|
6600
|
+
//#region src/plugins/transfer/index.ts
|
|
6601
|
+
/**
|
|
6602
|
+
* @azlib/cms - Built-in Universal Transfer Plugin
|
|
6603
|
+
*/
|
|
6604
|
+
/**
|
|
6605
|
+
* Built-in Universal Data Transfer plugin factory for @azlib/cms.
|
|
6606
|
+
* Equips the CMS engine with dynamic, schema-driven import and export capabilities
|
|
6607
|
+
* across all collections, featuring file schema discovery, definition mapping,
|
|
6608
|
+
* multi-level data validation, database constraint verification, and multi-format support.
|
|
6609
|
+
*/
|
|
6610
|
+
const transferPlugin = definePlugin((options) => {
|
|
6611
|
+
const opts = options || {};
|
|
6612
|
+
return {
|
|
6613
|
+
name: "transfer",
|
|
6614
|
+
version: "1.0.0",
|
|
6615
|
+
description: "Universal schema-driven data transfer plugin supporting import/export, definition mapping, and constraint validation",
|
|
6616
|
+
setup(ctx) {
|
|
6617
|
+
const service = new TransferService(ctx.engine, opts);
|
|
6618
|
+
ctx.engine.__transferService = service;
|
|
6619
|
+
if (opts.enableRoutes !== false) registerTransferRoutes(ctx, service, opts);
|
|
6620
|
+
}
|
|
6621
|
+
};
|
|
6622
|
+
});
|
|
6623
|
+
/**
|
|
6624
|
+
* Retrieve the active TransferService instance associated with a CMSEngine.
|
|
6625
|
+
*/
|
|
6626
|
+
function getTransferService(engine, options) {
|
|
6627
|
+
if (engine.__transferService) return engine.__transferService;
|
|
6628
|
+
const service = new TransferService(engine, options);
|
|
6629
|
+
engine.__transferService = service;
|
|
6630
|
+
return service;
|
|
6631
|
+
}
|
|
6632
|
+
//#endregion
|
|
6633
|
+
export { CMSClient, CMSEngine, CMSRouter, ContentLifecycle, DEFAULT_COLLECTIONS, DEFAULT_ROLE_CAPABILITIES, EcommerceClient, EcommerceService, HRMSClient, HRMSService, HRMS_EMPLOYEE_TRANSFER_PRESET, HRMS_EMPLOYER_TRANSFER_PRESET, HooksManager, MediaManager, MemoryStorageAdapter, OptionsManager, RBACManager, RevisionManager, TaxonomyManager, TransferClient, TransferService, VALID_STATUS_TRANSITIONS, applyFieldTransform, collection, createAttendanceCollection, createCMSEngine, createCMSRouter, createCmsClient, createDiscountCollection, createEcommerceTaxonomies, createEmployeeCollection, createEmployerCollection, createHRMSTaxonomies, createLeaveRequestCollection, createLeaveTypeCollection, createOrderCollection, createProductCollection, defaultHooks, defineConfig, definePlugin, detectFormat, ecommercePlugin, fields, generateSuggestedMapping, getEcommerceClient, getEcommerceService, getHRMSClient, getHRMSService, getTransferClient, getTransferService, hrmsPlugin, inspectSource, mapCmsItemForExport, mapSourceRecord, normalizeConfig, parseCsvSource, parseExcelSource, parseJsonSource, parseSource, resolveUniqueSlug, serializeCsv, serializeExcel, serializeJson, serializeSource, slugify, summarizeCollection, transferPlugin, validateAndNormalizeData, validateTransferBatch };
|
|
4829
6634
|
|
|
4830
6635
|
//# sourceMappingURL=index.mjs.map
|