@infuro/cms-core 1.0.23 → 1.0.25
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/admin.cjs +5161 -3307
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.d.cts +52 -11
- package/dist/admin.d.ts +52 -11
- package/dist/admin.js +5095 -3238
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +595 -60
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +603 -68
- package/dist/api.js.map +1 -1
- package/dist/auth.cjs +9 -1
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.js +9 -1
- package/dist/auth.js.map +1 -1
- package/dist/cli.cjs +7 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/{index-BGAh4fPQ.d.cts → index--vbixpxE.d.cts} +17 -2
- package/dist/{index-Cnwh7B3r.d.ts → index-DMJgi-fy.d.ts} +17 -2
- package/dist/index.cjs +683 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +684 -76
- package/dist/index.js.map +1 -1
- package/package.json +130 -129
package/dist/index.js
CHANGED
|
@@ -17,10 +17,52 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
17
17
|
return result;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
// src/plugins/erp/erp-log.ts
|
|
21
|
+
function logErp(event, detail) {
|
|
22
|
+
if (detail && Object.keys(detail).length) console.info(ERP_LOG, event, detail);
|
|
23
|
+
else console.info(ERP_LOG, event);
|
|
24
|
+
}
|
|
25
|
+
function warnErp(event, detail) {
|
|
26
|
+
console.warn(ERP_LOG, event, detail);
|
|
27
|
+
}
|
|
28
|
+
function errorErp(event, detail) {
|
|
29
|
+
console.error(ERP_LOG, event, detail);
|
|
30
|
+
}
|
|
31
|
+
function erpSafeWebhookUrl(url) {
|
|
32
|
+
try {
|
|
33
|
+
const u = new URL(url);
|
|
34
|
+
return `${u.origin}${u.pathname}`;
|
|
35
|
+
} catch {
|
|
36
|
+
return "(invalid webhook URL)";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
var ERP_LOG;
|
|
40
|
+
var init_erp_log = __esm({
|
|
41
|
+
"src/plugins/erp/erp-log.ts"() {
|
|
42
|
+
"use strict";
|
|
43
|
+
ERP_LOG = "[webcore:erp]";
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
20
47
|
// src/plugins/erp/erp-queue.ts
|
|
48
|
+
function queuePayloadSummary(payload) {
|
|
49
|
+
if (payload.kind === "order") {
|
|
50
|
+
const o = payload.order;
|
|
51
|
+
return {
|
|
52
|
+
kind: payload.kind,
|
|
53
|
+
platformOrderId: o.platformOrderId ?? o.platformOrderNumber,
|
|
54
|
+
itemCount: Array.isArray(o.items) ? o.items.length : 0
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { kind: payload.kind };
|
|
58
|
+
}
|
|
21
59
|
async function queueErp(cms, payload) {
|
|
22
60
|
const queue = cms.getPlugin("queue");
|
|
23
|
-
if (!queue)
|
|
61
|
+
if (!queue) {
|
|
62
|
+
warnErp("queue:add_skipped", { reason: "queue_plugin_missing", ...queuePayloadSummary(payload) });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
logErp("queue:add", { job: ERP_QUEUE_NAME, ...queuePayloadSummary(payload) });
|
|
24
66
|
await queue.add(ERP_QUEUE_NAME, payload);
|
|
25
67
|
}
|
|
26
68
|
function registerErpQueueProcessor(cms) {
|
|
@@ -28,18 +70,31 @@ function registerErpQueueProcessor(cms) {
|
|
|
28
70
|
if (!queue) return;
|
|
29
71
|
queue.registerProcessor(ERP_QUEUE_NAME, async (data) => {
|
|
30
72
|
const erp = cms.getPlugin("erp");
|
|
31
|
-
if (!erp)
|
|
73
|
+
if (!erp) {
|
|
74
|
+
warnErp("queue:processor_skip", { reason: "erp_plugin_missing" });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
32
77
|
const payload = data;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
78
|
+
logErp("queue:job_start", queuePayloadSummary(payload));
|
|
79
|
+
try {
|
|
80
|
+
if (payload.kind === "lead") {
|
|
81
|
+
await erp.submission.submitContact(payload.contact);
|
|
82
|
+
} else if (payload.kind === "formOpportunity") {
|
|
83
|
+
await erp.submission.submitFormOpportunity(payload.contact);
|
|
84
|
+
} else if (payload.kind === "createContact") {
|
|
85
|
+
await erp.submission.submitCreateContact(payload.contact);
|
|
86
|
+
} else if (payload.kind === "order") {
|
|
87
|
+
await erp.submission.submitOrder(payload.order);
|
|
88
|
+
} else if (payload.kind === "productUpsert") {
|
|
89
|
+
await erp.submission.submitProductUpsert(payload.product);
|
|
90
|
+
}
|
|
91
|
+
logErp("queue:job_done", queuePayloadSummary(payload));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
errorErp("queue:job_failed", {
|
|
94
|
+
...queuePayloadSummary(payload),
|
|
95
|
+
message: e instanceof Error ? e.message : String(e)
|
|
96
|
+
});
|
|
97
|
+
throw e;
|
|
43
98
|
}
|
|
44
99
|
});
|
|
45
100
|
}
|
|
@@ -47,6 +102,7 @@ var ERP_QUEUE_NAME;
|
|
|
47
102
|
var init_erp_queue = __esm({
|
|
48
103
|
"src/plugins/erp/erp-queue.ts"() {
|
|
49
104
|
"use strict";
|
|
105
|
+
init_erp_log();
|
|
50
106
|
ERP_QUEUE_NAME = "erp";
|
|
51
107
|
}
|
|
52
108
|
});
|
|
@@ -98,21 +154,36 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
|
|
|
98
154
|
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
99
155
|
for (const row of cfgRows) {
|
|
100
156
|
const r = row;
|
|
101
|
-
if (r.key === "enabled" && r.value === "false")
|
|
157
|
+
if (r.key === "enabled" && r.value === "false") {
|
|
158
|
+
logErp("paid-order:skip", { orderId, reason: "erp_config_disabled" });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!cms.getPlugin("erp")) {
|
|
163
|
+
logErp("paid-order:skip", { orderId, reason: "erp_plugin_missing" });
|
|
164
|
+
return;
|
|
102
165
|
}
|
|
103
|
-
if (!cms.getPlugin("erp")) return;
|
|
104
166
|
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
105
167
|
const ord = await orderRepo.findOne({
|
|
106
168
|
where: { id: orderId },
|
|
107
169
|
relations: ["items", "items.product", "contact", "billingAddress", "shippingAddress", "payments"]
|
|
108
170
|
});
|
|
109
|
-
if (!ord)
|
|
171
|
+
if (!ord) {
|
|
172
|
+
logErp("paid-order:skip", { orderId, reason: "order_not_found" });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
110
175
|
const o = ord;
|
|
111
176
|
const okKind = o.orderKind === void 0 || o.orderKind === null || o.orderKind === "sale";
|
|
112
|
-
if (!okKind)
|
|
177
|
+
if (!okKind) {
|
|
178
|
+
logErp("paid-order:skip", { orderId, reason: "order_kind_not_sale", orderKind: o.orderKind });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
113
181
|
const rawPayments = o.payments ?? [];
|
|
114
182
|
const completedPayments = rawPayments.filter((pay) => pay.status === "completed" && pay.deleted !== true);
|
|
115
|
-
if (!completedPayments.length)
|
|
183
|
+
if (!completedPayments.length) {
|
|
184
|
+
logErp("paid-order:skip", { orderId, reason: "no_completed_payments" });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
116
187
|
const rawItems = o.items ?? [];
|
|
117
188
|
const lines = rawItems.filter((it) => it.product).map((it) => {
|
|
118
189
|
const p = it.product;
|
|
@@ -131,7 +202,10 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
|
|
|
131
202
|
type: itemType
|
|
132
203
|
};
|
|
133
204
|
});
|
|
134
|
-
if (!lines.length)
|
|
205
|
+
if (!lines.length) {
|
|
206
|
+
logErp("paid-order:skip", { orderId, reason: "no_line_items_with_product" });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
135
209
|
const contact = o.contact;
|
|
136
210
|
const orderTotalMajor = Number(o.total);
|
|
137
211
|
const paymentDtos = completedPayments.length === 1 && Number.isFinite(orderTotalMajor) ? [paymentRowToWebhookDto(completedPayments[0], orderTotalMajor)] : completedPayments.map((pay) => paymentRowToWebhookDto(pay));
|
|
@@ -153,13 +227,28 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
|
|
|
153
227
|
payments: paymentDtos,
|
|
154
228
|
metadata: { ...baseMeta, source: "storefront" }
|
|
155
229
|
};
|
|
230
|
+
logErp("paid-order:payload_built", {
|
|
231
|
+
orderId,
|
|
232
|
+
platformOrderId: orderDto.platformOrderId,
|
|
233
|
+
status: orderDto.status,
|
|
234
|
+
itemCount: lines.length,
|
|
235
|
+
skus: lines.map((l) => l.sku),
|
|
236
|
+
paymentCount: paymentDtos.length,
|
|
237
|
+
paymentIds: paymentDtos.map((p) => p.id),
|
|
238
|
+
total: orderTotalMajor
|
|
239
|
+
});
|
|
156
240
|
await queueErp(cms, { kind: "order", order: orderDto });
|
|
157
|
-
} catch {
|
|
241
|
+
} catch (e) {
|
|
242
|
+
errorErp("paid-order:enqueue_failed", {
|
|
243
|
+
orderId,
|
|
244
|
+
message: e instanceof Error ? e.message : String(e)
|
|
245
|
+
});
|
|
158
246
|
}
|
|
159
247
|
}
|
|
160
248
|
var init_paid_order_erp = __esm({
|
|
161
249
|
"src/plugins/erp/paid-order-erp.ts"() {
|
|
162
250
|
"use strict";
|
|
251
|
+
init_erp_log();
|
|
163
252
|
init_erp_queue();
|
|
164
253
|
}
|
|
165
254
|
});
|
|
@@ -635,6 +724,7 @@ async function createCmsApp(options) {
|
|
|
635
724
|
}
|
|
636
725
|
|
|
637
726
|
// src/plugins/erp/erp-submission.ts
|
|
727
|
+
init_erp_log();
|
|
638
728
|
var ERPSubmissionService = class {
|
|
639
729
|
webhookUrl;
|
|
640
730
|
webhookJwt;
|
|
@@ -714,7 +804,29 @@ var ERPSubmissionService = class {
|
|
|
714
804
|
};
|
|
715
805
|
return this.postWebhookJson(envelope);
|
|
716
806
|
}
|
|
807
|
+
summarizeWebhookBody(body) {
|
|
808
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
809
|
+
return { bodyKind: typeof body };
|
|
810
|
+
}
|
|
811
|
+
const o = body;
|
|
812
|
+
const out = { event_type: o.event_type, timestamp: o.timestamp };
|
|
813
|
+
const data = o.data;
|
|
814
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
815
|
+
const d = data;
|
|
816
|
+
out.dataKeys = Object.keys(d);
|
|
817
|
+
out.platformOrderId = d.platformOrderId ?? d.platformOrderNumber;
|
|
818
|
+
out.itemCount = Array.isArray(d.items) ? d.items.length : void 0;
|
|
819
|
+
}
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
717
822
|
async postWebhookJson(body) {
|
|
823
|
+
const safeUrl = erpSafeWebhookUrl(this.webhookUrl);
|
|
824
|
+
const bodyJson = JSON.stringify(body);
|
|
825
|
+
logErp("webhook:post_start", {
|
|
826
|
+
url: safeUrl,
|
|
827
|
+
bodyBytes: bodyJson.length,
|
|
828
|
+
...this.summarizeWebhookBody(body)
|
|
829
|
+
});
|
|
718
830
|
try {
|
|
719
831
|
const res = await fetch(this.webhookUrl, {
|
|
720
832
|
method: "POST",
|
|
@@ -722,13 +834,19 @@ var ERPSubmissionService = class {
|
|
|
722
834
|
"Content-Type": "application/json",
|
|
723
835
|
"X-External-Token": this.webhookJwt
|
|
724
836
|
},
|
|
725
|
-
body:
|
|
837
|
+
body: bodyJson
|
|
726
838
|
});
|
|
727
|
-
if (res.ok) return { success: true, status: res.status };
|
|
728
839
|
const text = await res.text();
|
|
840
|
+
const preview = text.length > 500 ? `${text.slice(0, 500)}\u2026` : text;
|
|
841
|
+
if (res.ok) {
|
|
842
|
+
logErp("webhook:post_ok", { status: res.status, responsePreview: preview || "(empty body)" });
|
|
843
|
+
return { success: true, status: res.status };
|
|
844
|
+
}
|
|
845
|
+
warnErp("webhook:post_http_error", { status: res.status, responsePreview: preview });
|
|
729
846
|
return { success: false, error: `${res.status} ${text.slice(0, 500)}`, status: res.status };
|
|
730
847
|
} catch (e) {
|
|
731
848
|
const message = e instanceof Error ? e.message : "ERP webhook request failed";
|
|
849
|
+
errorErp("webhook:post_fetch_failed", { url: safeUrl, message });
|
|
732
850
|
return { success: false, error: message };
|
|
733
851
|
}
|
|
734
852
|
}
|
|
@@ -842,7 +960,20 @@ var ERPSubmissionService = class {
|
|
|
842
960
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
843
961
|
data: orderDto
|
|
844
962
|
};
|
|
845
|
-
|
|
963
|
+
logErp("submitOrder:envelope_ready", {
|
|
964
|
+
event_type: envelope.event_type,
|
|
965
|
+
timestamp: envelope.timestamp,
|
|
966
|
+
platformOrderId: orderDto.platformOrderId ?? orderDto.platformOrderNumber,
|
|
967
|
+
itemCount: Array.isArray(orderDto.items) ? orderDto.items.length : 0,
|
|
968
|
+
paymentCount: Array.isArray(orderDto.payments) ? orderDto.payments.length : 0
|
|
969
|
+
});
|
|
970
|
+
const result = await this.postWebhookJson(envelope);
|
|
971
|
+
if (result.success) {
|
|
972
|
+
logErp("submitOrder:complete", { ok: true, status: result.status });
|
|
973
|
+
} else {
|
|
974
|
+
warnErp("submitOrder:complete", { ok: false, status: result.status, error: result.error });
|
|
975
|
+
}
|
|
976
|
+
return result;
|
|
846
977
|
}
|
|
847
978
|
extractContactData(formData, formFields) {
|
|
848
979
|
const contactData = {
|
|
@@ -3979,11 +4110,11 @@ async function readBufferFromPublicUrl(url) {
|
|
|
3979
4110
|
throw new Error("Unsupported media URL");
|
|
3980
4111
|
}
|
|
3981
4112
|
function sanitizeZipPath(entryName) {
|
|
3982
|
-
const
|
|
3983
|
-
for (const seg of
|
|
4113
|
+
const norm3 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
4114
|
+
for (const seg of norm3) {
|
|
3984
4115
|
if (seg === ".." || seg === ".") return null;
|
|
3985
4116
|
}
|
|
3986
|
-
return
|
|
4117
|
+
return norm3;
|
|
3987
4118
|
}
|
|
3988
4119
|
function shouldSkipEntry(parts) {
|
|
3989
4120
|
if (parts[0] === "__MACOSX") return true;
|
|
@@ -5003,18 +5134,37 @@ function createUsersApiHandlers(config) {
|
|
|
5003
5134
|
try {
|
|
5004
5135
|
const body = await req.json();
|
|
5005
5136
|
if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
|
|
5006
|
-
const
|
|
5007
|
-
|
|
5137
|
+
const email = body.email;
|
|
5138
|
+
const existing = await userRepo().findOne({ where: { email } });
|
|
5139
|
+
if (existing && !existing.deleted) {
|
|
5140
|
+
return json({ error: "User with this email already exists" }, { status: 400 });
|
|
5141
|
+
}
|
|
5008
5142
|
const groupRepo = dataSource.getRepository(entityMap.user_groups);
|
|
5009
5143
|
const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
|
|
5010
5144
|
const gid = body.groupId ?? null;
|
|
5011
5145
|
const isCustomer = !!(customerG && gid === customerG.id);
|
|
5012
5146
|
const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
|
|
5013
5147
|
const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
|
|
5014
|
-
const newUser = await
|
|
5148
|
+
const newUser = existing?.deleted ? await (async () => {
|
|
5149
|
+
await userRepo().update(existing.id, {
|
|
5150
|
+
deleted: false,
|
|
5151
|
+
deletedAt: null,
|
|
5152
|
+
deletedBy: null,
|
|
5153
|
+
name: body.name,
|
|
5154
|
+
email,
|
|
5155
|
+
password: null,
|
|
5156
|
+
blocked,
|
|
5157
|
+
groupId: gid,
|
|
5158
|
+
adminAccess,
|
|
5159
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
5160
|
+
});
|
|
5161
|
+
const row = await userRepo().findOne({ where: { id: existing.id } });
|
|
5162
|
+
if (!row) throw new Error("user missing after restore");
|
|
5163
|
+
return row;
|
|
5164
|
+
})() : await userRepo().save(
|
|
5015
5165
|
userRepo().create({
|
|
5016
5166
|
name: body.name,
|
|
5017
|
-
email
|
|
5167
|
+
email,
|
|
5018
5168
|
password: null,
|
|
5019
5169
|
blocked,
|
|
5020
5170
|
groupId: gid,
|
|
@@ -5167,21 +5317,110 @@ function createUserAvatarHandler(config) {
|
|
|
5167
5317
|
}
|
|
5168
5318
|
};
|
|
5169
5319
|
}
|
|
5320
|
+
var PROFILE_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5170
5321
|
function createUserProfileHandler(config) {
|
|
5171
|
-
const { dataSource, entityMap, json, getSession } = config;
|
|
5172
|
-
|
|
5322
|
+
const { dataSource, entityMap, json, getSession, onProfileUpdated } = config;
|
|
5323
|
+
async function loadCurrentUser() {
|
|
5173
5324
|
const session = await getSession();
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5325
|
+
const su = session?.user;
|
|
5326
|
+
if (!su?.email && su?.id == null) {
|
|
5327
|
+
return { ok: false, response: json({ error: "Unauthorized" }, { status: 401 }) };
|
|
5328
|
+
}
|
|
5329
|
+
const userRepo = dataSource.getRepository(entityMap.users);
|
|
5330
|
+
let user = null;
|
|
5331
|
+
const uidRaw = su.id != null ? String(su.id).trim() : "";
|
|
5332
|
+
const uid = uidRaw && /^\d+$/.test(uidRaw) ? parseInt(uidRaw, 10) : NaN;
|
|
5333
|
+
if (Number.isFinite(uid) && uid > 0) {
|
|
5334
|
+
user = await userRepo.findOne({
|
|
5335
|
+
where: { id: uid, deleted: false },
|
|
5336
|
+
select: ["id", "name", "email", "phone", "createdAt"]
|
|
5337
|
+
});
|
|
5338
|
+
}
|
|
5339
|
+
if (!user && su.email) {
|
|
5340
|
+
const em = String(su.email).trim().toLowerCase();
|
|
5341
|
+
if (em) {
|
|
5342
|
+
user = await userRepo.findOne({
|
|
5343
|
+
where: { email: em, deleted: false },
|
|
5344
|
+
select: ["id", "name", "email", "phone", "createdAt"]
|
|
5345
|
+
});
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
if (!user) return { ok: false, response: json({ error: "Not found" }, { status: 404 }) };
|
|
5349
|
+
return { ok: true, user };
|
|
5350
|
+
}
|
|
5351
|
+
return {
|
|
5352
|
+
async GET(_req) {
|
|
5353
|
+
try {
|
|
5354
|
+
const r = await loadCurrentUser();
|
|
5355
|
+
if (!r.ok) return r.response;
|
|
5356
|
+
const u = r.user;
|
|
5357
|
+
return json({
|
|
5358
|
+
id: u.id,
|
|
5359
|
+
name: u.name ?? "",
|
|
5360
|
+
email: u.email ?? "",
|
|
5361
|
+
phone: u.phone ?? null,
|
|
5362
|
+
createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : u.createdAt ?? void 0
|
|
5363
|
+
});
|
|
5364
|
+
} catch {
|
|
5365
|
+
return json({ error: "Internal server error" }, { status: 500 });
|
|
5366
|
+
}
|
|
5367
|
+
},
|
|
5368
|
+
async PUT(req) {
|
|
5369
|
+
try {
|
|
5370
|
+
const r = await loadCurrentUser();
|
|
5371
|
+
if (!r.ok) return r.response;
|
|
5372
|
+
const current = r.user;
|
|
5373
|
+
let body;
|
|
5374
|
+
try {
|
|
5375
|
+
body = await req.json();
|
|
5376
|
+
} catch {
|
|
5377
|
+
return json({ error: "Invalid JSON" }, { status: 400 });
|
|
5378
|
+
}
|
|
5379
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
5380
|
+
if (!name) return json({ error: "Name is required" }, { status: 400 });
|
|
5381
|
+
const emailRaw = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
|
|
5382
|
+
if (!emailRaw || !PROFILE_EMAIL_RE.test(emailRaw)) {
|
|
5383
|
+
return json({ error: "Valid email is required" }, { status: 400 });
|
|
5384
|
+
}
|
|
5385
|
+
const phone = body.phone === null || body.phone === void 0 ? null : typeof body.phone === "string" ? body.phone.trim() || null : null;
|
|
5386
|
+
const userRepo = dataSource.getRepository(entityMap.users);
|
|
5387
|
+
if (emailRaw !== String(current.email ?? "").toLowerCase()) {
|
|
5388
|
+
const taken = await userRepo.findOne({
|
|
5389
|
+
where: { email: emailRaw, deleted: false },
|
|
5390
|
+
select: ["id"]
|
|
5391
|
+
});
|
|
5392
|
+
if (taken && taken.id !== current.id) {
|
|
5393
|
+
return json({ error: "Email is already in use" }, { status: 409 });
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5396
|
+
await userRepo.update(
|
|
5397
|
+
{ id: current.id },
|
|
5398
|
+
{
|
|
5399
|
+
name,
|
|
5400
|
+
email: emailRaw,
|
|
5401
|
+
phone,
|
|
5402
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
5403
|
+
}
|
|
5404
|
+
);
|
|
5405
|
+
const updated = await userRepo.findOne({
|
|
5406
|
+
where: { id: current.id },
|
|
5407
|
+
select: ["id", "name", "email", "phone"]
|
|
5408
|
+
});
|
|
5409
|
+
if (!updated) return json({ error: "Not found" }, { status: 404 });
|
|
5410
|
+
const row = updated;
|
|
5411
|
+
if (onProfileUpdated) {
|
|
5412
|
+
try {
|
|
5413
|
+
await onProfileUpdated(req, row);
|
|
5414
|
+
} catch {
|
|
5415
|
+
}
|
|
5416
|
+
}
|
|
5417
|
+
return json({
|
|
5418
|
+
message: "Profile updated successfully",
|
|
5419
|
+
user: { id: row.id, name: row.name, email: row.email, phone: row.phone }
|
|
5420
|
+
});
|
|
5421
|
+
} catch {
|
|
5422
|
+
return json({ error: "Internal server error" }, { status: 500 });
|
|
5423
|
+
}
|
|
5185
5424
|
}
|
|
5186
5425
|
};
|
|
5187
5426
|
}
|
|
@@ -8515,7 +8754,7 @@ function getNextAuthOptions(config) {
|
|
|
8515
8754
|
}
|
|
8516
8755
|
},
|
|
8517
8756
|
callbacks: {
|
|
8518
|
-
async jwt({ token, user }) {
|
|
8757
|
+
async jwt({ token, user, trigger, session }) {
|
|
8519
8758
|
if (user) {
|
|
8520
8759
|
const u = user;
|
|
8521
8760
|
token.id = u.id;
|
|
@@ -8524,11 +8763,19 @@ function getNextAuthOptions(config) {
|
|
|
8524
8763
|
token.entityPerms = u.entityPerms;
|
|
8525
8764
|
token.adminAccess = u.adminAccess;
|
|
8526
8765
|
}
|
|
8766
|
+
if (trigger === "update" && session && typeof session === "object") {
|
|
8767
|
+
const s = session;
|
|
8768
|
+
const t = token;
|
|
8769
|
+
if (typeof s.name === "string") t.name = s.name;
|
|
8770
|
+
if (typeof s.email === "string") t.email = s.email;
|
|
8771
|
+
}
|
|
8527
8772
|
return token;
|
|
8528
8773
|
},
|
|
8529
8774
|
async session({ session, token }) {
|
|
8530
8775
|
if (session.user) {
|
|
8531
8776
|
const t = token;
|
|
8777
|
+
if (typeof t.name === "string") session.user.name = t.name;
|
|
8778
|
+
if (typeof t.email === "string") session.user.email = t.email;
|
|
8532
8779
|
session.user.id = t.id;
|
|
8533
8780
|
session.user.groupId = t.groupId;
|
|
8534
8781
|
session.user.isRBACAdmin = t.isRBACAdmin;
|
|
@@ -8543,7 +8790,67 @@ function getNextAuthOptions(config) {
|
|
|
8543
8790
|
}
|
|
8544
8791
|
|
|
8545
8792
|
// src/api/crud.ts
|
|
8546
|
-
import { ILike as ILike2, MoreThan as MoreThan2, Not } from "typeorm";
|
|
8793
|
+
import { Between, ILike as ILike2, LessThanOrEqual, MoreThan as MoreThan2, MoreThanOrEqual as MoreThanOrEqual2, Not } from "typeorm";
|
|
8794
|
+
|
|
8795
|
+
// src/lib/address-geo-validation.ts
|
|
8796
|
+
import { Country, State, City } from "country-state-city";
|
|
8797
|
+
function norm2(s) {
|
|
8798
|
+
return typeof s === "string" ? s.trim() : "";
|
|
8799
|
+
}
|
|
8800
|
+
function resolveCountry(input) {
|
|
8801
|
+
const t = input.trim();
|
|
8802
|
+
if (!t) return void 0;
|
|
8803
|
+
if (t.length === 2) {
|
|
8804
|
+
const byCode = Country.getCountryByCode(t.toUpperCase());
|
|
8805
|
+
if (byCode) return byCode;
|
|
8806
|
+
}
|
|
8807
|
+
const lower = t.toLowerCase();
|
|
8808
|
+
return Country.getAllCountries().find((c) => c.name.toLowerCase() === lower);
|
|
8809
|
+
}
|
|
8810
|
+
function resolveState(countryIso, input) {
|
|
8811
|
+
const t = input.trim();
|
|
8812
|
+
if (!t || !countryIso) return void 0;
|
|
8813
|
+
const states = State.getStatesOfCountry(countryIso);
|
|
8814
|
+
const lower = t.toLowerCase();
|
|
8815
|
+
return states.find((s) => s.isoCode.toLowerCase() === t.toLowerCase() || s.name.toLowerCase() === lower);
|
|
8816
|
+
}
|
|
8817
|
+
function resolveCity(countryIso, stateIso, input) {
|
|
8818
|
+
const t = input.trim();
|
|
8819
|
+
if (!t || !countryIso || !stateIso) return void 0;
|
|
8820
|
+
const lower = t.toLowerCase();
|
|
8821
|
+
const cities = City.getCitiesOfState(countryIso, stateIso);
|
|
8822
|
+
return cities.find((c) => c.name.toLowerCase() === lower);
|
|
8823
|
+
}
|
|
8824
|
+
function assertValidAddressHierarchy(country, state, city) {
|
|
8825
|
+
const c = resolveCountry(country);
|
|
8826
|
+
if (!c) return { ok: false, error: "Invalid or unknown country." };
|
|
8827
|
+
const st = resolveState(c.isoCode, state);
|
|
8828
|
+
if (!st) return { ok: false, error: "State or province does not match the selected country." };
|
|
8829
|
+
const ct = resolveCity(c.isoCode, st.isoCode, city);
|
|
8830
|
+
if (!ct) return { ok: false, error: "City does not match the selected state." };
|
|
8831
|
+
return { ok: true, country: c.name, state: st.name, city: ct.name };
|
|
8832
|
+
}
|
|
8833
|
+
function validateAndNormalizeAddressRow(row) {
|
|
8834
|
+
const line1 = norm2(row.line1);
|
|
8835
|
+
const postalCode = norm2(row.postalCode);
|
|
8836
|
+
const countryIn = norm2(row.country);
|
|
8837
|
+
const stateIn = norm2(row.state);
|
|
8838
|
+
const cityIn = norm2(row.city);
|
|
8839
|
+
if (!line1) return "Street address (line 1) is required.";
|
|
8840
|
+
if (!postalCode) return "Postal code is required.";
|
|
8841
|
+
if (!countryIn || !stateIn || !cityIn) return "Country, state, and city are required.";
|
|
8842
|
+
const geo = assertValidAddressHierarchy(countryIn, stateIn, cityIn);
|
|
8843
|
+
if (!geo.ok) return geo.error;
|
|
8844
|
+
row.line1 = line1;
|
|
8845
|
+
row.line2 = norm2(row.line2) || null;
|
|
8846
|
+
row.postalCode = postalCode;
|
|
8847
|
+
row.country = geo.country;
|
|
8848
|
+
row.state = geo.state;
|
|
8849
|
+
row.city = geo.city;
|
|
8850
|
+
return null;
|
|
8851
|
+
}
|
|
8852
|
+
|
|
8853
|
+
// src/api/crud.ts
|
|
8547
8854
|
var CRUD_LOG = "[cms-crud]";
|
|
8548
8855
|
function logCrudClientError(op, detail) {
|
|
8549
8856
|
console.warn(CRUD_LOG, op, detail);
|
|
@@ -8640,6 +8947,156 @@ function buildSearchWhereClause(repo, search) {
|
|
|
8640
8947
|
function entityHasSoftDelete(repo) {
|
|
8641
8948
|
return repo.metadata.columns.some((c) => c.propertyName === "deleted");
|
|
8642
8949
|
}
|
|
8950
|
+
var LIST_QUERY_RESERVED = /* @__PURE__ */ new Set(["page", "limit", "sortField", "sortOrder", "search"]);
|
|
8951
|
+
function dayStartUtc(isoDay) {
|
|
8952
|
+
return /* @__PURE__ */ new Date(isoDay + "T00:00:00.000Z");
|
|
8953
|
+
}
|
|
8954
|
+
function dayEndUtc(isoDay) {
|
|
8955
|
+
return /* @__PURE__ */ new Date(isoDay + "T23:59:59.999Z");
|
|
8956
|
+
}
|
|
8957
|
+
function columnTypeLabel(col) {
|
|
8958
|
+
const t = col.type;
|
|
8959
|
+
if (typeof t === "string") return t.toLowerCase();
|
|
8960
|
+
if (typeof t === "function") return t.name?.toLowerCase?.() ?? "";
|
|
8961
|
+
if (t && typeof t === "object" && "name" in t && typeof t.name === "string") {
|
|
8962
|
+
return String(t.name).toLowerCase();
|
|
8963
|
+
}
|
|
8964
|
+
return "";
|
|
8965
|
+
}
|
|
8966
|
+
function isListDateColumn(col) {
|
|
8967
|
+
const tl = columnTypeLabel(col);
|
|
8968
|
+
return DATE_COLUMN_TYPES.has(tl) || col.type === Date || TIMESTAMP_PROP_NAMES.has(col.propertyName);
|
|
8969
|
+
}
|
|
8970
|
+
function isListNumericColumn(col) {
|
|
8971
|
+
const tl = columnTypeLabel(col);
|
|
8972
|
+
if (col.type === Number) return true;
|
|
8973
|
+
const patterns = [
|
|
8974
|
+
"int",
|
|
8975
|
+
"integer",
|
|
8976
|
+
"int2",
|
|
8977
|
+
"int4",
|
|
8978
|
+
"int8",
|
|
8979
|
+
"smallint",
|
|
8980
|
+
"bigint",
|
|
8981
|
+
"float",
|
|
8982
|
+
"double",
|
|
8983
|
+
"decimal",
|
|
8984
|
+
"numeric",
|
|
8985
|
+
"real",
|
|
8986
|
+
"tinyint",
|
|
8987
|
+
"mediumint"
|
|
8988
|
+
];
|
|
8989
|
+
if (patterns.some((x) => tl.includes(x))) return true;
|
|
8990
|
+
if (tl.includes("unsigned") && (tl.includes("int") || tl.includes("bigint") || tl.includes("small"))) return true;
|
|
8991
|
+
if (!tl && /Id$/i.test(col.propertyName) && !TIMESTAMP_PROP_NAMES.has(col.propertyName)) return true;
|
|
8992
|
+
return false;
|
|
8993
|
+
}
|
|
8994
|
+
function isListBooleanColumn(col) {
|
|
8995
|
+
const tl = columnTypeLabel(col);
|
|
8996
|
+
return tl === "boolean" || tl === "bool" || col.type === Boolean;
|
|
8997
|
+
}
|
|
8998
|
+
function isListStringColumn(col) {
|
|
8999
|
+
const tl = columnTypeLabel(col);
|
|
9000
|
+
if (isListDateColumn(col) || isListNumericColumn(col) || isListBooleanColumn(col)) return false;
|
|
9001
|
+
if (["varchar", "character varying", "text", "citext", "uuid", "char", "character", "enum"].some(
|
|
9002
|
+
(x) => tl.includes(x)
|
|
9003
|
+
)) {
|
|
9004
|
+
return true;
|
|
9005
|
+
}
|
|
9006
|
+
if (col.type === String) return true;
|
|
9007
|
+
return false;
|
|
9008
|
+
}
|
|
9009
|
+
function mergeListWhereAnd(where, patch) {
|
|
9010
|
+
if (Object.keys(patch).length === 0) return where;
|
|
9011
|
+
if (Array.isArray(where)) {
|
|
9012
|
+
if (where.length === 0) return [patch];
|
|
9013
|
+
return where.map((w) => ({ ...w, ...patch }));
|
|
9014
|
+
}
|
|
9015
|
+
if (where && typeof where === "object" && Object.keys(where).length > 0) {
|
|
9016
|
+
return { ...where, ...patch };
|
|
9017
|
+
}
|
|
9018
|
+
return patch;
|
|
9019
|
+
}
|
|
9020
|
+
function buildListFilterAndFromSearchParams(repo, searchParams) {
|
|
9021
|
+
const and = {};
|
|
9022
|
+
const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
9023
|
+
for (const col of repo.metadata.columns) {
|
|
9024
|
+
const name = col.propertyName;
|
|
9025
|
+
if (!columnNames.has(name)) continue;
|
|
9026
|
+
if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
|
|
9027
|
+
if (!isListDateColumn(col)) continue;
|
|
9028
|
+
const from = searchParams.get(`${name}From`)?.trim();
|
|
9029
|
+
const to = searchParams.get(`${name}To`)?.trim();
|
|
9030
|
+
if (!from && !to) continue;
|
|
9031
|
+
if (from && to) {
|
|
9032
|
+
and[name] = Between(dayStartUtc(from), dayEndUtc(to));
|
|
9033
|
+
} else if (from) {
|
|
9034
|
+
and[name] = MoreThanOrEqual2(dayStartUtc(from));
|
|
9035
|
+
} else if (to) {
|
|
9036
|
+
and[name] = LessThanOrEqual(dayEndUtc(to));
|
|
9037
|
+
}
|
|
9038
|
+
}
|
|
9039
|
+
for (const col of repo.metadata.columns) {
|
|
9040
|
+
const name = col.propertyName;
|
|
9041
|
+
if (!columnNames.has(name)) continue;
|
|
9042
|
+
if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
|
|
9043
|
+
if (!isListNumericColumn(col)) continue;
|
|
9044
|
+
if (Object.prototype.hasOwnProperty.call(and, name)) continue;
|
|
9045
|
+
const minRaw = searchParams.get(`${name}Min`)?.trim();
|
|
9046
|
+
const maxRaw = searchParams.get(`${name}Max`)?.trim();
|
|
9047
|
+
if (!minRaw && !maxRaw) continue;
|
|
9048
|
+
const parseNum = (s) => {
|
|
9049
|
+
const n = Number(s);
|
|
9050
|
+
return Number.isFinite(n) ? n : null;
|
|
9051
|
+
};
|
|
9052
|
+
const nMin = minRaw ? parseNum(minRaw) : null;
|
|
9053
|
+
const nMax = maxRaw ? parseNum(maxRaw) : null;
|
|
9054
|
+
if (nMin != null && nMax != null) {
|
|
9055
|
+
and[name] = Between(nMin, nMax);
|
|
9056
|
+
} else if (nMin != null) {
|
|
9057
|
+
and[name] = MoreThanOrEqual2(nMin);
|
|
9058
|
+
} else if (nMax != null) {
|
|
9059
|
+
and[name] = LessThanOrEqual(nMax);
|
|
9060
|
+
}
|
|
9061
|
+
}
|
|
9062
|
+
for (const col of repo.metadata.columns) {
|
|
9063
|
+
const name = col.propertyName;
|
|
9064
|
+
if (!columnNames.has(name)) continue;
|
|
9065
|
+
if (LIST_QUERY_RESERVED.has(name)) continue;
|
|
9066
|
+
if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
|
|
9067
|
+
if (!isListStringColumn(col)) continue;
|
|
9068
|
+
if (Object.prototype.hasOwnProperty.call(and, name)) continue;
|
|
9069
|
+
const raw = searchParams.get(name)?.trim();
|
|
9070
|
+
if (!raw) continue;
|
|
9071
|
+
and[name] = ILike2(`%${raw}%`);
|
|
9072
|
+
}
|
|
9073
|
+
return and;
|
|
9074
|
+
}
|
|
9075
|
+
function buildExactListParamWhere(repo, searchParams) {
|
|
9076
|
+
const extraWhere = {};
|
|
9077
|
+
const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
9078
|
+
for (const col of repo.metadata.columns) {
|
|
9079
|
+
const name = col.propertyName;
|
|
9080
|
+
if (!columnNames.has(name)) continue;
|
|
9081
|
+
if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
|
|
9082
|
+
if (!isListNumericColumn(col)) continue;
|
|
9083
|
+
const v = searchParams.get(name)?.trim();
|
|
9084
|
+
if (v == null || v === "") continue;
|
|
9085
|
+
const n = Number(v);
|
|
9086
|
+
if (!Number.isFinite(n)) continue;
|
|
9087
|
+
extraWhere[name] = n;
|
|
9088
|
+
}
|
|
9089
|
+
for (const col of repo.metadata.columns) {
|
|
9090
|
+
if (String(col.type) !== "boolean") continue;
|
|
9091
|
+
const name = col.propertyName;
|
|
9092
|
+
if (!columnNames.has(name)) continue;
|
|
9093
|
+
const raw = searchParams.get(name)?.trim();
|
|
9094
|
+
if (raw === "true" || raw === "false") {
|
|
9095
|
+
extraWhere[name] = raw === "true";
|
|
9096
|
+
}
|
|
9097
|
+
}
|
|
9098
|
+
return extraWhere;
|
|
9099
|
+
}
|
|
8643
9100
|
function mergeDeletedFalseWhere(repo, where) {
|
|
8644
9101
|
if (!entityHasSoftDelete(repo)) return where;
|
|
8645
9102
|
const d = { deleted: false };
|
|
@@ -8649,6 +9106,12 @@ function mergeDeletedFalseWhere(repo, where) {
|
|
|
8649
9106
|
}
|
|
8650
9107
|
return Object.keys(where).length > 0 ? { ...where, ...d } : d;
|
|
8651
9108
|
}
|
|
9109
|
+
function pickDefaultListSortField(columnNames, columns) {
|
|
9110
|
+
for (const candidate of ["createdAt", "updatedAt", "id", "name", "sortOrder", "title"]) {
|
|
9111
|
+
if (columnNames.has(candidate)) return candidate;
|
|
9112
|
+
}
|
|
9113
|
+
return columns[0]?.propertyName ?? "id";
|
|
9114
|
+
}
|
|
8652
9115
|
function normalizeProductSku(value) {
|
|
8653
9116
|
if (value == null) return null;
|
|
8654
9117
|
const s = String(value).trim();
|
|
@@ -8751,6 +9214,18 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
8751
9214
|
if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
|
|
8752
9215
|
if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
|
|
8753
9216
|
if (dateTo) qb.andWhere("order.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
|
|
9217
|
+
const totalMin = searchParams.get("totalMin")?.trim();
|
|
9218
|
+
const totalMax = searchParams.get("totalMax")?.trim();
|
|
9219
|
+
if (totalMin) {
|
|
9220
|
+
const n = Number(totalMin);
|
|
9221
|
+
if (Number.isFinite(n)) qb.andWhere("order.total >= :totalMin", { totalMin: n });
|
|
9222
|
+
}
|
|
9223
|
+
if (totalMax) {
|
|
9224
|
+
const n = Number(totalMax);
|
|
9225
|
+
if (Number.isFinite(n)) qb.andWhere("order.total <= :totalMax", { totalMax: n });
|
|
9226
|
+
}
|
|
9227
|
+
const currency = searchParams.get("currency")?.trim();
|
|
9228
|
+
if (currency) qb.andWhere("order.currency ILIKE :orderCurrency", { orderCurrency: `%${currency}%` });
|
|
8754
9229
|
if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
|
|
8755
9230
|
const [rows, total2] = await qb.getManyAndCount();
|
|
8756
9231
|
const data2 = rows.map((order) => {
|
|
@@ -8789,8 +9264,30 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
8789
9264
|
if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
|
|
8790
9265
|
if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
|
|
8791
9266
|
if (dateTo) qb.andWhere("payment.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
|
|
9267
|
+
const paidAtFrom = searchParams.get("paidAtFrom")?.trim();
|
|
9268
|
+
const paidAtTo = searchParams.get("paidAtTo")?.trim();
|
|
9269
|
+
if (paidAtFrom) {
|
|
9270
|
+
qb.andWhere("payment.paidAt >= :paidAtFrom", { paidAtFrom: /* @__PURE__ */ new Date(paidAtFrom + "T00:00:00.000Z") });
|
|
9271
|
+
}
|
|
9272
|
+
if (paidAtTo) {
|
|
9273
|
+
qb.andWhere("payment.paidAt <= :paidAtTo", { paidAtTo: /* @__PURE__ */ new Date(paidAtTo + "T23:59:59.999Z") });
|
|
9274
|
+
}
|
|
8792
9275
|
if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
|
|
8793
9276
|
if (orderNumberParam) qb.andWhere("ord.orderNumber ILIKE :orderNumber", { orderNumber: `%${orderNumberParam}%` });
|
|
9277
|
+
const amountMin = searchParams.get("amountMin")?.trim();
|
|
9278
|
+
const amountMax = searchParams.get("amountMax")?.trim();
|
|
9279
|
+
if (amountMin) {
|
|
9280
|
+
const n = Number(amountMin);
|
|
9281
|
+
if (Number.isFinite(n)) qb.andWhere("payment.amount >= :amountMin", { amountMin: n });
|
|
9282
|
+
}
|
|
9283
|
+
if (amountMax) {
|
|
9284
|
+
const n = Number(amountMax);
|
|
9285
|
+
if (Number.isFinite(n)) qb.andWhere("payment.amount <= :amountMax", { amountMax: n });
|
|
9286
|
+
}
|
|
9287
|
+
const extRef = searchParams.get("externalReference")?.trim();
|
|
9288
|
+
if (extRef) {
|
|
9289
|
+
qb.andWhere("payment.externalReference ILIKE :extRef", { extRef: `%${extRef}%` });
|
|
9290
|
+
}
|
|
8794
9291
|
const [rows, total2] = await qb.getManyAndCount();
|
|
8795
9292
|
const data2 = rows.map((payment) => {
|
|
8796
9293
|
const order = payment.order;
|
|
@@ -8809,18 +9306,36 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
8809
9306
|
const repo2 = dataSource.getRepository(entity);
|
|
8810
9307
|
const statusFilter = searchParams.get("status")?.trim();
|
|
8811
9308
|
const inventory = searchParams.get("inventory")?.trim();
|
|
8812
|
-
const productWhere = {
|
|
9309
|
+
const productWhere = {
|
|
9310
|
+
deleted: false,
|
|
9311
|
+
...buildListFilterAndFromSearchParams(repo2, searchParams)
|
|
9312
|
+
};
|
|
8813
9313
|
if (statusFilter) productWhere.status = statusFilter;
|
|
8814
9314
|
if (inventory === "in_stock") productWhere.quantity = MoreThan2(0);
|
|
8815
9315
|
if (inventory === "out_of_stock") productWhere.quantity = 0;
|
|
9316
|
+
for (const key of ["brandId", "categoryId", "collectionId"]) {
|
|
9317
|
+
const raw = searchParams.get(key)?.trim();
|
|
9318
|
+
if (raw) {
|
|
9319
|
+
const n = Number(raw);
|
|
9320
|
+
if (Number.isFinite(n)) productWhere[key] = n;
|
|
9321
|
+
}
|
|
9322
|
+
}
|
|
9323
|
+
const featuredRaw = searchParams.get("featured")?.trim();
|
|
9324
|
+
if (featuredRaw === "true" || featuredRaw === "false") {
|
|
9325
|
+
productWhere.featured = featuredRaw === "true";
|
|
9326
|
+
}
|
|
8816
9327
|
if (search && typeof search === "string" && search.trim()) {
|
|
8817
9328
|
productWhere.name = ILike2(`%${search.trim()}%`);
|
|
8818
9329
|
}
|
|
9330
|
+
const productColumnNames = new Set(repo2.metadata.columns.map((c) => c.propertyName));
|
|
9331
|
+
const defaultProductSort = pickDefaultListSortField(productColumnNames, repo2.metadata.columns);
|
|
9332
|
+
const sortParam2 = (searchParams.get("sortField") ?? "").trim();
|
|
9333
|
+
const productSortField = sortParam2 && productColumnNames.has(sortParam2) ? sortParam2 : defaultProductSort;
|
|
8819
9334
|
const [data2, total2] = await repo2.findAndCount({
|
|
8820
9335
|
where: Object.keys(productWhere).length ? productWhere : void 0,
|
|
8821
9336
|
skip,
|
|
8822
9337
|
take: limit,
|
|
8823
|
-
order: { [
|
|
9338
|
+
order: { [productSortField]: sortOrder }
|
|
8824
9339
|
});
|
|
8825
9340
|
return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
|
|
8826
9341
|
}
|
|
@@ -8913,27 +9428,22 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
8913
9428
|
}
|
|
8914
9429
|
return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
|
|
8915
9430
|
}
|
|
8916
|
-
const
|
|
9431
|
+
const defaultSortField = pickDefaultListSortField(columnNames, repo.metadata.columns);
|
|
9432
|
+
const sortParam = (searchParams.get("sortField") ?? "").trim();
|
|
9433
|
+
const sortField = sortParam && columnNames.has(sortParam) ? sortParam : defaultSortField;
|
|
8917
9434
|
let where = {};
|
|
8918
9435
|
if (search) {
|
|
8919
9436
|
where = buildSearchWhereClause(repo, search);
|
|
8920
9437
|
}
|
|
8921
|
-
|
|
8922
|
-
const
|
|
8923
|
-
|
|
8924
|
-
const v = searchParams.get(key);
|
|
8925
|
-
if (v != null && v !== "" && columnNames.has(key)) {
|
|
8926
|
-
const n = Number(v);
|
|
8927
|
-
if (Number.isFinite(n)) extraWhere[key] = n;
|
|
8928
|
-
}
|
|
8929
|
-
}
|
|
8930
|
-
if (Object.keys(extraWhere).length > 0) {
|
|
9438
|
+
where = mergeListWhereAnd(where, buildListFilterAndFromSearchParams(repo, searchParams));
|
|
9439
|
+
const exactParamWhere = buildExactListParamWhere(repo, searchParams);
|
|
9440
|
+
if (Object.keys(exactParamWhere).length > 0) {
|
|
8931
9441
|
if (Array.isArray(where)) {
|
|
8932
|
-
where = where.map((w) => ({ ...w, ...
|
|
9442
|
+
where = where.map((w) => ({ ...w, ...exactParamWhere }));
|
|
8933
9443
|
} else if (where && typeof where === "object" && Object.keys(where).length > 0) {
|
|
8934
|
-
where = { ...where, ...
|
|
9444
|
+
where = { ...where, ...exactParamWhere };
|
|
8935
9445
|
} else {
|
|
8936
|
-
where =
|
|
9446
|
+
where = exactParamWhere;
|
|
8937
9447
|
}
|
|
8938
9448
|
}
|
|
8939
9449
|
where = mergeDeletedFalseWhere(repo, where);
|
|
@@ -9013,6 +9523,10 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
9013
9523
|
}
|
|
9014
9524
|
const repo = dataSource.getRepository(entity);
|
|
9015
9525
|
const persistBody = resource === "media" ? body : pickColumnUpdates(repo, body);
|
|
9526
|
+
if (resource === "contacts" && "type" in persistBody) {
|
|
9527
|
+
const t = persistBody.type;
|
|
9528
|
+
if (t === "" || t === "none" || t == null) persistBody.type = null;
|
|
9529
|
+
}
|
|
9016
9530
|
if (resource !== "media" && Object.keys(persistBody).length === 0) {
|
|
9017
9531
|
logCrudClientError("POST create", {
|
|
9018
9532
|
reason: "no_scalar_columns_after_pick",
|
|
@@ -9035,6 +9549,17 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
9035
9549
|
}
|
|
9036
9550
|
}
|
|
9037
9551
|
}
|
|
9552
|
+
if (resource === "addresses") {
|
|
9553
|
+
const cid = Number(persistBody.contactId);
|
|
9554
|
+
if (!Number.isFinite(cid)) {
|
|
9555
|
+
return json({ error: "Valid contactId is required." }, { status: 400 });
|
|
9556
|
+
}
|
|
9557
|
+
if (persistBody.tag === "") persistBody.tag = null;
|
|
9558
|
+
const addrErr = validateAndNormalizeAddressRow(persistBody);
|
|
9559
|
+
if (addrErr) {
|
|
9560
|
+
return json({ error: addrErr }, { status: 400 });
|
|
9561
|
+
}
|
|
9562
|
+
}
|
|
9038
9563
|
sanitizeBodyForEntity(repo, persistBody);
|
|
9039
9564
|
let created;
|
|
9040
9565
|
try {
|
|
@@ -9352,10 +9877,60 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
9352
9877
|
if (!cur) return json({ message: "Not found" }, { status: 404 });
|
|
9353
9878
|
}
|
|
9354
9879
|
const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
|
|
9880
|
+
if (resource === "contacts" && "type" in updatePayload) {
|
|
9881
|
+
const t = updatePayload.type;
|
|
9882
|
+
if (t === "" || t === "none" || t == null) updatePayload.type = null;
|
|
9883
|
+
}
|
|
9355
9884
|
if (resource === "media") {
|
|
9356
9885
|
const u = updatePayload;
|
|
9357
|
-
delete u.parentId;
|
|
9358
9886
|
delete u.kind;
|
|
9887
|
+
if (rawBody && typeof rawBody === "object" && "parentId" in rawBody) {
|
|
9888
|
+
let pid = null;
|
|
9889
|
+
const p = rawBody.parentId;
|
|
9890
|
+
if (p != null && p !== "") {
|
|
9891
|
+
const n = Number(p);
|
|
9892
|
+
if (!Number.isFinite(n)) {
|
|
9893
|
+
return json({ error: "Invalid parentId" }, { status: 400 });
|
|
9894
|
+
}
|
|
9895
|
+
pid = n;
|
|
9896
|
+
}
|
|
9897
|
+
if (pid != null) {
|
|
9898
|
+
const parent = await repo.findOne({
|
|
9899
|
+
where: { id: pid, deleted: false }
|
|
9900
|
+
});
|
|
9901
|
+
if (!parent || parent.kind !== "folder") {
|
|
9902
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
9903
|
+
}
|
|
9904
|
+
}
|
|
9905
|
+
const row = await repo.findOne({
|
|
9906
|
+
where: { id: numericId, deleted: false }
|
|
9907
|
+
});
|
|
9908
|
+
if (!row) return json({ message: "Not found" }, { status: 404 });
|
|
9909
|
+
if (pid === numericId) {
|
|
9910
|
+
return json({ error: "Invalid parentId" }, { status: 400 });
|
|
9911
|
+
}
|
|
9912
|
+
if (row.kind === "folder" && pid != null) {
|
|
9913
|
+
let walk = pid;
|
|
9914
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9915
|
+
while (walk != null) {
|
|
9916
|
+
if (walk === numericId) {
|
|
9917
|
+
return json(
|
|
9918
|
+
{ error: "Cannot move a folder into itself or a descendant folder" },
|
|
9919
|
+
{ status: 400 }
|
|
9920
|
+
);
|
|
9921
|
+
}
|
|
9922
|
+
if (seen.has(walk)) break;
|
|
9923
|
+
seen.add(walk);
|
|
9924
|
+
const anc = await repo.findOne({
|
|
9925
|
+
where: { id: walk, deleted: false }
|
|
9926
|
+
});
|
|
9927
|
+
walk = anc ? anc.parentId ?? null : null;
|
|
9928
|
+
}
|
|
9929
|
+
}
|
|
9930
|
+
u.parentId = pid;
|
|
9931
|
+
} else {
|
|
9932
|
+
delete u.parentId;
|
|
9933
|
+
}
|
|
9359
9934
|
}
|
|
9360
9935
|
if (resource === "products") {
|
|
9361
9936
|
const currentRow = await repo.findOne({
|
|
@@ -9374,6 +9949,26 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
9374
9949
|
updatePayload.sku = effSku;
|
|
9375
9950
|
}
|
|
9376
9951
|
}
|
|
9952
|
+
if (resource === "addresses" && Object.keys(updatePayload).length > 0) {
|
|
9953
|
+
const currentRow = await repo.findOne({
|
|
9954
|
+
where: { id: numericId }
|
|
9955
|
+
});
|
|
9956
|
+
if (!currentRow) return json({ message: "Not found" }, { status: 404 });
|
|
9957
|
+
const merged = {
|
|
9958
|
+
...currentRow,
|
|
9959
|
+
...updatePayload
|
|
9960
|
+
};
|
|
9961
|
+
if (merged.tag === "") merged.tag = null;
|
|
9962
|
+
const addrErr = validateAndNormalizeAddressRow(merged);
|
|
9963
|
+
if (addrErr) {
|
|
9964
|
+
return json({ error: addrErr }, { status: 400 });
|
|
9965
|
+
}
|
|
9966
|
+
for (const k of Object.keys(updatePayload)) {
|
|
9967
|
+
if (k in merged) {
|
|
9968
|
+
updatePayload[k] = merged[k];
|
|
9969
|
+
}
|
|
9970
|
+
}
|
|
9971
|
+
}
|
|
9377
9972
|
if (Object.keys(updatePayload).length > 0) {
|
|
9378
9973
|
sanitizeBodyForEntity(repo, updatePayload);
|
|
9379
9974
|
await repo.update(numericId, updatePayload);
|
|
@@ -10545,7 +11140,7 @@ function createCmsApiHandler(config) {
|
|
|
10545
11140
|
} : usersApi;
|
|
10546
11141
|
const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
|
|
10547
11142
|
const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
|
|
10548
|
-
const
|
|
11143
|
+
const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
|
|
10549
11144
|
const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
|
|
10550
11145
|
const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
|
|
10551
11146
|
dataSource,
|
|
@@ -10644,7 +11239,10 @@ function createCmsApiHandler(config) {
|
|
|
10644
11239
|
}
|
|
10645
11240
|
if (path.length === 2) {
|
|
10646
11241
|
if (path[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
|
|
10647
|
-
if (path[1] === "profile" &&
|
|
11242
|
+
if (path[1] === "profile" && profileHandlers) {
|
|
11243
|
+
if (m === "GET") return profileHandlers.GET(req);
|
|
11244
|
+
if (m === "PUT") return profileHandlers.PUT(req);
|
|
11245
|
+
}
|
|
10648
11246
|
const id = path[1];
|
|
10649
11247
|
if (m === "GET") return usersHandlers.getById(req, id);
|
|
10650
11248
|
if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);
|
|
@@ -11375,18 +11973,19 @@ function createStorefrontApiHandler(config) {
|
|
|
11375
11973
|
const contactOrErr = await getContactForAddresses();
|
|
11376
11974
|
if (contactOrErr instanceof Response) return contactOrErr;
|
|
11377
11975
|
const b = await req.json().catch(() => ({}));
|
|
11378
|
-
const
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
|
|
11386
|
-
|
|
11387
|
-
|
|
11388
|
-
|
|
11389
|
-
);
|
|
11976
|
+
const row = {
|
|
11977
|
+
contactId: contactOrErr.contactId,
|
|
11978
|
+
tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
|
|
11979
|
+
line1: typeof b.line1 === "string" ? b.line1 : "",
|
|
11980
|
+
line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
|
|
11981
|
+
city: typeof b.city === "string" ? b.city : "",
|
|
11982
|
+
state: typeof b.state === "string" ? b.state : "",
|
|
11983
|
+
postalCode: typeof b.postalCode === "string" ? b.postalCode : "",
|
|
11984
|
+
country: typeof b.country === "string" ? b.country : ""
|
|
11985
|
+
};
|
|
11986
|
+
const addrErr = validateAndNormalizeAddressRow(row);
|
|
11987
|
+
if (addrErr) return json({ error: addrErr }, { status: 400 });
|
|
11988
|
+
const created = await addressRepo().save(addressRepo().create(row));
|
|
11390
11989
|
return json(serializeAddress2(created));
|
|
11391
11990
|
}
|
|
11392
11991
|
if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
|
|
@@ -11405,7 +12004,16 @@ function createStorefrontApiHandler(config) {
|
|
|
11405
12004
|
if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
|
|
11406
12005
|
if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
|
|
11407
12006
|
if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
|
|
11408
|
-
if (Object.keys(updates).length)
|
|
12007
|
+
if (Object.keys(updates).length) {
|
|
12008
|
+
const merged = { ...existing, ...updates };
|
|
12009
|
+
if (merged.tag === "") merged.tag = null;
|
|
12010
|
+
const addrErr = validateAndNormalizeAddressRow(merged);
|
|
12011
|
+
if (addrErr) return json({ error: addrErr }, { status: 400 });
|
|
12012
|
+
for (const k of Object.keys(updates)) {
|
|
12013
|
+
if (k in merged) updates[k] = merged[k];
|
|
12014
|
+
}
|
|
12015
|
+
await addressRepo().update(id, updates);
|
|
12016
|
+
}
|
|
11409
12017
|
const updated = await addressRepo().findOne({ where: { id } });
|
|
11410
12018
|
return json(serializeAddress2(updated));
|
|
11411
12019
|
}
|