@infuro/cms-core 1.0.21 → 1.0.23

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
@@ -537,6 +537,40 @@ function logCrudClientError(op, detail) {
537
537
  function logCrudServerError(op, detail) {
538
538
  console.error(CRUD_LOG, op, detail);
539
539
  }
540
+ async function aggregateMediaFolderFileSizes(dataSource, folderIds) {
541
+ const map = /* @__PURE__ */ new Map();
542
+ if (folderIds.length === 0) return map;
543
+ try {
544
+ const rows = await dataSource.query(
545
+ `
546
+ WITH RECURSIVE walk AS (
547
+ SELECT m."parentId" AS root_id, m.id, m.kind, m.size
548
+ FROM media m
549
+ WHERE m."parentId" = ANY($1) AND m.deleted = false
550
+ UNION ALL
551
+ SELECT w.root_id, m.id, m.kind, m.size
552
+ FROM walk w
553
+ INNER JOIN media m ON m."parentId" = w.id AND m.deleted = false
554
+ )
555
+ SELECT root_id AS "rootId", COALESCE(SUM(size) FILTER (WHERE kind = 'file'), 0)::bigint AS "totalSize"
556
+ FROM walk
557
+ GROUP BY root_id
558
+ `,
559
+ [folderIds]
560
+ );
561
+ for (const r of rows) {
562
+ map.set(Number(r.rootId), Number(r.totalSize));
563
+ }
564
+ for (const id of folderIds) {
565
+ if (!map.has(id)) map.set(id, 0);
566
+ }
567
+ } catch (err) {
568
+ logCrudServerError("media folder size aggregate failed", {
569
+ message: err instanceof Error ? err.message : String(err)
570
+ });
571
+ }
572
+ return map;
573
+ }
540
574
  var DATE_COLUMN_TYPES = /* @__PURE__ */ new Set([
541
575
  "date",
542
576
  "datetime",
@@ -601,6 +635,16 @@ function mergeDeletedFalseWhere(repo, where) {
601
635
  }
602
636
  return Object.keys(where).length > 0 ? { ...where, ...d } : d;
603
637
  }
638
+ function normalizeProductSku(value) {
639
+ if (value == null) return null;
640
+ const s = String(value).trim();
641
+ return s === "" ? null : s;
642
+ }
643
+ async function assertProductSkuUnique(repo, sku, excludeId) {
644
+ const where = excludeId != null ? { sku, deleted: false, id: (0, import_typeorm.Not)(excludeId) } : { sku, deleted: false };
645
+ const row = await repo.findOne({ where });
646
+ return row == null;
647
+ }
604
648
  function buildSoftDeletePayload(meta, deletedBy) {
605
649
  const payload = { deleted: true };
606
650
  if (meta.columns.some((c) => c.propertyName === "deletedAt")) {
@@ -823,19 +867,36 @@ function createCrudHandler(dataSource, entityMap, options) {
823
867
  qb.andWhere("m.filename ILIKE :search", { search: `%${search.trim()}%` });
824
868
  }
825
869
  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
- );
870
+ if (typeFilter === "folder") {
871
+ qb.andWhere("m.kind = :folderKind", { folderKind: "folder" });
872
+ } else if (typeFilter === "file") {
873
+ qb.andWhere("m.kind = :fileKind", { fileKind: "file" });
874
+ } else if (typeFilter === "image") {
875
+ qb.andWhere("m.mimeType LIKE :imageMimeType", { imageMimeType: "image/%" });
876
+ } else if (typeFilter === "video") {
877
+ qb.andWhere("m.mimeType LIKE :videoMimeType", { videoMimeType: "video/%" });
878
+ } else if (typeFilter === "audio") {
879
+ qb.andWhere("m.mimeType LIKE :audioMimeType", { audioMimeType: "audio/%" });
880
+ } else if (typeFilter === "Document") {
881
+ qb.andWhere("m.mimeType LIKE :documentMimeType", { documentMimeType: "application/pdf" });
882
+ } else if (typeFilter === "application") {
883
+ qb.andWhere("m.kind = :folderKind", { folderKind: "folder" });
884
+ }
833
885
  }
834
886
  const allowedSort = ["filename", "createdAt", "id"];
835
887
  const sf = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "filename";
836
888
  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();
889
+ qb.orderBy(`m.${sf}`, so).skip(skip).take(limit);
890
+ const [rows, total2] = await qb.getManyAndCount();
891
+ const mediaRows = rows;
892
+ const folderIds = mediaRows.filter((m) => m.kind === "folder").map((m) => m.id);
893
+ let data2 = rows;
894
+ if (folderIds.length > 0) {
895
+ const sizeMap = await aggregateMediaFolderFileSizes(dataSource, folderIds);
896
+ data2 = mediaRows.map(
897
+ (m) => m.kind === "folder" ? { ...m, size: sizeMap.get(m.id) ?? 0 } : m
898
+ );
899
+ }
839
900
  return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
840
901
  }
841
902
  const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
@@ -946,6 +1007,20 @@ function createCrudHandler(dataSource, entityMap, options) {
946
1007
  });
947
1008
  return json({ error: "Invalid request payload" }, { status: 400 });
948
1009
  }
1010
+ if (resource === "products") {
1011
+ if ("sku" in persistBody) {
1012
+ const skuNorm = normalizeProductSku(persistBody.sku);
1013
+ if (skuNorm) {
1014
+ const ok = await assertProductSkuUnique(repo, skuNorm);
1015
+ if (!ok) {
1016
+ return json({ error: "SKU already exists. Please use a unique SKU." }, { status: 400 });
1017
+ }
1018
+ persistBody.sku = skuNorm;
1019
+ } else {
1020
+ persistBody.sku = null;
1021
+ }
1022
+ }
1023
+ }
949
1024
  sanitizeBodyForEntity(repo, persistBody);
950
1025
  let created;
951
1026
  try {
@@ -1140,7 +1215,20 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1140
1215
  relations: ["order", "order.contact", "contact"]
1141
1216
  });
1142
1217
  if (!payment) return json({ message: "Not found" }, { status: 404 });
1143
- return json(payment);
1218
+ const p = payment;
1219
+ const order = p.order;
1220
+ const orderContact = order?.contact;
1221
+ const contact = p.contact;
1222
+ const customer = orderContact ?? contact;
1223
+ return json({
1224
+ ...p,
1225
+ order: order ? {
1226
+ id: order.id,
1227
+ orderNumber: order.orderNumber,
1228
+ contact: orderContact ? { name: orderContact.name, email: orderContact.email } : null
1229
+ } : null,
1230
+ contact: customer ? { id: customer.id, name: customer.name, email: customer.email } : null
1231
+ });
1144
1232
  }
1145
1233
  if (resource === "blogs") {
1146
1234
  const blog = await repo.findOne({
@@ -1255,6 +1343,23 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1255
1343
  delete u.parentId;
1256
1344
  delete u.kind;
1257
1345
  }
1346
+ if (resource === "products") {
1347
+ const currentRow = await repo.findOne({
1348
+ where: { id: numericId, deleted: false }
1349
+ });
1350
+ if (!currentRow) return json({ message: "Not found" }, { status: 404 });
1351
+ const merged = { ...currentRow, ...updatePayload };
1352
+ const effSku = normalizeProductSku(merged.sku);
1353
+ if (effSku) {
1354
+ const ok = await assertProductSkuUnique(repo, effSku, numericId);
1355
+ if (!ok) {
1356
+ return json({ error: "SKU already exists. Please use a unique SKU." }, { status: 400 });
1357
+ }
1358
+ }
1359
+ if ("sku" in updatePayload) {
1360
+ updatePayload.sku = effSku;
1361
+ }
1362
+ }
1258
1363
  if (Object.keys(updatePayload).length > 0) {
1259
1364
  sanitizeBodyForEntity(repo, updatePayload);
1260
1365
  await repo.update(numericId, updatePayload);
@@ -1284,6 +1389,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1284
1389
  where: { id: numericId, deleted: false }
1285
1390
  });
1286
1391
  if (!existing) return json({ message: "Not found" }, { status: 404 });
1392
+ if (resource === "contacts") {
1393
+ const result2 = await repo.delete(numericId);
1394
+ if (result2.affected === 0) return json({ message: "Not found" }, { status: 404 });
1395
+ return json({ message: "Deleted successfully" }, { status: 200 });
1396
+ }
1287
1397
  let deletedBy = null;
1288
1398
  if (getDeletedByUserId) {
1289
1399
  try {
@@ -1831,12 +1941,14 @@ function createDashboardStatsHandler(config) {
1831
1941
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
1832
1942
  const repo = (name) => entityMap[name] ? dataSource.getRepository(entityMap[name]) : void 0;
1833
1943
  const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions, contactTypeRows] = await Promise.all([
1834
- repo("contacts")?.count() ?? 0,
1944
+ repo("contacts")?.count({ where: { deleted: false } }) ?? 0,
1835
1945
  repo("forms")?.count({ where: { deleted: false } }) ?? 0,
1836
1946
  repo("form_submissions")?.count() ?? 0,
1837
1947
  repo("users")?.count({ where: { deleted: false } }) ?? 0,
1838
1948
  repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
1839
- repo("contacts")?.count({ where: { createdAt: (0, import_typeorm5.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
1949
+ repo("contacts")?.count({
1950
+ where: { deleted: false, createdAt: (0, import_typeorm5.MoreThanOrEqual)(sevenDaysAgo) }
1951
+ }) ?? 0,
1840
1952
  repo("form_submissions")?.count({ where: { createdAt: (0, import_typeorm5.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
1841
1953
  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
1954
  ]);
@@ -2469,11 +2581,14 @@ function createFormSubmissionHandler(config) {
2469
2581
  const contactRepo = dataSource.getRepository(entityMap.contacts);
2470
2582
  let contact = await contactRepo.findOne({ where: { email: contactData.email } });
2471
2583
  if (!contact) {
2584
+ const createdAt = /* @__PURE__ */ new Date();
2472
2585
  contact = await contactRepo.save(
2473
2586
  contactRepo.create({
2474
2587
  name: contactData.name,
2475
2588
  email: contactData.email,
2476
- phone: contactData.phone
2589
+ phone: contactData.phone,
2590
+ createdAt,
2591
+ updatedAt: createdAt
2477
2592
  })
2478
2593
  );
2479
2594
  }
@@ -2629,12 +2744,13 @@ function createUsersApiHandlers(config) {
2629
2744
  const gid = body.groupId ?? null;
2630
2745
  const isCustomer = !!(customerG && gid === customerG.id);
2631
2746
  const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
2747
+ const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
2632
2748
  const newUser = await userRepo().save(
2633
2749
  userRepo().create({
2634
2750
  name: body.name,
2635
2751
  email: body.email,
2636
2752
  password: null,
2637
- blocked: true,
2753
+ blocked,
2638
2754
  groupId: gid,
2639
2755
  adminAccess
2640
2756
  })
@@ -2649,7 +2765,14 @@ function createUsersApiHandlers(config) {
2649
2765
  inviteLink,
2650
2766
  newUser.name ?? ""
2651
2767
  );
2652
- return json({ message: "User created successfully (blocked until password is set)", user: newUser, inviteLink }, { status: 201 });
2768
+ return json(
2769
+ {
2770
+ message: blocked ? "User created successfully (blocked until password is set)" : "User created successfully",
2771
+ user: newUser,
2772
+ inviteLink
2773
+ },
2774
+ { status: 201 }
2775
+ );
2653
2776
  } catch {
2654
2777
  return json({ error: "Server Error" }, { status: 500 });
2655
2778
  }
@@ -2947,15 +3070,36 @@ function createChatHandlers(config) {
2947
3070
  const email = body?.email?.trim();
2948
3071
  if (!name || !email) return json({ error: "name and email required" }, { status: 400 });
2949
3072
  const repo = contactRepo();
2950
- let contact = await repo.findOne({ where: { email, deleted: false } });
2951
- if (!contact) {
2952
- const created = repo.create({ name, email, phone: body.phone?.trim() || null });
2953
- contact = await repo.save(created);
3073
+ const phone = body.phone?.trim() || null;
3074
+ const existing = await repo.findOne({ where: { email } });
3075
+ let contact;
3076
+ if (!existing) {
3077
+ const createdAt = /* @__PURE__ */ new Date();
3078
+ contact = await repo.save(
3079
+ repo.create({ name, email, phone, createdAt, updatedAt: createdAt })
3080
+ );
3081
+ } else {
3082
+ const row = existing;
3083
+ if (row.deleted) {
3084
+ await repo.update(row.id, {
3085
+ deleted: false,
3086
+ deletedAt: null,
3087
+ deletedBy: null,
3088
+ name,
3089
+ phone
3090
+ });
3091
+ const refreshed = await repo.findOne({ where: { id: row.id } });
3092
+ if (!refreshed) return json({ error: "Failed to identify", detail: "contact missing after reactivate" }, { status: 500 });
3093
+ contact = refreshed;
3094
+ } else {
3095
+ contact = existing;
3096
+ }
2954
3097
  }
2955
3098
  const convRepoInst = convRepo();
2956
- const conv = await convRepoInst.save(convRepoInst.create({ contactId: contact.id }));
3099
+ const contactId = contact.id;
3100
+ const conv = await convRepoInst.save(convRepoInst.create({ contactId }));
2957
3101
  return json({
2958
- contactId: contact.id,
3102
+ contactId,
2959
3103
  conversationId: conv.id
2960
3104
  });
2961
3105
  } catch (err) {