@infuro/cms-core 1.0.22 → 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) {
@@ -537,6 +595,40 @@ function logCrudClientError(op, detail) {
537
595
  function logCrudServerError(op, detail) {
538
596
  console.error(CRUD_LOG, op, detail);
539
597
  }
598
+ async function aggregateMediaFolderFileSizes(dataSource, folderIds) {
599
+ const map = /* @__PURE__ */ new Map();
600
+ if (folderIds.length === 0) return map;
601
+ try {
602
+ const rows = await dataSource.query(
603
+ `
604
+ WITH RECURSIVE walk AS (
605
+ SELECT m."parentId" AS root_id, m.id, m.kind, m.size
606
+ FROM media m
607
+ WHERE m."parentId" = ANY($1) AND m.deleted = false
608
+ UNION ALL
609
+ SELECT w.root_id, m.id, m.kind, m.size
610
+ FROM walk w
611
+ INNER JOIN media m ON m."parentId" = w.id AND m.deleted = false
612
+ )
613
+ SELECT root_id AS "rootId", COALESCE(SUM(size) FILTER (WHERE kind = 'file'), 0)::bigint AS "totalSize"
614
+ FROM walk
615
+ GROUP BY root_id
616
+ `,
617
+ [folderIds]
618
+ );
619
+ for (const r of rows) {
620
+ map.set(Number(r.rootId), Number(r.totalSize));
621
+ }
622
+ for (const id of folderIds) {
623
+ if (!map.has(id)) map.set(id, 0);
624
+ }
625
+ } catch (err) {
626
+ logCrudServerError("media folder size aggregate failed", {
627
+ message: err instanceof Error ? err.message : String(err)
628
+ });
629
+ }
630
+ return map;
631
+ }
540
632
  var DATE_COLUMN_TYPES = /* @__PURE__ */ new Set([
541
633
  "date",
542
634
  "datetime",
@@ -601,6 +693,16 @@ function mergeDeletedFalseWhere(repo, where) {
601
693
  }
602
694
  return Object.keys(where).length > 0 ? { ...where, ...d } : d;
603
695
  }
696
+ function normalizeProductSku(value) {
697
+ if (value == null) return null;
698
+ const s = String(value).trim();
699
+ return s === "" ? null : s;
700
+ }
701
+ async function assertProductSkuUnique(repo, sku, excludeId) {
702
+ const where = excludeId != null ? { sku, deleted: false, id: (0, import_typeorm.Not)(excludeId) } : { sku, deleted: false };
703
+ const row = await repo.findOne({ where });
704
+ return row == null;
705
+ }
604
706
  function buildSoftDeletePayload(meta, deletedBy) {
605
707
  const payload = { deleted: true };
606
708
  if (meta.columns.some((c) => c.propertyName === "deletedAt")) {
@@ -755,6 +857,17 @@ function createCrudHandler(dataSource, entityMap, options) {
755
857
  if (statusFilter) productWhere.status = statusFilter;
756
858
  if (inventory === "in_stock") productWhere.quantity = (0, import_typeorm.MoreThan)(0);
757
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
+ }
758
871
  if (search && typeof search === "string" && search.trim()) {
759
872
  productWhere.name = (0, import_typeorm.ILike)(`%${search.trim()}%`);
760
873
  }
@@ -823,19 +936,36 @@ function createCrudHandler(dataSource, entityMap, options) {
823
936
  qb.andWhere("m.filename ILIKE :search", { search: `%${search.trim()}%` });
824
937
  }
825
938
  if (typeFilter) {
826
- qb.andWhere(
827
- new import_typeorm.Brackets((sq) => {
828
- sq.where("m.kind = :folderKind", { folderKind: "folder" }).orWhere("m.mimeType LIKE :mtp", {
829
- mtp: `${typeFilter}/%`
830
- });
831
- })
832
- );
939
+ if (typeFilter === "folder") {
940
+ qb.andWhere("m.kind = :folderKind", { folderKind: "folder" });
941
+ } else if (typeFilter === "file") {
942
+ qb.andWhere("m.kind = :fileKind", { fileKind: "file" });
943
+ } else if (typeFilter === "image") {
944
+ qb.andWhere("m.mimeType LIKE :imageMimeType", { imageMimeType: "image/%" });
945
+ } else if (typeFilter === "video") {
946
+ qb.andWhere("m.mimeType LIKE :videoMimeType", { videoMimeType: "video/%" });
947
+ } else if (typeFilter === "audio") {
948
+ qb.andWhere("m.mimeType LIKE :audioMimeType", { audioMimeType: "audio/%" });
949
+ } else if (typeFilter === "Document") {
950
+ qb.andWhere("m.mimeType LIKE :documentMimeType", { documentMimeType: "application/pdf" });
951
+ } else if (typeFilter === "application") {
952
+ qb.andWhere("m.kind = :folderKind", { folderKind: "folder" });
953
+ }
833
954
  }
834
955
  const allowedSort = ["filename", "createdAt", "id"];
835
956
  const sf = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "filename";
836
957
  const so = sortOrder === "DESC" ? "DESC" : "ASC";
837
- qb.orderBy("CASE WHEN m.kind = :fk THEN 0 ELSE 1 END", "ASC").addOrderBy(`m.${sf}`, so).setParameter("fk", "folder").skip(skip).take(limit);
838
- const [data2, total2] = await qb.getManyAndCount();
958
+ qb.orderBy(`m.${sf}`, so).skip(skip).take(limit);
959
+ const [rows, total2] = await qb.getManyAndCount();
960
+ const mediaRows = rows;
961
+ const folderIds = mediaRows.filter((m) => m.kind === "folder").map((m) => m.id);
962
+ let data2 = rows;
963
+ if (folderIds.length > 0) {
964
+ const sizeMap = await aggregateMediaFolderFileSizes(dataSource, folderIds);
965
+ data2 = mediaRows.map(
966
+ (m) => m.kind === "folder" ? { ...m, size: sizeMap.get(m.id) ?? 0 } : m
967
+ );
968
+ }
839
969
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
840
970
  }
841
971
  const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
@@ -843,7 +973,7 @@ function createCrudHandler(dataSource, entityMap, options) {
843
973
  if (search) {
844
974
  where = buildSearchWhereClause(repo, search);
845
975
  }
846
- const intFilterKeys = ["productId", "attributeId", "taxId"];
976
+ const intFilterKeys = ["productId", "attributeId", "taxId", "brandId", "categoryId", "collectionId", "parentId"];
847
977
  const extraWhere = {};
848
978
  for (const key of intFilterKeys) {
849
979
  const v = searchParams.get(key);
@@ -852,6 +982,15 @@ function createCrudHandler(dataSource, entityMap, options) {
852
982
  if (Number.isFinite(n)) extraWhere[key] = n;
853
983
  }
854
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
+ }
855
994
  if (Object.keys(extraWhere).length > 0) {
856
995
  if (Array.isArray(where)) {
857
996
  where = where.map((w) => ({ ...w, ...extraWhere }));
@@ -946,6 +1085,31 @@ function createCrudHandler(dataSource, entityMap, options) {
946
1085
  });
947
1086
  return json({ error: "Invalid request payload" }, { status: 400 });
948
1087
  }
1088
+ if (resource === "products") {
1089
+ if ("sku" in persistBody) {
1090
+ const skuNorm = normalizeProductSku(persistBody.sku);
1091
+ if (skuNorm) {
1092
+ const ok = await assertProductSkuUnique(repo, skuNorm);
1093
+ if (!ok) {
1094
+ return json({ error: "SKU already exists. Please use a unique SKU." }, { status: 400 });
1095
+ }
1096
+ persistBody.sku = skuNorm;
1097
+ } else {
1098
+ persistBody.sku = null;
1099
+ }
1100
+ }
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
+ }
949
1113
  sanitizeBodyForEntity(repo, persistBody);
950
1114
  let created;
951
1115
  try {
@@ -1140,7 +1304,20 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1140
1304
  relations: ["order", "order.contact", "contact"]
1141
1305
  });
1142
1306
  if (!payment) return json({ message: "Not found" }, { status: 404 });
1143
- return json(payment);
1307
+ const p = payment;
1308
+ const order = p.order;
1309
+ const orderContact = order?.contact;
1310
+ const contact = p.contact;
1311
+ const customer = orderContact ?? contact;
1312
+ return json({
1313
+ ...p,
1314
+ order: order ? {
1315
+ id: order.id,
1316
+ orderNumber: order.orderNumber,
1317
+ contact: orderContact ? { name: orderContact.name, email: orderContact.email } : null
1318
+ } : null,
1319
+ contact: customer ? { id: customer.id, name: customer.name, email: customer.email } : null
1320
+ });
1144
1321
  }
1145
1322
  if (resource === "blogs") {
1146
1323
  const blog = await repo.findOne({
@@ -1252,8 +1429,91 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1252
1429
  const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
1253
1430
  if (resource === "media") {
1254
1431
  const u = updatePayload;
1255
- delete u.parentId;
1256
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
+ }
1480
+ }
1481
+ if (resource === "products") {
1482
+ const currentRow = await repo.findOne({
1483
+ where: { id: numericId, deleted: false }
1484
+ });
1485
+ if (!currentRow) return json({ message: "Not found" }, { status: 404 });
1486
+ const merged = { ...currentRow, ...updatePayload };
1487
+ const effSku = normalizeProductSku(merged.sku);
1488
+ if (effSku) {
1489
+ const ok = await assertProductSkuUnique(repo, effSku, numericId);
1490
+ if (!ok) {
1491
+ return json({ error: "SKU already exists. Please use a unique SKU." }, { status: 400 });
1492
+ }
1493
+ }
1494
+ if ("sku" in updatePayload) {
1495
+ updatePayload.sku = effSku;
1496
+ }
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
+ }
1257
1517
  }
1258
1518
  if (Object.keys(updatePayload).length > 0) {
1259
1519
  sanitizeBodyForEntity(repo, updatePayload);
@@ -1284,6 +1544,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1284
1544
  where: { id: numericId, deleted: false }
1285
1545
  });
1286
1546
  if (!existing) return json({ message: "Not found" }, { status: 404 });
1547
+ if (resource === "contacts") {
1548
+ const result2 = await repo.delete(numericId);
1549
+ if (result2.affected === 0) return json({ message: "Not found" }, { status: 404 });
1550
+ return json({ message: "Deleted successfully" }, { status: 200 });
1551
+ }
1287
1552
  let deletedBy = null;
1288
1553
  if (getDeletedByUserId) {
1289
1554
  try {
@@ -1603,11 +1868,11 @@ async function readBufferFromPublicUrl(url) {
1603
1868
  throw new Error("Unsupported media URL");
1604
1869
  }
1605
1870
  function sanitizeZipPath(entryName) {
1606
- const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
1607
- for (const seg of norm) {
1871
+ const norm2 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
1872
+ for (const seg of norm2) {
1608
1873
  if (seg === ".." || seg === ".") return null;
1609
1874
  }
1610
- return norm;
1875
+ return norm2;
1611
1876
  }
1612
1877
  function shouldSkipEntry(parts) {
1613
1878
  if (parts[0] === "__MACOSX") return true;
@@ -1831,12 +2096,14 @@ function createDashboardStatsHandler(config) {
1831
2096
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
1832
2097
  const repo = (name) => entityMap[name] ? dataSource.getRepository(entityMap[name]) : void 0;
1833
2098
  const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions, contactTypeRows] = await Promise.all([
1834
- repo("contacts")?.count() ?? 0,
2099
+ repo("contacts")?.count({ where: { deleted: false } }) ?? 0,
1835
2100
  repo("forms")?.count({ where: { deleted: false } }) ?? 0,
1836
2101
  repo("form_submissions")?.count() ?? 0,
1837
2102
  repo("users")?.count({ where: { deleted: false } }) ?? 0,
1838
2103
  repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
1839
- repo("contacts")?.count({ where: { createdAt: (0, import_typeorm5.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
2104
+ repo("contacts")?.count({
2105
+ where: { deleted: false, createdAt: (0, import_typeorm5.MoreThanOrEqual)(sevenDaysAgo) }
2106
+ }) ?? 0,
1840
2107
  repo("form_submissions")?.count({ where: { createdAt: (0, import_typeorm5.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
1841
2108
  repo("contacts")?.createQueryBuilder("c").select("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')", "type").addSelect("COUNT(*)", "count").where("c.deleted = :deleted", { deleted: false }).groupBy("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')").getRawMany() ?? []
1842
2109
  ]);
@@ -2469,11 +2736,14 @@ function createFormSubmissionHandler(config) {
2469
2736
  const contactRepo = dataSource.getRepository(entityMap.contacts);
2470
2737
  let contact = await contactRepo.findOne({ where: { email: contactData.email } });
2471
2738
  if (!contact) {
2739
+ const createdAt = /* @__PURE__ */ new Date();
2472
2740
  contact = await contactRepo.save(
2473
2741
  contactRepo.create({
2474
2742
  name: contactData.name,
2475
2743
  email: contactData.email,
2476
- phone: contactData.phone
2744
+ phone: contactData.phone,
2745
+ createdAt,
2746
+ updatedAt: createdAt
2477
2747
  })
2478
2748
  );
2479
2749
  }
@@ -2629,12 +2899,13 @@ function createUsersApiHandlers(config) {
2629
2899
  const gid = body.groupId ?? null;
2630
2900
  const isCustomer = !!(customerG && gid === customerG.id);
2631
2901
  const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
2902
+ const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
2632
2903
  const newUser = await userRepo().save(
2633
2904
  userRepo().create({
2634
2905
  name: body.name,
2635
2906
  email: body.email,
2636
2907
  password: null,
2637
- blocked: true,
2908
+ blocked,
2638
2909
  groupId: gid,
2639
2910
  adminAccess
2640
2911
  })
@@ -2649,7 +2920,14 @@ function createUsersApiHandlers(config) {
2649
2920
  inviteLink,
2650
2921
  newUser.name ?? ""
2651
2922
  );
2652
- return json({ message: "User created successfully (blocked until password is set)", user: newUser, inviteLink }, { status: 201 });
2923
+ return json(
2924
+ {
2925
+ message: blocked ? "User created successfully (blocked until password is set)" : "User created successfully",
2926
+ user: newUser,
2927
+ inviteLink
2928
+ },
2929
+ { status: 201 }
2930
+ );
2653
2931
  } catch {
2654
2932
  return json({ error: "Server Error" }, { status: 500 });
2655
2933
  }
@@ -2951,7 +3229,10 @@ function createChatHandlers(config) {
2951
3229
  const existing = await repo.findOne({ where: { email } });
2952
3230
  let contact;
2953
3231
  if (!existing) {
2954
- contact = await repo.save(repo.create({ name, email, phone }));
3232
+ const createdAt = /* @__PURE__ */ new Date();
3233
+ contact = await repo.save(
3234
+ repo.create({ name, email, phone, createdAt, updatedAt: createdAt })
3235
+ );
2955
3236
  } else {
2956
3237
  const row = existing;
2957
3238
  if (row.deleted) {
@@ -7964,18 +8245,19 @@ function createStorefrontApiHandler(config) {
7964
8245
  const contactOrErr = await getContactForAddresses();
7965
8246
  if (contactOrErr instanceof Response) return contactOrErr;
7966
8247
  const b = await req.json().catch(() => ({}));
7967
- const created = await addressRepo().save(
7968
- addressRepo().create({
7969
- contactId: contactOrErr.contactId,
7970
- tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
7971
- line1: typeof b.line1 === "string" ? b.line1.trim() || null : null,
7972
- line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
7973
- city: typeof b.city === "string" ? b.city.trim() || null : null,
7974
- state: typeof b.state === "string" ? b.state.trim() || null : null,
7975
- postalCode: typeof b.postalCode === "string" ? b.postalCode.trim() || null : null,
7976
- country: typeof b.country === "string" ? b.country.trim() || null : null
7977
- })
7978
- );
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));
7979
8261
  return json(serializeAddress2(created));
7980
8262
  }
7981
8263
  if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
@@ -7994,7 +8276,16 @@ function createStorefrontApiHandler(config) {
7994
8276
  if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
7995
8277
  if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
7996
8278
  if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
7997
- 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
+ }
7998
8289
  const updated = await addressRepo().findOne({ where: { id } });
7999
8290
  return json(serializeAddress2(updated));
8000
8291
  }