@infuro/cms-core 1.0.24 → 1.0.26

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 CHANGED
@@ -38,10 +38,52 @@ var __decorateClass = (decorators, target, key, kind) => {
38
38
  return result;
39
39
  };
40
40
 
41
+ // src/plugins/erp/erp-log.ts
42
+ function logErp(event, detail) {
43
+ if (detail && Object.keys(detail).length) console.info(ERP_LOG, event, detail);
44
+ else console.info(ERP_LOG, event);
45
+ }
46
+ function warnErp(event, detail) {
47
+ console.warn(ERP_LOG, event, detail);
48
+ }
49
+ function errorErp(event, detail) {
50
+ console.error(ERP_LOG, event, detail);
51
+ }
52
+ function erpSafeWebhookUrl(url) {
53
+ try {
54
+ const u = new URL(url);
55
+ return `${u.origin}${u.pathname}`;
56
+ } catch {
57
+ return "(invalid webhook URL)";
58
+ }
59
+ }
60
+ var ERP_LOG;
61
+ var init_erp_log = __esm({
62
+ "src/plugins/erp/erp-log.ts"() {
63
+ "use strict";
64
+ ERP_LOG = "[webcore:erp]";
65
+ }
66
+ });
67
+
41
68
  // src/plugins/erp/erp-queue.ts
69
+ function queuePayloadSummary(payload) {
70
+ if (payload.kind === "order") {
71
+ const o = payload.order;
72
+ return {
73
+ kind: payload.kind,
74
+ platformOrderId: o.platformOrderId ?? o.platformOrderNumber,
75
+ itemCount: Array.isArray(o.items) ? o.items.length : 0
76
+ };
77
+ }
78
+ return { kind: payload.kind };
79
+ }
42
80
  async function queueErp(cms, payload) {
43
81
  const queue = cms.getPlugin("queue");
44
- if (!queue) return;
82
+ if (!queue) {
83
+ warnErp("queue:add_skipped", { reason: "queue_plugin_missing", ...queuePayloadSummary(payload) });
84
+ return;
85
+ }
86
+ logErp("queue:add", { job: ERP_QUEUE_NAME, ...queuePayloadSummary(payload) });
45
87
  await queue.add(ERP_QUEUE_NAME, payload);
46
88
  }
47
89
  function registerErpQueueProcessor(cms) {
@@ -49,18 +91,31 @@ function registerErpQueueProcessor(cms) {
49
91
  if (!queue) return;
50
92
  queue.registerProcessor(ERP_QUEUE_NAME, async (data) => {
51
93
  const erp = cms.getPlugin("erp");
52
- if (!erp) return;
94
+ if (!erp) {
95
+ warnErp("queue:processor_skip", { reason: "erp_plugin_missing" });
96
+ return;
97
+ }
53
98
  const payload = data;
54
- if (payload.kind === "lead") {
55
- await erp.submission.submitContact(payload.contact);
56
- } else if (payload.kind === "formOpportunity") {
57
- await erp.submission.submitFormOpportunity(payload.contact);
58
- } else if (payload.kind === "createContact") {
59
- await erp.submission.submitCreateContact(payload.contact);
60
- } else if (payload.kind === "order") {
61
- await erp.submission.submitOrder(payload.order);
62
- } else if (payload.kind === "productUpsert") {
63
- await erp.submission.submitProductUpsert(payload.product);
99
+ logErp("queue:job_start", queuePayloadSummary(payload));
100
+ try {
101
+ if (payload.kind === "lead") {
102
+ await erp.submission.submitContact(payload.contact);
103
+ } else if (payload.kind === "formOpportunity") {
104
+ await erp.submission.submitFormOpportunity(payload.contact);
105
+ } else if (payload.kind === "createContact") {
106
+ await erp.submission.submitCreateContact(payload.contact);
107
+ } else if (payload.kind === "order") {
108
+ await erp.submission.submitOrder(payload.order);
109
+ } else if (payload.kind === "productUpsert") {
110
+ await erp.submission.submitProductUpsert(payload.product);
111
+ }
112
+ logErp("queue:job_done", queuePayloadSummary(payload));
113
+ } catch (e) {
114
+ errorErp("queue:job_failed", {
115
+ ...queuePayloadSummary(payload),
116
+ message: e instanceof Error ? e.message : String(e)
117
+ });
118
+ throw e;
64
119
  }
65
120
  });
66
121
  }
@@ -68,6 +123,7 @@ var ERP_QUEUE_NAME;
68
123
  var init_erp_queue = __esm({
69
124
  "src/plugins/erp/erp-queue.ts"() {
70
125
  "use strict";
126
+ init_erp_log();
71
127
  ERP_QUEUE_NAME = "erp";
72
128
  }
73
129
  });
@@ -119,21 +175,36 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
119
175
  const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
120
176
  for (const row of cfgRows) {
121
177
  const r = row;
122
- if (r.key === "enabled" && r.value === "false") return;
178
+ if (r.key === "enabled" && r.value === "false") {
179
+ logErp("paid-order:skip", { orderId, reason: "erp_config_disabled" });
180
+ return;
181
+ }
182
+ }
183
+ if (!cms.getPlugin("erp")) {
184
+ logErp("paid-order:skip", { orderId, reason: "erp_plugin_missing" });
185
+ return;
123
186
  }
124
- if (!cms.getPlugin("erp")) return;
125
187
  const orderRepo = dataSource.getRepository(entityMap.orders);
126
188
  const ord = await orderRepo.findOne({
127
189
  where: { id: orderId },
128
190
  relations: ["items", "items.product", "contact", "billingAddress", "shippingAddress", "payments"]
129
191
  });
130
- if (!ord) return;
192
+ if (!ord) {
193
+ logErp("paid-order:skip", { orderId, reason: "order_not_found" });
194
+ return;
195
+ }
131
196
  const o = ord;
132
197
  const okKind = o.orderKind === void 0 || o.orderKind === null || o.orderKind === "sale";
133
- if (!okKind) return;
198
+ if (!okKind) {
199
+ logErp("paid-order:skip", { orderId, reason: "order_kind_not_sale", orderKind: o.orderKind });
200
+ return;
201
+ }
134
202
  const rawPayments = o.payments ?? [];
135
203
  const completedPayments = rawPayments.filter((pay) => pay.status === "completed" && pay.deleted !== true);
136
- if (!completedPayments.length) return;
204
+ if (!completedPayments.length) {
205
+ logErp("paid-order:skip", { orderId, reason: "no_completed_payments" });
206
+ return;
207
+ }
137
208
  const rawItems = o.items ?? [];
138
209
  const lines = rawItems.filter((it) => it.product).map((it) => {
139
210
  const p = it.product;
@@ -152,7 +223,10 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
152
223
  type: itemType
153
224
  };
154
225
  });
155
- if (!lines.length) return;
226
+ if (!lines.length) {
227
+ logErp("paid-order:skip", { orderId, reason: "no_line_items_with_product" });
228
+ return;
229
+ }
156
230
  const contact = o.contact;
157
231
  const orderTotalMajor = Number(o.total);
158
232
  const paymentDtos = completedPayments.length === 1 && Number.isFinite(orderTotalMajor) ? [paymentRowToWebhookDto(completedPayments[0], orderTotalMajor)] : completedPayments.map((pay) => paymentRowToWebhookDto(pay));
@@ -174,13 +248,28 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
174
248
  payments: paymentDtos,
175
249
  metadata: { ...baseMeta, source: "storefront" }
176
250
  };
251
+ logErp("paid-order:payload_built", {
252
+ orderId,
253
+ platformOrderId: orderDto.platformOrderId,
254
+ status: orderDto.status,
255
+ itemCount: lines.length,
256
+ skus: lines.map((l) => l.sku),
257
+ paymentCount: paymentDtos.length,
258
+ paymentIds: paymentDtos.map((p) => p.id),
259
+ total: orderTotalMajor
260
+ });
177
261
  await queueErp(cms, { kind: "order", order: orderDto });
178
- } catch {
262
+ } catch (e) {
263
+ errorErp("paid-order:enqueue_failed", {
264
+ orderId,
265
+ message: e instanceof Error ? e.message : String(e)
266
+ });
179
267
  }
180
268
  }
181
269
  var init_paid_order_erp = __esm({
182
270
  "src/plugins/erp/paid-order-erp.ts"() {
183
271
  "use strict";
272
+ init_erp_log();
184
273
  init_erp_queue();
185
274
  }
186
275
  });
@@ -816,6 +905,7 @@ async function createCmsApp(options) {
816
905
  }
817
906
 
818
907
  // src/plugins/erp/erp-submission.ts
908
+ init_erp_log();
819
909
  var ERPSubmissionService = class {
820
910
  webhookUrl;
821
911
  webhookJwt;
@@ -895,7 +985,29 @@ var ERPSubmissionService = class {
895
985
  };
896
986
  return this.postWebhookJson(envelope);
897
987
  }
988
+ summarizeWebhookBody(body) {
989
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
990
+ return { bodyKind: typeof body };
991
+ }
992
+ const o = body;
993
+ const out = { event_type: o.event_type, timestamp: o.timestamp };
994
+ const data = o.data;
995
+ if (data && typeof data === "object" && !Array.isArray(data)) {
996
+ const d = data;
997
+ out.dataKeys = Object.keys(d);
998
+ out.platformOrderId = d.platformOrderId ?? d.platformOrderNumber;
999
+ out.itemCount = Array.isArray(d.items) ? d.items.length : void 0;
1000
+ }
1001
+ return out;
1002
+ }
898
1003
  async postWebhookJson(body) {
1004
+ const safeUrl = erpSafeWebhookUrl(this.webhookUrl);
1005
+ const bodyJson = JSON.stringify(body);
1006
+ logErp("webhook:post_start", {
1007
+ url: safeUrl,
1008
+ bodyBytes: bodyJson.length,
1009
+ ...this.summarizeWebhookBody(body)
1010
+ });
899
1011
  try {
900
1012
  const res = await fetch(this.webhookUrl, {
901
1013
  method: "POST",
@@ -903,13 +1015,19 @@ var ERPSubmissionService = class {
903
1015
  "Content-Type": "application/json",
904
1016
  "X-External-Token": this.webhookJwt
905
1017
  },
906
- body: JSON.stringify(body)
1018
+ body: bodyJson
907
1019
  });
908
- if (res.ok) return { success: true, status: res.status };
909
1020
  const text = await res.text();
1021
+ const preview = text.length > 500 ? `${text.slice(0, 500)}\u2026` : text;
1022
+ if (res.ok) {
1023
+ logErp("webhook:post_ok", { status: res.status, responsePreview: preview || "(empty body)" });
1024
+ return { success: true, status: res.status };
1025
+ }
1026
+ warnErp("webhook:post_http_error", { status: res.status, responsePreview: preview });
910
1027
  return { success: false, error: `${res.status} ${text.slice(0, 500)}`, status: res.status };
911
1028
  } catch (e) {
912
1029
  const message = e instanceof Error ? e.message : "ERP webhook request failed";
1030
+ errorErp("webhook:post_fetch_failed", { url: safeUrl, message });
913
1031
  return { success: false, error: message };
914
1032
  }
915
1033
  }
@@ -1023,7 +1141,20 @@ var ERPSubmissionService = class {
1023
1141
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1024
1142
  data: orderDto
1025
1143
  };
1026
- return this.postWebhookJson(envelope);
1144
+ logErp("submitOrder:envelope_ready", {
1145
+ event_type: envelope.event_type,
1146
+ timestamp: envelope.timestamp,
1147
+ platformOrderId: orderDto.platformOrderId ?? orderDto.platformOrderNumber,
1148
+ itemCount: Array.isArray(orderDto.items) ? orderDto.items.length : 0,
1149
+ paymentCount: Array.isArray(orderDto.payments) ? orderDto.payments.length : 0
1150
+ });
1151
+ const result = await this.postWebhookJson(envelope);
1152
+ if (result.success) {
1153
+ logErp("submitOrder:complete", { ok: true, status: result.status });
1154
+ } else {
1155
+ warnErp("submitOrder:complete", { ok: false, status: result.status, error: result.error });
1156
+ }
1157
+ return result;
1027
1158
  }
1028
1159
  extractContactData(formData, formFields) {
1029
1160
  const contactData = {
@@ -5184,18 +5315,37 @@ function createUsersApiHandlers(config) {
5184
5315
  try {
5185
5316
  const body = await req.json();
5186
5317
  if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
5187
- const existing = await userRepo().findOne({ where: { email: body.email } });
5188
- if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
5318
+ const email = body.email;
5319
+ const existing = await userRepo().findOne({ where: { email } });
5320
+ if (existing && !existing.deleted) {
5321
+ return json({ error: "User with this email already exists" }, { status: 400 });
5322
+ }
5189
5323
  const groupRepo = dataSource.getRepository(entityMap.user_groups);
5190
5324
  const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
5191
5325
  const gid = body.groupId ?? null;
5192
5326
  const isCustomer = !!(customerG && gid === customerG.id);
5193
5327
  const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
5194
5328
  const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
5195
- const newUser = await userRepo().save(
5329
+ const newUser = existing?.deleted ? await (async () => {
5330
+ await userRepo().update(existing.id, {
5331
+ deleted: false,
5332
+ deletedAt: null,
5333
+ deletedBy: null,
5334
+ name: body.name,
5335
+ email,
5336
+ password: null,
5337
+ blocked,
5338
+ groupId: gid,
5339
+ adminAccess,
5340
+ updatedAt: /* @__PURE__ */ new Date()
5341
+ });
5342
+ const row = await userRepo().findOne({ where: { id: existing.id } });
5343
+ if (!row) throw new Error("user missing after restore");
5344
+ return row;
5345
+ })() : await userRepo().save(
5196
5346
  userRepo().create({
5197
5347
  name: body.name,
5198
- email: body.email,
5348
+ email,
5199
5349
  password: null,
5200
5350
  blocked,
5201
5351
  groupId: gid,
@@ -5348,21 +5498,110 @@ function createUserAvatarHandler(config) {
5348
5498
  }
5349
5499
  };
5350
5500
  }
5501
+ var PROFILE_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5351
5502
  function createUserProfileHandler(config) {
5352
- const { dataSource, entityMap, json, getSession } = config;
5353
- return async function PUT(req) {
5503
+ const { dataSource, entityMap, json, getSession, onProfileUpdated } = config;
5504
+ async function loadCurrentUser() {
5354
5505
  const session = await getSession();
5355
- if (!session?.user?.email) return json({ error: "Unauthorized" }, { status: 401 });
5356
- try {
5357
- const body = await req.json();
5358
- if (!body?.name) return json({ error: "Name is required" }, { status: 400 });
5359
- const userRepo = dataSource.getRepository(entityMap.users);
5360
- await userRepo.update({ email: session.user.email }, { name: body.name, updatedAt: /* @__PURE__ */ new Date() });
5361
- const updated = await userRepo.findOne({ where: { email: session.user.email }, select: ["id", "name", "email"] });
5362
- if (!updated) return json({ error: "Not found" }, { status: 404 });
5363
- return json({ message: "Profile updated successfully", user: { id: updated.id, name: updated.name, email: updated.email } });
5364
- } catch {
5365
- return json({ error: "Internal server error" }, { status: 500 });
5506
+ const su = session?.user;
5507
+ if (!su?.email && su?.id == null) {
5508
+ return { ok: false, response: json({ error: "Unauthorized" }, { status: 401 }) };
5509
+ }
5510
+ const userRepo = dataSource.getRepository(entityMap.users);
5511
+ let user = null;
5512
+ const uidRaw = su.id != null ? String(su.id).trim() : "";
5513
+ const uid = uidRaw && /^\d+$/.test(uidRaw) ? parseInt(uidRaw, 10) : NaN;
5514
+ if (Number.isFinite(uid) && uid > 0) {
5515
+ user = await userRepo.findOne({
5516
+ where: { id: uid, deleted: false },
5517
+ select: ["id", "name", "email", "phone", "createdAt"]
5518
+ });
5519
+ }
5520
+ if (!user && su.email) {
5521
+ const em = String(su.email).trim().toLowerCase();
5522
+ if (em) {
5523
+ user = await userRepo.findOne({
5524
+ where: { email: em, deleted: false },
5525
+ select: ["id", "name", "email", "phone", "createdAt"]
5526
+ });
5527
+ }
5528
+ }
5529
+ if (!user) return { ok: false, response: json({ error: "Not found" }, { status: 404 }) };
5530
+ return { ok: true, user };
5531
+ }
5532
+ return {
5533
+ async GET(_req) {
5534
+ try {
5535
+ const r = await loadCurrentUser();
5536
+ if (!r.ok) return r.response;
5537
+ const u = r.user;
5538
+ return json({
5539
+ id: u.id,
5540
+ name: u.name ?? "",
5541
+ email: u.email ?? "",
5542
+ phone: u.phone ?? null,
5543
+ createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : u.createdAt ?? void 0
5544
+ });
5545
+ } catch {
5546
+ return json({ error: "Internal server error" }, { status: 500 });
5547
+ }
5548
+ },
5549
+ async PUT(req) {
5550
+ try {
5551
+ const r = await loadCurrentUser();
5552
+ if (!r.ok) return r.response;
5553
+ const current = r.user;
5554
+ let body;
5555
+ try {
5556
+ body = await req.json();
5557
+ } catch {
5558
+ return json({ error: "Invalid JSON" }, { status: 400 });
5559
+ }
5560
+ const name = typeof body.name === "string" ? body.name.trim() : "";
5561
+ if (!name) return json({ error: "Name is required" }, { status: 400 });
5562
+ const emailRaw = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
5563
+ if (!emailRaw || !PROFILE_EMAIL_RE.test(emailRaw)) {
5564
+ return json({ error: "Valid email is required" }, { status: 400 });
5565
+ }
5566
+ const phone = body.phone === null || body.phone === void 0 ? null : typeof body.phone === "string" ? body.phone.trim() || null : null;
5567
+ const userRepo = dataSource.getRepository(entityMap.users);
5568
+ if (emailRaw !== String(current.email ?? "").toLowerCase()) {
5569
+ const taken = await userRepo.findOne({
5570
+ where: { email: emailRaw, deleted: false },
5571
+ select: ["id"]
5572
+ });
5573
+ if (taken && taken.id !== current.id) {
5574
+ return json({ error: "Email is already in use" }, { status: 409 });
5575
+ }
5576
+ }
5577
+ await userRepo.update(
5578
+ { id: current.id },
5579
+ {
5580
+ name,
5581
+ email: emailRaw,
5582
+ phone,
5583
+ updatedAt: /* @__PURE__ */ new Date()
5584
+ }
5585
+ );
5586
+ const updated = await userRepo.findOne({
5587
+ where: { id: current.id },
5588
+ select: ["id", "name", "email", "phone"]
5589
+ });
5590
+ if (!updated) return json({ error: "Not found" }, { status: 404 });
5591
+ const row = updated;
5592
+ if (onProfileUpdated) {
5593
+ try {
5594
+ await onProfileUpdated(req, row);
5595
+ } catch {
5596
+ }
5597
+ }
5598
+ return json({
5599
+ message: "Profile updated successfully",
5600
+ user: { id: row.id, name: row.name, email: row.email, phone: row.phone }
5601
+ });
5602
+ } catch {
5603
+ return json({ error: "Internal server error" }, { status: 500 });
5604
+ }
5366
5605
  }
5367
5606
  };
5368
5607
  }
@@ -8687,7 +8926,7 @@ function getNextAuthOptions(config) {
8687
8926
  }
8688
8927
  },
8689
8928
  callbacks: {
8690
- async jwt({ token, user }) {
8929
+ async jwt({ token, user, trigger, session }) {
8691
8930
  if (user) {
8692
8931
  const u = user;
8693
8932
  token.id = u.id;
@@ -8696,11 +8935,19 @@ function getNextAuthOptions(config) {
8696
8935
  token.entityPerms = u.entityPerms;
8697
8936
  token.adminAccess = u.adminAccess;
8698
8937
  }
8938
+ if (trigger === "update" && session && typeof session === "object") {
8939
+ const s = session;
8940
+ const t = token;
8941
+ if (typeof s.name === "string") t.name = s.name;
8942
+ if (typeof s.email === "string") t.email = s.email;
8943
+ }
8699
8944
  return token;
8700
8945
  },
8701
8946
  async session({ session, token }) {
8702
8947
  if (session.user) {
8703
8948
  const t = token;
8949
+ if (typeof t.name === "string") session.user.name = t.name;
8950
+ if (typeof t.email === "string") session.user.email = t.email;
8704
8951
  session.user.id = t.id;
8705
8952
  session.user.groupId = t.groupId;
8706
8953
  session.user.isRBACAdmin = t.isRBACAdmin;
@@ -8872,6 +9119,156 @@ function buildSearchWhereClause(repo, search) {
8872
9119
  function entityHasSoftDelete(repo) {
8873
9120
  return repo.metadata.columns.some((c) => c.propertyName === "deleted");
8874
9121
  }
9122
+ var LIST_QUERY_RESERVED = /* @__PURE__ */ new Set(["page", "limit", "sortField", "sortOrder", "search"]);
9123
+ function dayStartUtc(isoDay) {
9124
+ return /* @__PURE__ */ new Date(isoDay + "T00:00:00.000Z");
9125
+ }
9126
+ function dayEndUtc(isoDay) {
9127
+ return /* @__PURE__ */ new Date(isoDay + "T23:59:59.999Z");
9128
+ }
9129
+ function columnTypeLabel(col) {
9130
+ const t = col.type;
9131
+ if (typeof t === "string") return t.toLowerCase();
9132
+ if (typeof t === "function") return t.name?.toLowerCase?.() ?? "";
9133
+ if (t && typeof t === "object" && "name" in t && typeof t.name === "string") {
9134
+ return String(t.name).toLowerCase();
9135
+ }
9136
+ return "";
9137
+ }
9138
+ function isListDateColumn(col) {
9139
+ const tl = columnTypeLabel(col);
9140
+ return DATE_COLUMN_TYPES.has(tl) || col.type === Date || TIMESTAMP_PROP_NAMES.has(col.propertyName);
9141
+ }
9142
+ function isListNumericColumn(col) {
9143
+ const tl = columnTypeLabel(col);
9144
+ if (col.type === Number) return true;
9145
+ const patterns = [
9146
+ "int",
9147
+ "integer",
9148
+ "int2",
9149
+ "int4",
9150
+ "int8",
9151
+ "smallint",
9152
+ "bigint",
9153
+ "float",
9154
+ "double",
9155
+ "decimal",
9156
+ "numeric",
9157
+ "real",
9158
+ "tinyint",
9159
+ "mediumint"
9160
+ ];
9161
+ if (patterns.some((x) => tl.includes(x))) return true;
9162
+ if (tl.includes("unsigned") && (tl.includes("int") || tl.includes("bigint") || tl.includes("small"))) return true;
9163
+ if (!tl && /Id$/i.test(col.propertyName) && !TIMESTAMP_PROP_NAMES.has(col.propertyName)) return true;
9164
+ return false;
9165
+ }
9166
+ function isListBooleanColumn(col) {
9167
+ const tl = columnTypeLabel(col);
9168
+ return tl === "boolean" || tl === "bool" || col.type === Boolean;
9169
+ }
9170
+ function isListStringColumn(col) {
9171
+ const tl = columnTypeLabel(col);
9172
+ if (isListDateColumn(col) || isListNumericColumn(col) || isListBooleanColumn(col)) return false;
9173
+ if (["varchar", "character varying", "text", "citext", "uuid", "char", "character", "enum"].some(
9174
+ (x) => tl.includes(x)
9175
+ )) {
9176
+ return true;
9177
+ }
9178
+ if (col.type === String) return true;
9179
+ return false;
9180
+ }
9181
+ function mergeListWhereAnd(where, patch) {
9182
+ if (Object.keys(patch).length === 0) return where;
9183
+ if (Array.isArray(where)) {
9184
+ if (where.length === 0) return [patch];
9185
+ return where.map((w) => ({ ...w, ...patch }));
9186
+ }
9187
+ if (where && typeof where === "object" && Object.keys(where).length > 0) {
9188
+ return { ...where, ...patch };
9189
+ }
9190
+ return patch;
9191
+ }
9192
+ function buildListFilterAndFromSearchParams(repo, searchParams) {
9193
+ const and = {};
9194
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
9195
+ for (const col of repo.metadata.columns) {
9196
+ const name = col.propertyName;
9197
+ if (!columnNames.has(name)) continue;
9198
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9199
+ if (!isListDateColumn(col)) continue;
9200
+ const from = searchParams.get(`${name}From`)?.trim();
9201
+ const to = searchParams.get(`${name}To`)?.trim();
9202
+ if (!from && !to) continue;
9203
+ if (from && to) {
9204
+ and[name] = (0, import_typeorm46.Between)(dayStartUtc(from), dayEndUtc(to));
9205
+ } else if (from) {
9206
+ and[name] = (0, import_typeorm46.MoreThanOrEqual)(dayStartUtc(from));
9207
+ } else if (to) {
9208
+ and[name] = (0, import_typeorm46.LessThanOrEqual)(dayEndUtc(to));
9209
+ }
9210
+ }
9211
+ for (const col of repo.metadata.columns) {
9212
+ const name = col.propertyName;
9213
+ if (!columnNames.has(name)) continue;
9214
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9215
+ if (!isListNumericColumn(col)) continue;
9216
+ if (Object.prototype.hasOwnProperty.call(and, name)) continue;
9217
+ const minRaw = searchParams.get(`${name}Min`)?.trim();
9218
+ const maxRaw = searchParams.get(`${name}Max`)?.trim();
9219
+ if (!minRaw && !maxRaw) continue;
9220
+ const parseNum = (s) => {
9221
+ const n = Number(s);
9222
+ return Number.isFinite(n) ? n : null;
9223
+ };
9224
+ const nMin = minRaw ? parseNum(minRaw) : null;
9225
+ const nMax = maxRaw ? parseNum(maxRaw) : null;
9226
+ if (nMin != null && nMax != null) {
9227
+ and[name] = (0, import_typeorm46.Between)(nMin, nMax);
9228
+ } else if (nMin != null) {
9229
+ and[name] = (0, import_typeorm46.MoreThanOrEqual)(nMin);
9230
+ } else if (nMax != null) {
9231
+ and[name] = (0, import_typeorm46.LessThanOrEqual)(nMax);
9232
+ }
9233
+ }
9234
+ for (const col of repo.metadata.columns) {
9235
+ const name = col.propertyName;
9236
+ if (!columnNames.has(name)) continue;
9237
+ if (LIST_QUERY_RESERVED.has(name)) continue;
9238
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9239
+ if (!isListStringColumn(col)) continue;
9240
+ if (Object.prototype.hasOwnProperty.call(and, name)) continue;
9241
+ const raw = searchParams.get(name)?.trim();
9242
+ if (!raw) continue;
9243
+ and[name] = (0, import_typeorm46.ILike)(`%${raw}%`);
9244
+ }
9245
+ return and;
9246
+ }
9247
+ function buildExactListParamWhere(repo, searchParams) {
9248
+ const extraWhere = {};
9249
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
9250
+ for (const col of repo.metadata.columns) {
9251
+ const name = col.propertyName;
9252
+ if (!columnNames.has(name)) continue;
9253
+ if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
9254
+ if (!isListNumericColumn(col)) continue;
9255
+ const v = searchParams.get(name)?.trim();
9256
+ if (v == null || v === "") continue;
9257
+ const n = Number(v);
9258
+ if (!Number.isFinite(n)) continue;
9259
+ extraWhere[name] = n;
9260
+ }
9261
+ for (const col of repo.metadata.columns) {
9262
+ if (String(col.type) !== "boolean") continue;
9263
+ const name = col.propertyName;
9264
+ if (!columnNames.has(name)) continue;
9265
+ const raw = searchParams.get(name)?.trim();
9266
+ if (raw === "true" || raw === "false") {
9267
+ extraWhere[name] = raw === "true";
9268
+ }
9269
+ }
9270
+ return extraWhere;
9271
+ }
8875
9272
  function mergeDeletedFalseWhere(repo, where) {
8876
9273
  if (!entityHasSoftDelete(repo)) return where;
8877
9274
  const d = { deleted: false };
@@ -8881,6 +9278,12 @@ function mergeDeletedFalseWhere(repo, where) {
8881
9278
  }
8882
9279
  return Object.keys(where).length > 0 ? { ...where, ...d } : d;
8883
9280
  }
9281
+ function pickDefaultListSortField(columnNames, columns) {
9282
+ for (const candidate of ["createdAt", "updatedAt", "id", "name", "sortOrder", "title"]) {
9283
+ if (columnNames.has(candidate)) return candidate;
9284
+ }
9285
+ return columns[0]?.propertyName ?? "id";
9286
+ }
8884
9287
  function normalizeProductSku(value) {
8885
9288
  if (value == null) return null;
8886
9289
  const s = String(value).trim();
@@ -8983,6 +9386,18 @@ function createCrudHandler(dataSource, entityMap, options) {
8983
9386
  if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
8984
9387
  if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
8985
9388
  if (dateTo) qb.andWhere("order.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
9389
+ const totalMin = searchParams.get("totalMin")?.trim();
9390
+ const totalMax = searchParams.get("totalMax")?.trim();
9391
+ if (totalMin) {
9392
+ const n = Number(totalMin);
9393
+ if (Number.isFinite(n)) qb.andWhere("order.total >= :totalMin", { totalMin: n });
9394
+ }
9395
+ if (totalMax) {
9396
+ const n = Number(totalMax);
9397
+ if (Number.isFinite(n)) qb.andWhere("order.total <= :totalMax", { totalMax: n });
9398
+ }
9399
+ const currency = searchParams.get("currency")?.trim();
9400
+ if (currency) qb.andWhere("order.currency ILIKE :orderCurrency", { orderCurrency: `%${currency}%` });
8986
9401
  if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
8987
9402
  const [rows, total2] = await qb.getManyAndCount();
8988
9403
  const data2 = rows.map((order) => {
@@ -9021,8 +9436,30 @@ function createCrudHandler(dataSource, entityMap, options) {
9021
9436
  if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
9022
9437
  if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
9023
9438
  if (dateTo) qb.andWhere("payment.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
9439
+ const paidAtFrom = searchParams.get("paidAtFrom")?.trim();
9440
+ const paidAtTo = searchParams.get("paidAtTo")?.trim();
9441
+ if (paidAtFrom) {
9442
+ qb.andWhere("payment.paidAt >= :paidAtFrom", { paidAtFrom: /* @__PURE__ */ new Date(paidAtFrom + "T00:00:00.000Z") });
9443
+ }
9444
+ if (paidAtTo) {
9445
+ qb.andWhere("payment.paidAt <= :paidAtTo", { paidAtTo: /* @__PURE__ */ new Date(paidAtTo + "T23:59:59.999Z") });
9446
+ }
9024
9447
  if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
9025
9448
  if (orderNumberParam) qb.andWhere("ord.orderNumber ILIKE :orderNumber", { orderNumber: `%${orderNumberParam}%` });
9449
+ const amountMin = searchParams.get("amountMin")?.trim();
9450
+ const amountMax = searchParams.get("amountMax")?.trim();
9451
+ if (amountMin) {
9452
+ const n = Number(amountMin);
9453
+ if (Number.isFinite(n)) qb.andWhere("payment.amount >= :amountMin", { amountMin: n });
9454
+ }
9455
+ if (amountMax) {
9456
+ const n = Number(amountMax);
9457
+ if (Number.isFinite(n)) qb.andWhere("payment.amount <= :amountMax", { amountMax: n });
9458
+ }
9459
+ const extRef = searchParams.get("externalReference")?.trim();
9460
+ if (extRef) {
9461
+ qb.andWhere("payment.externalReference ILIKE :extRef", { extRef: `%${extRef}%` });
9462
+ }
9026
9463
  const [rows, total2] = await qb.getManyAndCount();
9027
9464
  const data2 = rows.map((payment) => {
9028
9465
  const order = payment.order;
@@ -9041,7 +9478,10 @@ function createCrudHandler(dataSource, entityMap, options) {
9041
9478
  const repo2 = dataSource.getRepository(entity);
9042
9479
  const statusFilter = searchParams.get("status")?.trim();
9043
9480
  const inventory = searchParams.get("inventory")?.trim();
9044
- const productWhere = { deleted: false };
9481
+ const productWhere = {
9482
+ deleted: false,
9483
+ ...buildListFilterAndFromSearchParams(repo2, searchParams)
9484
+ };
9045
9485
  if (statusFilter) productWhere.status = statusFilter;
9046
9486
  if (inventory === "in_stock") productWhere.quantity = (0, import_typeorm46.MoreThan)(0);
9047
9487
  if (inventory === "out_of_stock") productWhere.quantity = 0;
@@ -9059,11 +9499,15 @@ function createCrudHandler(dataSource, entityMap, options) {
9059
9499
  if (search && typeof search === "string" && search.trim()) {
9060
9500
  productWhere.name = (0, import_typeorm46.ILike)(`%${search.trim()}%`);
9061
9501
  }
9502
+ const productColumnNames = new Set(repo2.metadata.columns.map((c) => c.propertyName));
9503
+ const defaultProductSort = pickDefaultListSortField(productColumnNames, repo2.metadata.columns);
9504
+ const sortParam2 = (searchParams.get("sortField") ?? "").trim();
9505
+ const productSortField = sortParam2 && productColumnNames.has(sortParam2) ? sortParam2 : defaultProductSort;
9062
9506
  const [data2, total2] = await repo2.findAndCount({
9063
9507
  where: Object.keys(productWhere).length ? productWhere : void 0,
9064
9508
  skip,
9065
9509
  take: limit,
9066
- order: { [sortFieldRaw]: sortOrder }
9510
+ order: { [productSortField]: sortOrder }
9067
9511
  });
9068
9512
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
9069
9513
  }
@@ -9156,36 +9600,22 @@ function createCrudHandler(dataSource, entityMap, options) {
9156
9600
  }
9157
9601
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
9158
9602
  }
9159
- const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
9603
+ const defaultSortField = pickDefaultListSortField(columnNames, repo.metadata.columns);
9604
+ const sortParam = (searchParams.get("sortField") ?? "").trim();
9605
+ const sortField = sortParam && columnNames.has(sortParam) ? sortParam : defaultSortField;
9160
9606
  let where = {};
9161
9607
  if (search) {
9162
9608
  where = buildSearchWhereClause(repo, search);
9163
9609
  }
9164
- const intFilterKeys = ["productId", "attributeId", "taxId", "brandId", "categoryId", "collectionId", "parentId"];
9165
- const extraWhere = {};
9166
- for (const key of intFilterKeys) {
9167
- const v = searchParams.get(key);
9168
- if (v != null && v !== "" && columnNames.has(key)) {
9169
- const n = Number(v);
9170
- if (Number.isFinite(n)) extraWhere[key] = n;
9171
- }
9172
- }
9173
- for (const col of repo.metadata.columns) {
9174
- if (String(col.type) !== "boolean") continue;
9175
- const name = col.propertyName;
9176
- if (!columnNames.has(name)) continue;
9177
- const raw = searchParams.get(name)?.trim();
9178
- if (raw === "true" || raw === "false") {
9179
- extraWhere[name] = raw === "true";
9180
- }
9181
- }
9182
- if (Object.keys(extraWhere).length > 0) {
9610
+ where = mergeListWhereAnd(where, buildListFilterAndFromSearchParams(repo, searchParams));
9611
+ const exactParamWhere = buildExactListParamWhere(repo, searchParams);
9612
+ if (Object.keys(exactParamWhere).length > 0) {
9183
9613
  if (Array.isArray(where)) {
9184
- where = where.map((w) => ({ ...w, ...extraWhere }));
9614
+ where = where.map((w) => ({ ...w, ...exactParamWhere }));
9185
9615
  } else if (where && typeof where === "object" && Object.keys(where).length > 0) {
9186
- where = { ...where, ...extraWhere };
9616
+ where = { ...where, ...exactParamWhere };
9187
9617
  } else {
9188
- where = extraWhere;
9618
+ where = exactParamWhere;
9189
9619
  }
9190
9620
  }
9191
9621
  where = mergeDeletedFalseWhere(repo, where);
@@ -9265,6 +9695,10 @@ function createCrudHandler(dataSource, entityMap, options) {
9265
9695
  }
9266
9696
  const repo = dataSource.getRepository(entity);
9267
9697
  const persistBody = resource === "media" ? body : pickColumnUpdates(repo, body);
9698
+ if (resource === "contacts" && "type" in persistBody) {
9699
+ const t = persistBody.type;
9700
+ if (t === "" || t === "none" || t == null) persistBody.type = null;
9701
+ }
9268
9702
  if (resource !== "media" && Object.keys(persistBody).length === 0) {
9269
9703
  logCrudClientError("POST create", {
9270
9704
  reason: "no_scalar_columns_after_pick",
@@ -9615,6 +10049,10 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
9615
10049
  if (!cur) return json({ message: "Not found" }, { status: 404 });
9616
10050
  }
9617
10051
  const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
10052
+ if (resource === "contacts" && "type" in updatePayload) {
10053
+ const t = updatePayload.type;
10054
+ if (t === "" || t === "none" || t == null) updatePayload.type = null;
10055
+ }
9618
10056
  if (resource === "media") {
9619
10057
  const u = updatePayload;
9620
10058
  delete u.kind;
@@ -10874,7 +11312,7 @@ function createCmsApiHandler(config) {
10874
11312
  } : usersApi;
10875
11313
  const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
10876
11314
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
10877
- const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
11315
+ const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
10878
11316
  const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
10879
11317
  const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
10880
11318
  dataSource,
@@ -10973,7 +11411,10 @@ function createCmsApiHandler(config) {
10973
11411
  }
10974
11412
  if (path.length === 2) {
10975
11413
  if (path[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
10976
- if (path[1] === "profile" && m === "PUT" && profilePut) return profilePut(req);
11414
+ if (path[1] === "profile" && profileHandlers) {
11415
+ if (m === "GET") return profileHandlers.GET(req);
11416
+ if (m === "PUT") return profileHandlers.PUT(req);
11417
+ }
10977
11418
  const id = path[1];
10978
11419
  if (m === "GET") return usersHandlers.getById(req, id);
10979
11420
  if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);