@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.js CHANGED
@@ -476,6 +476,64 @@ async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, produc
476
476
  }
477
477
  }
478
478
 
479
+ // src/lib/address-geo-validation.ts
480
+ import { Country, State, City } from "country-state-city";
481
+ function norm(s) {
482
+ return typeof s === "string" ? s.trim() : "";
483
+ }
484
+ function resolveCountry(input) {
485
+ const t = input.trim();
486
+ if (!t) return void 0;
487
+ if (t.length === 2) {
488
+ const byCode = Country.getCountryByCode(t.toUpperCase());
489
+ if (byCode) return byCode;
490
+ }
491
+ const lower = t.toLowerCase();
492
+ return Country.getAllCountries().find((c) => c.name.toLowerCase() === lower);
493
+ }
494
+ function resolveState(countryIso, input) {
495
+ const t = input.trim();
496
+ if (!t || !countryIso) return void 0;
497
+ const states = State.getStatesOfCountry(countryIso);
498
+ const lower = t.toLowerCase();
499
+ return states.find((s) => s.isoCode.toLowerCase() === t.toLowerCase() || s.name.toLowerCase() === lower);
500
+ }
501
+ function resolveCity(countryIso, stateIso, input) {
502
+ const t = input.trim();
503
+ if (!t || !countryIso || !stateIso) return void 0;
504
+ const lower = t.toLowerCase();
505
+ const cities = City.getCitiesOfState(countryIso, stateIso);
506
+ return cities.find((c) => c.name.toLowerCase() === lower);
507
+ }
508
+ function assertValidAddressHierarchy(country, state, city) {
509
+ const c = resolveCountry(country);
510
+ if (!c) return { ok: false, error: "Invalid or unknown country." };
511
+ const st = resolveState(c.isoCode, state);
512
+ if (!st) return { ok: false, error: "State or province does not match the selected country." };
513
+ const ct = resolveCity(c.isoCode, st.isoCode, city);
514
+ if (!ct) return { ok: false, error: "City does not match the selected state." };
515
+ return { ok: true, country: c.name, state: st.name, city: ct.name };
516
+ }
517
+ function validateAndNormalizeAddressRow(row) {
518
+ const line1 = norm(row.line1);
519
+ const postalCode = norm(row.postalCode);
520
+ const countryIn = norm(row.country);
521
+ const stateIn = norm(row.state);
522
+ const cityIn = norm(row.city);
523
+ if (!line1) return "Street address (line 1) is required.";
524
+ if (!postalCode) return "Postal code is required.";
525
+ if (!countryIn || !stateIn || !cityIn) return "Country, state, and city are required.";
526
+ const geo = assertValidAddressHierarchy(countryIn, stateIn, cityIn);
527
+ if (!geo.ok) return geo.error;
528
+ row.line1 = line1;
529
+ row.line2 = norm(row.line2) || null;
530
+ row.postalCode = postalCode;
531
+ row.country = geo.country;
532
+ row.state = geo.state;
533
+ row.city = geo.city;
534
+ return null;
535
+ }
536
+
479
537
  // src/api/crud.ts
480
538
  var CRUD_LOG = "[cms-crud]";
481
539
  function logCrudClientError(op, detail) {
@@ -746,6 +804,17 @@ function createCrudHandler(dataSource, entityMap, options) {
746
804
  if (statusFilter) productWhere.status = statusFilter;
747
805
  if (inventory === "in_stock") productWhere.quantity = MoreThan(0);
748
806
  if (inventory === "out_of_stock") productWhere.quantity = 0;
807
+ for (const key of ["brandId", "categoryId", "collectionId"]) {
808
+ const raw = searchParams.get(key)?.trim();
809
+ if (raw) {
810
+ const n = Number(raw);
811
+ if (Number.isFinite(n)) productWhere[key] = n;
812
+ }
813
+ }
814
+ const featuredRaw = searchParams.get("featured")?.trim();
815
+ if (featuredRaw === "true" || featuredRaw === "false") {
816
+ productWhere.featured = featuredRaw === "true";
817
+ }
749
818
  if (search && typeof search === "string" && search.trim()) {
750
819
  productWhere.name = ILike(`%${search.trim()}%`);
751
820
  }
@@ -851,7 +920,7 @@ function createCrudHandler(dataSource, entityMap, options) {
851
920
  if (search) {
852
921
  where = buildSearchWhereClause(repo, search);
853
922
  }
854
- const intFilterKeys = ["productId", "attributeId", "taxId"];
923
+ const intFilterKeys = ["productId", "attributeId", "taxId", "brandId", "categoryId", "collectionId", "parentId"];
855
924
  const extraWhere = {};
856
925
  for (const key of intFilterKeys) {
857
926
  const v = searchParams.get(key);
@@ -860,6 +929,15 @@ function createCrudHandler(dataSource, entityMap, options) {
860
929
  if (Number.isFinite(n)) extraWhere[key] = n;
861
930
  }
862
931
  }
932
+ for (const col of repo.metadata.columns) {
933
+ if (String(col.type) !== "boolean") continue;
934
+ const name = col.propertyName;
935
+ if (!columnNames.has(name)) continue;
936
+ const raw = searchParams.get(name)?.trim();
937
+ if (raw === "true" || raw === "false") {
938
+ extraWhere[name] = raw === "true";
939
+ }
940
+ }
863
941
  if (Object.keys(extraWhere).length > 0) {
864
942
  if (Array.isArray(where)) {
865
943
  where = where.map((w) => ({ ...w, ...extraWhere }));
@@ -968,6 +1046,17 @@ function createCrudHandler(dataSource, entityMap, options) {
968
1046
  }
969
1047
  }
970
1048
  }
1049
+ if (resource === "addresses") {
1050
+ const cid = Number(persistBody.contactId);
1051
+ if (!Number.isFinite(cid)) {
1052
+ return json({ error: "Valid contactId is required." }, { status: 400 });
1053
+ }
1054
+ if (persistBody.tag === "") persistBody.tag = null;
1055
+ const addrErr = validateAndNormalizeAddressRow(persistBody);
1056
+ if (addrErr) {
1057
+ return json({ error: addrErr }, { status: 400 });
1058
+ }
1059
+ }
971
1060
  sanitizeBodyForEntity(repo, persistBody);
972
1061
  let created;
973
1062
  try {
@@ -1287,8 +1376,54 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1287
1376
  const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
1288
1377
  if (resource === "media") {
1289
1378
  const u = updatePayload;
1290
- delete u.parentId;
1291
1379
  delete u.kind;
1380
+ if (rawBody && typeof rawBody === "object" && "parentId" in rawBody) {
1381
+ let pid = null;
1382
+ const p = rawBody.parentId;
1383
+ if (p != null && p !== "") {
1384
+ const n = Number(p);
1385
+ if (!Number.isFinite(n)) {
1386
+ return json({ error: "Invalid parentId" }, { status: 400 });
1387
+ }
1388
+ pid = n;
1389
+ }
1390
+ if (pid != null) {
1391
+ const parent = await repo.findOne({
1392
+ where: { id: pid, deleted: false }
1393
+ });
1394
+ if (!parent || parent.kind !== "folder") {
1395
+ return json({ error: "parent must be a folder" }, { status: 400 });
1396
+ }
1397
+ }
1398
+ const row = await repo.findOne({
1399
+ where: { id: numericId, deleted: false }
1400
+ });
1401
+ if (!row) return json({ message: "Not found" }, { status: 404 });
1402
+ if (pid === numericId) {
1403
+ return json({ error: "Invalid parentId" }, { status: 400 });
1404
+ }
1405
+ if (row.kind === "folder" && pid != null) {
1406
+ let walk = pid;
1407
+ const seen = /* @__PURE__ */ new Set();
1408
+ while (walk != null) {
1409
+ if (walk === numericId) {
1410
+ return json(
1411
+ { error: "Cannot move a folder into itself or a descendant folder" },
1412
+ { status: 400 }
1413
+ );
1414
+ }
1415
+ if (seen.has(walk)) break;
1416
+ seen.add(walk);
1417
+ const anc = await repo.findOne({
1418
+ where: { id: walk, deleted: false }
1419
+ });
1420
+ walk = anc ? anc.parentId ?? null : null;
1421
+ }
1422
+ }
1423
+ u.parentId = pid;
1424
+ } else {
1425
+ delete u.parentId;
1426
+ }
1292
1427
  }
1293
1428
  if (resource === "products") {
1294
1429
  const currentRow = await repo.findOne({
@@ -1307,6 +1442,26 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1307
1442
  updatePayload.sku = effSku;
1308
1443
  }
1309
1444
  }
1445
+ if (resource === "addresses" && Object.keys(updatePayload).length > 0) {
1446
+ const currentRow = await repo.findOne({
1447
+ where: { id: numericId }
1448
+ });
1449
+ if (!currentRow) return json({ message: "Not found" }, { status: 404 });
1450
+ const merged = {
1451
+ ...currentRow,
1452
+ ...updatePayload
1453
+ };
1454
+ if (merged.tag === "") merged.tag = null;
1455
+ const addrErr = validateAndNormalizeAddressRow(merged);
1456
+ if (addrErr) {
1457
+ return json({ error: addrErr }, { status: 400 });
1458
+ }
1459
+ for (const k of Object.keys(updatePayload)) {
1460
+ if (k in merged) {
1461
+ updatePayload[k] = merged[k];
1462
+ }
1463
+ }
1464
+ }
1310
1465
  if (Object.keys(updatePayload).length > 0) {
1311
1466
  sanitizeBodyForEntity(repo, updatePayload);
1312
1467
  await repo.update(numericId, updatePayload);
@@ -1660,11 +1815,11 @@ async function readBufferFromPublicUrl(url) {
1660
1815
  throw new Error("Unsupported media URL");
1661
1816
  }
1662
1817
  function sanitizeZipPath(entryName) {
1663
- const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
1664
- for (const seg of norm) {
1818
+ const norm2 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
1819
+ for (const seg of norm2) {
1665
1820
  if (seg === ".." || seg === ".") return null;
1666
1821
  }
1667
- return norm;
1822
+ return norm2;
1668
1823
  }
1669
1824
  function shouldSkipEntry(parts) {
1670
1825
  if (parts[0] === "__MACOSX") return true;
@@ -8046,18 +8201,19 @@ function createStorefrontApiHandler(config) {
8046
8201
  const contactOrErr = await getContactForAddresses();
8047
8202
  if (contactOrErr instanceof Response) return contactOrErr;
8048
8203
  const b = await req.json().catch(() => ({}));
8049
- const created = await addressRepo().save(
8050
- addressRepo().create({
8051
- contactId: contactOrErr.contactId,
8052
- tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
8053
- line1: typeof b.line1 === "string" ? b.line1.trim() || null : null,
8054
- line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
8055
- city: typeof b.city === "string" ? b.city.trim() || null : null,
8056
- state: typeof b.state === "string" ? b.state.trim() || null : null,
8057
- postalCode: typeof b.postalCode === "string" ? b.postalCode.trim() || null : null,
8058
- country: typeof b.country === "string" ? b.country.trim() || null : null
8059
- })
8060
- );
8204
+ const row = {
8205
+ contactId: contactOrErr.contactId,
8206
+ tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
8207
+ line1: typeof b.line1 === "string" ? b.line1 : "",
8208
+ line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
8209
+ city: typeof b.city === "string" ? b.city : "",
8210
+ state: typeof b.state === "string" ? b.state : "",
8211
+ postalCode: typeof b.postalCode === "string" ? b.postalCode : "",
8212
+ country: typeof b.country === "string" ? b.country : ""
8213
+ };
8214
+ const addrErr = validateAndNormalizeAddressRow(row);
8215
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
8216
+ const created = await addressRepo().save(addressRepo().create(row));
8061
8217
  return json(serializeAddress2(created));
8062
8218
  }
8063
8219
  if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
@@ -8076,7 +8232,16 @@ function createStorefrontApiHandler(config) {
8076
8232
  if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
8077
8233
  if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
8078
8234
  if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
8079
- if (Object.keys(updates).length) await addressRepo().update(id, updates);
8235
+ if (Object.keys(updates).length) {
8236
+ const merged = { ...existing, ...updates };
8237
+ if (merged.tag === "") merged.tag = null;
8238
+ const addrErr = validateAndNormalizeAddressRow(merged);
8239
+ if (addrErr) return json({ error: addrErr }, { status: 400 });
8240
+ for (const k of Object.keys(updates)) {
8241
+ if (k in merged) updates[k] = merged[k];
8242
+ }
8243
+ await addressRepo().update(id, updates);
8244
+ }
8080
8245
  const updated = await addressRepo().findOne({ where: { id } });
8081
8246
  return json(serializeAddress2(updated));
8082
8247
  }