@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.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) return;
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) return;
73
+ if (!erp) {
74
+ warnErp("queue:processor_skip", { reason: "erp_plugin_missing" });
75
+ return;
76
+ }
32
77
  const payload = data;
33
- if (payload.kind === "lead") {
34
- await erp.submission.submitContact(payload.contact);
35
- } else if (payload.kind === "formOpportunity") {
36
- await erp.submission.submitFormOpportunity(payload.contact);
37
- } else if (payload.kind === "createContact") {
38
- await erp.submission.submitCreateContact(payload.contact);
39
- } else if (payload.kind === "order") {
40
- await erp.submission.submitOrder(payload.order);
41
- } else if (payload.kind === "productUpsert") {
42
- await erp.submission.submitProductUpsert(payload.product);
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") return;
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) return;
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) return;
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) return;
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) return;
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: JSON.stringify(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
- return this.postWebhookJson(envelope);
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 = {
@@ -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 existing = await userRepo().findOne({ where: { email: body.email } });
5007
- if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
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 userRepo().save(
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: body.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
- return async function PUT(req) {
5322
+ const { dataSource, entityMap, json, getSession, onProfileUpdated } = config;
5323
+ async function loadCurrentUser() {
5173
5324
  const session = await getSession();
5174
- if (!session?.user?.email) return json({ error: "Unauthorized" }, { status: 401 });
5175
- try {
5176
- const body = await req.json();
5177
- if (!body?.name) return json({ error: "Name is required" }, { status: 400 });
5178
- const userRepo = dataSource.getRepository(entityMap.users);
5179
- await userRepo.update({ email: session.user.email }, { name: body.name, updatedAt: /* @__PURE__ */ new Date() });
5180
- const updated = await userRepo.findOne({ where: { email: session.user.email }, select: ["id", "name", "email"] });
5181
- if (!updated) return json({ error: "Not found" }, { status: 404 });
5182
- return json({ message: "Profile updated successfully", user: { id: updated.id, name: updated.name, email: updated.email } });
5183
- } catch {
5184
- return json({ error: "Internal server error" }, { status: 500 });
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,7 @@ 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";
8547
8794
 
8548
8795
  // src/lib/address-geo-validation.ts
8549
8796
  import { Country, State, City } from "country-state-city";
@@ -8700,6 +8947,156 @@ function buildSearchWhereClause(repo, search) {
8700
8947
  function entityHasSoftDelete(repo) {
8701
8948
  return repo.metadata.columns.some((c) => c.propertyName === "deleted");
8702
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
+ }
8703
9100
  function mergeDeletedFalseWhere(repo, where) {
8704
9101
  if (!entityHasSoftDelete(repo)) return where;
8705
9102
  const d = { deleted: false };
@@ -8709,6 +9106,12 @@ function mergeDeletedFalseWhere(repo, where) {
8709
9106
  }
8710
9107
  return Object.keys(where).length > 0 ? { ...where, ...d } : d;
8711
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
+ }
8712
9115
  function normalizeProductSku(value) {
8713
9116
  if (value == null) return null;
8714
9117
  const s = String(value).trim();
@@ -8811,6 +9214,18 @@ function createCrudHandler(dataSource, entityMap, options) {
8811
9214
  if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
8812
9215
  if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
8813
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}%` });
8814
9229
  if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
8815
9230
  const [rows, total2] = await qb.getManyAndCount();
8816
9231
  const data2 = rows.map((order) => {
@@ -8849,8 +9264,30 @@ function createCrudHandler(dataSource, entityMap, options) {
8849
9264
  if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
8850
9265
  if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
8851
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
+ }
8852
9275
  if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
8853
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
+ }
8854
9291
  const [rows, total2] = await qb.getManyAndCount();
8855
9292
  const data2 = rows.map((payment) => {
8856
9293
  const order = payment.order;
@@ -8869,7 +9306,10 @@ function createCrudHandler(dataSource, entityMap, options) {
8869
9306
  const repo2 = dataSource.getRepository(entity);
8870
9307
  const statusFilter = searchParams.get("status")?.trim();
8871
9308
  const inventory = searchParams.get("inventory")?.trim();
8872
- const productWhere = { deleted: false };
9309
+ const productWhere = {
9310
+ deleted: false,
9311
+ ...buildListFilterAndFromSearchParams(repo2, searchParams)
9312
+ };
8873
9313
  if (statusFilter) productWhere.status = statusFilter;
8874
9314
  if (inventory === "in_stock") productWhere.quantity = MoreThan2(0);
8875
9315
  if (inventory === "out_of_stock") productWhere.quantity = 0;
@@ -8887,11 +9327,15 @@ function createCrudHandler(dataSource, entityMap, options) {
8887
9327
  if (search && typeof search === "string" && search.trim()) {
8888
9328
  productWhere.name = ILike2(`%${search.trim()}%`);
8889
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;
8890
9334
  const [data2, total2] = await repo2.findAndCount({
8891
9335
  where: Object.keys(productWhere).length ? productWhere : void 0,
8892
9336
  skip,
8893
9337
  take: limit,
8894
- order: { [sortFieldRaw]: sortOrder }
9338
+ order: { [productSortField]: sortOrder }
8895
9339
  });
8896
9340
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
8897
9341
  }
@@ -8984,36 +9428,22 @@ function createCrudHandler(dataSource, entityMap, options) {
8984
9428
  }
8985
9429
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
8986
9430
  }
8987
- const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
9431
+ const defaultSortField = pickDefaultListSortField(columnNames, repo.metadata.columns);
9432
+ const sortParam = (searchParams.get("sortField") ?? "").trim();
9433
+ const sortField = sortParam && columnNames.has(sortParam) ? sortParam : defaultSortField;
8988
9434
  let where = {};
8989
9435
  if (search) {
8990
9436
  where = buildSearchWhereClause(repo, search);
8991
9437
  }
8992
- const intFilterKeys = ["productId", "attributeId", "taxId", "brandId", "categoryId", "collectionId", "parentId"];
8993
- const extraWhere = {};
8994
- for (const key of intFilterKeys) {
8995
- const v = searchParams.get(key);
8996
- if (v != null && v !== "" && columnNames.has(key)) {
8997
- const n = Number(v);
8998
- if (Number.isFinite(n)) extraWhere[key] = n;
8999
- }
9000
- }
9001
- for (const col of repo.metadata.columns) {
9002
- if (String(col.type) !== "boolean") continue;
9003
- const name = col.propertyName;
9004
- if (!columnNames.has(name)) continue;
9005
- const raw = searchParams.get(name)?.trim();
9006
- if (raw === "true" || raw === "false") {
9007
- extraWhere[name] = raw === "true";
9008
- }
9009
- }
9010
- 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) {
9011
9441
  if (Array.isArray(where)) {
9012
- where = where.map((w) => ({ ...w, ...extraWhere }));
9442
+ where = where.map((w) => ({ ...w, ...exactParamWhere }));
9013
9443
  } else if (where && typeof where === "object" && Object.keys(where).length > 0) {
9014
- where = { ...where, ...extraWhere };
9444
+ where = { ...where, ...exactParamWhere };
9015
9445
  } else {
9016
- where = extraWhere;
9446
+ where = exactParamWhere;
9017
9447
  }
9018
9448
  }
9019
9449
  where = mergeDeletedFalseWhere(repo, where);
@@ -9093,6 +9523,10 @@ function createCrudHandler(dataSource, entityMap, options) {
9093
9523
  }
9094
9524
  const repo = dataSource.getRepository(entity);
9095
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
+ }
9096
9530
  if (resource !== "media" && Object.keys(persistBody).length === 0) {
9097
9531
  logCrudClientError("POST create", {
9098
9532
  reason: "no_scalar_columns_after_pick",
@@ -9443,6 +9877,10 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
9443
9877
  if (!cur) return json({ message: "Not found" }, { status: 404 });
9444
9878
  }
9445
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
+ }
9446
9884
  if (resource === "media") {
9447
9885
  const u = updatePayload;
9448
9886
  delete u.kind;
@@ -10702,7 +11140,7 @@ function createCmsApiHandler(config) {
10702
11140
  } : usersApi;
10703
11141
  const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
10704
11142
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
10705
- const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
11143
+ const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
10706
11144
  const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
10707
11145
  const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
10708
11146
  dataSource,
@@ -10801,7 +11239,10 @@ function createCmsApiHandler(config) {
10801
11239
  }
10802
11240
  if (path.length === 2) {
10803
11241
  if (path[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
10804
- if (path[1] === "profile" && m === "PUT" && profilePut) return profilePut(req);
11242
+ if (path[1] === "profile" && profileHandlers) {
11243
+ if (m === "GET") return profileHandlers.GET(req);
11244
+ if (m === "PUT") return profileHandlers.PUT(req);
11245
+ }
10805
11246
  const id = path[1];
10806
11247
  if (m === "GET") return usersHandlers.getById(req, id);
10807
11248
  if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);