@infuro/cms-core 1.0.23 → 1.0.24

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/api.cjs CHANGED
@@ -529,6 +529,64 @@ async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, produc
529
529
  }
530
530
  }
531
531
 
532
+ // src/lib/address-geo-validation.ts
533
+ var import_country_state_city = require("country-state-city");
534
+ function norm(s) {
535
+ return typeof s === "string" ? s.trim() : "";
536
+ }
537
+ function resolveCountry(input) {
538
+ const t = input.trim();
539
+ if (!t) return void 0;
540
+ if (t.length === 2) {
541
+ const byCode = import_country_state_city.Country.getCountryByCode(t.toUpperCase());
542
+ if (byCode) return byCode;
543
+ }
544
+ const lower = t.toLowerCase();
545
+ return import_country_state_city.Country.getAllCountries().find((c) => c.name.toLowerCase() === lower);
546
+ }
547
+ function resolveState(countryIso, input) {
548
+ const t = input.trim();
549
+ if (!t || !countryIso) return void 0;
550
+ const states = import_country_state_city.State.getStatesOfCountry(countryIso);
551
+ const lower = t.toLowerCase();
552
+ return states.find((s) => s.isoCode.toLowerCase() === t.toLowerCase() || s.name.toLowerCase() === lower);
553
+ }
554
+ function resolveCity(countryIso, stateIso, input) {
555
+ const t = input.trim();
556
+ if (!t || !countryIso || !stateIso) return void 0;
557
+ const lower = t.toLowerCase();
558
+ const cities = import_country_state_city.City.getCitiesOfState(countryIso, stateIso);
559
+ return cities.find((c) => c.name.toLowerCase() === lower);
560
+ }
561
+ function assertValidAddressHierarchy(country, state, city) {
562
+ const c = resolveCountry(country);
563
+ if (!c) return { ok: false, error: "Invalid or unknown country." };
564
+ const st = resolveState(c.isoCode, state);
565
+ if (!st) return { ok: false, error: "State or province does not match the selected country." };
566
+ const ct = resolveCity(c.isoCode, st.isoCode, city);
567
+ if (!ct) return { ok: false, error: "City does not match the selected state." };
568
+ return { ok: true, country: c.name, state: st.name, city: ct.name };
569
+ }
570
+ function validateAndNormalizeAddressRow(row) {
571
+ const line1 = norm(row.line1);
572
+ const postalCode = norm(row.postalCode);
573
+ const countryIn = norm(row.country);
574
+ const stateIn = norm(row.state);
575
+ const cityIn = norm(row.city);
576
+ if (!line1) return "Street address (line 1) is required.";
577
+ if (!postalCode) return "Postal code is required.";
578
+ if (!countryIn || !stateIn || !cityIn) return "Country, state, and city are required.";
579
+ const geo = assertValidAddressHierarchy(countryIn, stateIn, cityIn);
580
+ if (!geo.ok) return geo.error;
581
+ row.line1 = line1;
582
+ row.line2 = norm(row.line2) || null;
583
+ row.postalCode = postalCode;
584
+ row.country = geo.country;
585
+ row.state = geo.state;
586
+ row.city = geo.city;
587
+ return null;
588
+ }
589
+
532
590
  // src/api/crud.ts
533
591
  var CRUD_LOG = "[cms-crud]";
534
592
  function logCrudClientError(op, detail) {
@@ -799,6 +857,17 @@ function createCrudHandler(dataSource, entityMap, options) {
799
857
  if (statusFilter) productWhere.status = statusFilter;
800
858
  if (inventory === "in_stock") productWhere.quantity = (0, import_typeorm.MoreThan)(0);
801
859
  if (inventory === "out_of_stock") productWhere.quantity = 0;
860
+ for (const key of ["brandId", "categoryId", "collectionId"]) {
861
+ const raw = searchParams.get(key)?.trim();
862
+ if (raw) {
863
+ const n = Number(raw);
864
+ if (Number.isFinite(n)) productWhere[key] = n;
865
+ }
866
+ }
867
+ const featuredRaw = searchParams.get("featured")?.trim();
868
+ if (featuredRaw === "true" || featuredRaw === "false") {
869
+ productWhere.featured = featuredRaw === "true";
870
+ }
802
871
  if (search && typeof search === "string" && search.trim()) {
803
872
  productWhere.name = (0, import_typeorm.ILike)(`%${search.trim()}%`);
804
873
  }
@@ -904,7 +973,7 @@ function createCrudHandler(dataSource, entityMap, options) {
904
973
  if (search) {
905
974
  where = buildSearchWhereClause(repo, search);
906
975
  }
907
- const intFilterKeys = ["productId", "attributeId", "taxId"];
976
+ const intFilterKeys = ["productId", "attributeId", "taxId", "brandId", "categoryId", "collectionId", "parentId"];
908
977
  const extraWhere = {};
909
978
  for (const key of intFilterKeys) {
910
979
  const v = searchParams.get(key);
@@ -913,6 +982,15 @@ function createCrudHandler(dataSource, entityMap, options) {
913
982
  if (Number.isFinite(n)) extraWhere[key] = n;
914
983
  }
915
984
  }
985
+ for (const col of repo.metadata.columns) {
986
+ if (String(col.type) !== "boolean") continue;
987
+ const name = col.propertyName;
988
+ if (!columnNames.has(name)) continue;
989
+ const raw = searchParams.get(name)?.trim();
990
+ if (raw === "true" || raw === "false") {
991
+ extraWhere[name] = raw === "true";
992
+ }
993
+ }
916
994
  if (Object.keys(extraWhere).length > 0) {
917
995
  if (Array.isArray(where)) {
918
996
  where = where.map((w) => ({ ...w, ...extraWhere }));
@@ -1021,6 +1099,17 @@ function createCrudHandler(dataSource, entityMap, options) {
1021
1099
  }
1022
1100
  }
1023
1101
  }
1102
+ if (resource === "addresses") {
1103
+ const cid = Number(persistBody.contactId);
1104
+ if (!Number.isFinite(cid)) {
1105
+ return json({ error: "Valid contactId is required." }, { status: 400 });
1106
+ }
1107
+ if (persistBody.tag === "") persistBody.tag = null;
1108
+ const addrErr = validateAndNormalizeAddressRow(persistBody);
1109
+ if (addrErr) {
1110
+ return json({ error: addrErr }, { status: 400 });
1111
+ }
1112
+ }
1024
1113
  sanitizeBodyForEntity(repo, persistBody);
1025
1114
  let created;
1026
1115
  try {
@@ -1340,8 +1429,54 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1340
1429
  const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
1341
1430
  if (resource === "media") {
1342
1431
  const u = updatePayload;
1343
- delete u.parentId;
1344
1432
  delete u.kind;
1433
+ if (rawBody && typeof rawBody === "object" && "parentId" in rawBody) {
1434
+ let pid = null;
1435
+ const p = rawBody.parentId;
1436
+ if (p != null && p !== "") {
1437
+ const n = Number(p);
1438
+ if (!Number.isFinite(n)) {
1439
+ return json({ error: "Invalid parentId" }, { status: 400 });
1440
+ }
1441
+ pid = n;
1442
+ }
1443
+ if (pid != null) {
1444
+ const parent = await repo.findOne({
1445
+ where: { id: pid, deleted: false }
1446
+ });
1447
+ if (!parent || parent.kind !== "folder") {
1448
+ return json({ error: "parent must be a folder" }, { status: 400 });
1449
+ }
1450
+ }
1451
+ const row = await repo.findOne({
1452
+ where: { id: numericId, deleted: false }
1453
+ });
1454
+ if (!row) return json({ message: "Not found" }, { status: 404 });
1455
+ if (pid === numericId) {
1456
+ return json({ error: "Invalid parentId" }, { status: 400 });
1457
+ }
1458
+ if (row.kind === "folder" && pid != null) {
1459
+ let walk = pid;
1460
+ const seen = /* @__PURE__ */ new Set();
1461
+ while (walk != null) {
1462
+ if (walk === numericId) {
1463
+ return json(
1464
+ { error: "Cannot move a folder into itself or a descendant folder" },
1465
+ { status: 400 }
1466
+ );
1467
+ }
1468
+ if (seen.has(walk)) break;
1469
+ seen.add(walk);
1470
+ const anc = await repo.findOne({
1471
+ where: { id: walk, deleted: false }
1472
+ });
1473
+ walk = anc ? anc.parentId ?? null : null;
1474
+ }
1475
+ }
1476
+ u.parentId = pid;
1477
+ } else {
1478
+ delete u.parentId;
1479
+ }
1345
1480
  }
1346
1481
  if (resource === "products") {
1347
1482
  const currentRow = await repo.findOne({
@@ -1360,6 +1495,26 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1360
1495
  updatePayload.sku = effSku;
1361
1496
  }
1362
1497
  }
1498
+ if (resource === "addresses" && Object.keys(updatePayload).length > 0) {
1499
+ const currentRow = await repo.findOne({
1500
+ where: { id: numericId }
1501
+ });
1502
+ if (!currentRow) return json({ message: "Not found" }, { status: 404 });
1503
+ const merged = {
1504
+ ...currentRow,
1505
+ ...updatePayload
1506
+ };
1507
+ if (merged.tag === "") merged.tag = null;
1508
+ const addrErr = validateAndNormalizeAddressRow(merged);
1509
+ if (addrErr) {
1510
+ return json({ error: addrErr }, { status: 400 });
1511
+ }
1512
+ for (const k of Object.keys(updatePayload)) {
1513
+ if (k in merged) {
1514
+ updatePayload[k] = merged[k];
1515
+ }
1516
+ }
1517
+ }
1363
1518
  if (Object.keys(updatePayload).length > 0) {
1364
1519
  sanitizeBodyForEntity(repo, updatePayload);
1365
1520
  await repo.update(numericId, updatePayload);
@@ -1713,11 +1868,11 @@ async function readBufferFromPublicUrl(url) {
1713
1868
  throw new Error("Unsupported media URL");
1714
1869
  }
1715
1870
  function sanitizeZipPath(entryName) {
1716
- const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
1717
- for (const seg of norm) {
1871
+ const norm2 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
1872
+ for (const seg of norm2) {
1718
1873
  if (seg === ".." || seg === ".") return null;
1719
1874
  }
1720
- return norm;
1875
+ return norm2;
1721
1876
  }
1722
1877
  function shouldSkipEntry(parts) {
1723
1878
  if (parts[0] === "__MACOSX") return true;
@@ -8090,18 +8245,19 @@ function createStorefrontApiHandler(config) {
8090
8245
  const contactOrErr = await getContactForAddresses();
8091
8246
  if (contactOrErr instanceof Response) return contactOrErr;
8092
8247
  const b = await req.json().catch(() => ({}));
8093
- const created = await addressRepo().save(
8094
- addressRepo().create({
8095
- contactId: contactOrErr.contactId,
8096
- tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
8097
- line1: typeof b.line1 === "string" ? b.line1.trim() || null : null,
8098
- line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
8099
- city: typeof b.city === "string" ? b.city.trim() || null : null,
8100
- state: typeof b.state === "string" ? b.state.trim() || null : null,
8101
- postalCode: typeof b.postalCode === "string" ? b.postalCode.trim() || null : null,
8102
- country: typeof b.country === "string" ? b.country.trim() || null : null
8103
- })
8104
- );
8248
+ const row = {
8249
+ contactId: contactOrErr.contactId,
8250
+ tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
8251
+ line1: typeof b.line1 === "string" ? b.line1 : "",
8252
+ line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
8253
+ city: typeof b.city === "string" ? b.city : "",
8254
+ state: typeof b.state === "string" ? b.state : "",
8255
+ postalCode: typeof b.postalCode === "string" ? b.postalCode : "",
8256
+ country: typeof b.country === "string" ? b.country : ""
8257
+ };
8258
+ const addrErr = validateAndNormalizeAddressRow(row);
8259
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
8260
+ const created = await addressRepo().save(addressRepo().create(row));
8105
8261
  return json(serializeAddress2(created));
8106
8262
  }
8107
8263
  if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
@@ -8120,7 +8276,16 @@ function createStorefrontApiHandler(config) {
8120
8276
  if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
8121
8277
  if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
8122
8278
  if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
8123
- if (Object.keys(updates).length) await addressRepo().update(id, updates);
8279
+ if (Object.keys(updates).length) {
8280
+ const merged = { ...existing, ...updates };
8281
+ if (merged.tag === "") merged.tag = null;
8282
+ const addrErr = validateAndNormalizeAddressRow(merged);
8283
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
8284
+ for (const k of Object.keys(updates)) {
8285
+ if (k in merged) updates[k] = merged[k];
8286
+ }
8287
+ await addressRepo().update(id, updates);
8288
+ }
8124
8289
  const updated = await addressRepo().findOne({ where: { id } });
8125
8290
  return json(serializeAddress2(updated));
8126
8291
  }