@infuro/cms-core 1.0.4 → 1.0.6

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
@@ -201,14 +201,156 @@ function createCrudHandler(dataSource, entityMap, options) {
201
201
  if (!resource || !entity) {
202
202
  return json({ error: "Invalid resource" }, { status: 400 });
203
203
  }
204
- const repo = dataSource.getRepository(entity);
205
204
  const { searchParams } = new URL(req.url);
206
205
  const page = Number(searchParams.get("page")) || 1;
207
- const limit = Number(searchParams.get("limit")) || 10;
206
+ const limit = Math.min(Number(searchParams.get("limit")) || 10, 100);
208
207
  const skip = (page - 1) * limit;
209
- const sortField = searchParams.get("sortField") || "createdAt";
208
+ const sortFieldRaw = searchParams.get("sortField") || "createdAt";
210
209
  const sortOrder = searchParams.get("sortOrder") === "desc" ? "DESC" : "ASC";
211
210
  const search = searchParams.get("search");
211
+ if (resource === "orders") {
212
+ const repo2 = dataSource.getRepository(entity);
213
+ const allowedSort = ["id", "orderNumber", "contactId", "status", "total", "currency", "createdAt", "updatedAt"];
214
+ const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
215
+ const sortOrderOrders = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
216
+ const statusFilter = searchParams.get("status")?.trim();
217
+ const dateFrom = searchParams.get("dateFrom")?.trim();
218
+ const dateTo = searchParams.get("dateTo")?.trim();
219
+ const paymentRef = searchParams.get("paymentRef")?.trim();
220
+ let orderIdsFromPayment = null;
221
+ if (paymentRef && entityMap["payments"]) {
222
+ const paymentRepo = dataSource.getRepository(entityMap["payments"]);
223
+ const payments = await paymentRepo.createQueryBuilder("p").select("p.orderId").where("p.externalReference = :ref", { ref: paymentRef }).orWhere("p.metadata->>'razorpayPaymentId' = :ref", { ref: paymentRef }).getRawMany();
224
+ orderIdsFromPayment = payments.map((r) => r.orderId);
225
+ if (orderIdsFromPayment.length === 0) {
226
+ return json({ total: 0, page, limit, totalPages: 0, data: [] });
227
+ }
228
+ }
229
+ const qb = repo2.createQueryBuilder("order").leftJoinAndSelect("order.contact", "contact").leftJoinAndSelect("order.items", "items").leftJoinAndSelect("items.product", "product").leftJoinAndSelect("product.collection", "collection").orderBy(`order.${sortField}`, sortOrderOrders).skip(skip).take(limit);
230
+ if (search && typeof search === "string" && search.trim()) {
231
+ const term = `%${search.trim()}%`;
232
+ qb.andWhere(
233
+ "(order.orderNumber ILIKE :term OR contact.name ILIKE :term OR contact.email ILIKE :term)",
234
+ { term }
235
+ );
236
+ }
237
+ if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
238
+ if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
239
+ if (dateTo) qb.andWhere("order.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
240
+ if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
241
+ const [rows, total2] = await qb.getManyAndCount();
242
+ const data2 = rows.map((order) => {
243
+ const contact = order.contact;
244
+ const items = order.items ?? [];
245
+ const itemsSummary = items.map((i) => {
246
+ const label = i.product?.collection?.name ?? i.product?.name ?? "Product";
247
+ return `${label} \xD7 ${i.quantity}`;
248
+ }).join(", ") || "\u2014";
249
+ return {
250
+ ...order,
251
+ contact: contact ? { id: contact.id, name: contact.name, email: contact.email, phone: contact.phone } : null,
252
+ itemsSummary
253
+ };
254
+ });
255
+ return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
256
+ }
257
+ if (resource === "payments") {
258
+ const repo2 = dataSource.getRepository(entity);
259
+ const allowedSort = ["id", "orderId", "amount", "currency", "status", "method", "paidAt", "createdAt", "updatedAt"];
260
+ const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
261
+ const sortOrderPayments = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
262
+ const statusFilter = searchParams.get("status")?.trim();
263
+ const dateFrom = searchParams.get("dateFrom")?.trim();
264
+ const dateTo = searchParams.get("dateTo")?.trim();
265
+ const methodFilter = searchParams.get("method")?.trim();
266
+ const orderNumberParam = searchParams.get("orderNumber")?.trim();
267
+ const qb = repo2.createQueryBuilder("payment").leftJoinAndSelect("payment.order", "ord").leftJoinAndSelect("ord.contact", "orderContact").leftJoinAndSelect("payment.contact", "contact").orderBy(`payment.${sortField}`, sortOrderPayments).skip(skip).take(limit);
268
+ if (search && typeof search === "string" && search.trim()) {
269
+ const term = `%${search.trim()}%`;
270
+ qb.andWhere(
271
+ "(orderContact.name ILIKE :term OR orderContact.email ILIKE :term OR contact.name ILIKE :term OR contact.email ILIKE :term)",
272
+ { term }
273
+ );
274
+ }
275
+ if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
276
+ if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
277
+ if (dateTo) qb.andWhere("payment.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
278
+ if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
279
+ if (orderNumberParam) qb.andWhere("ord.orderNumber ILIKE :orderNumber", { orderNumber: `%${orderNumberParam}%` });
280
+ const [rows, total2] = await qb.getManyAndCount();
281
+ const data2 = rows.map((payment) => {
282
+ const order = payment.order;
283
+ const orderContact = order?.contact;
284
+ const contact = payment.contact;
285
+ const customer = orderContact ?? contact;
286
+ return {
287
+ ...payment,
288
+ order: order ? { id: order.id, orderNumber: order.orderNumber, contact: orderContact ? { name: orderContact.name, email: orderContact.email } : null } : null,
289
+ contact: customer ? { id: customer.id, name: customer.name, email: customer.email } : null
290
+ };
291
+ });
292
+ return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
293
+ }
294
+ if (resource === "products") {
295
+ const repo2 = dataSource.getRepository(entity);
296
+ const statusFilter = searchParams.get("status")?.trim();
297
+ const inventory = searchParams.get("inventory")?.trim();
298
+ const productWhere = {};
299
+ if (statusFilter) productWhere.status = statusFilter;
300
+ if (inventory === "in_stock") productWhere.quantity = (0, import_typeorm.MoreThan)(0);
301
+ if (inventory === "out_of_stock") productWhere.quantity = 0;
302
+ if (search && typeof search === "string" && search.trim()) {
303
+ productWhere.name = (0, import_typeorm.ILike)(`%${search.trim()}%`);
304
+ }
305
+ const [data2, total2] = await repo2.findAndCount({
306
+ where: Object.keys(productWhere).length ? productWhere : void 0,
307
+ skip,
308
+ take: limit,
309
+ order: { [sortFieldRaw]: sortOrder }
310
+ });
311
+ return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
312
+ }
313
+ if (resource === "contacts") {
314
+ const repo2 = dataSource.getRepository(entity);
315
+ const allowedSort = ["id", "name", "email", "createdAt", "type"];
316
+ const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
317
+ const sortOrderContacts = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
318
+ const typeFilter2 = searchParams.get("type")?.trim();
319
+ const orderIdParam = searchParams.get("orderId")?.trim();
320
+ const includeSummary = searchParams.get("includeSummary") === "1";
321
+ const qb = repo2.createQueryBuilder("contact").orderBy(`contact.${sortField}`, sortOrderContacts).skip(skip).take(limit);
322
+ if (search && typeof search === "string" && search.trim()) {
323
+ const term = `%${search.trim()}%`;
324
+ qb.andWhere("(contact.name ILIKE :term OR contact.email ILIKE :term OR contact.phone ILIKE :term)", { term });
325
+ }
326
+ if (typeFilter2) qb.andWhere("contact.type = :type", { type: typeFilter2 });
327
+ if (orderIdParam) {
328
+ const orderId = Number(orderIdParam);
329
+ if (!Number.isNaN(orderId)) {
330
+ qb.andWhere('contact.id IN (SELECT "contactId" FROM orders WHERE id = :orderId)', { orderId });
331
+ }
332
+ }
333
+ if (includeSummary && entityMap["orders"] && entityMap["payments"]) {
334
+ qb.loadRelationCountAndMap("contact._orderCount", "contact.orders");
335
+ const [rows, total3] = await qb.getManyAndCount();
336
+ const contactIds = rows.map((c) => c.id);
337
+ const paymentRepo = dataSource.getRepository(entityMap["payments"]);
338
+ const paidByContact = await paymentRepo.createQueryBuilder("p").select("p.contactId", "contactId").addSelect("COALESCE(SUM(CAST(p.amount AS DECIMAL)), 0)", "total").where("p.contactId IN (:...ids)", { ids: contactIds.length ? contactIds : [0] }).andWhere("p.status = :status", { status: "completed" }).groupBy("p.contactId").getRawMany();
339
+ const totalPaidMap = new Map(paidByContact.map((r) => [r.contactId, Number(r.total)]));
340
+ const data3 = rows.map((c) => {
341
+ const { _orderCount, ...rest } = c;
342
+ return {
343
+ ...rest,
344
+ orderCount: _orderCount ?? 0,
345
+ totalPaid: totalPaidMap.get(rest.id) ?? 0
346
+ };
347
+ });
348
+ return json({ total: total3, page, limit, totalPages: Math.ceil(total3 / limit), data: data3 });
349
+ }
350
+ const [data2, total2] = await qb.getManyAndCount();
351
+ return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
352
+ }
353
+ const repo = dataSource.getRepository(entity);
212
354
  const typeFilter = searchParams.get("type");
213
355
  let where = {};
214
356
  if (resource === "media") {
@@ -222,7 +364,7 @@ function createCrudHandler(dataSource, entityMap, options) {
222
364
  const [data, total] = await repo.findAndCount({
223
365
  skip,
224
366
  take: limit,
225
- order: { [sortField]: sortOrder },
367
+ order: { [sortFieldRaw]: sortOrder },
226
368
  where
227
369
  });
228
370
  return json({ total, page, limit, totalPages: Math.ceil(total / limit), data });
@@ -242,6 +384,107 @@ function createCrudHandler(dataSource, entityMap, options) {
242
384
  sanitizeBodyForEntity(repo, body);
243
385
  const created = await repo.save(repo.create(body));
244
386
  return json(created, { status: 201 });
387
+ },
388
+ async GET_METADATA(req, resource) {
389
+ const authError = await requireAuth(req);
390
+ if (authError) return authError;
391
+ const entity = entityMap[resource];
392
+ if (!resource || !entity) {
393
+ return json({ error: "Invalid resource" }, { status: 400 });
394
+ }
395
+ const repo = dataSource.getRepository(entity);
396
+ const meta = repo.metadata;
397
+ const uniqueFromIndices = /* @__PURE__ */ new Set();
398
+ for (const idx of meta.indices) {
399
+ if (idx.isUnique && idx.columns.length === 1) {
400
+ uniqueFromIndices.add(idx.columns[0].propertyName);
401
+ }
402
+ }
403
+ for (const uniq of meta.uniques) {
404
+ if (uniq.columns.length === 1) {
405
+ uniqueFromIndices.add(uniq.columns[0].propertyName);
406
+ }
407
+ }
408
+ const columns = meta.columns.map((col) => ({
409
+ name: col.propertyName,
410
+ type: typeof col.type === "string" ? col.type : col.type?.name ?? "unknown",
411
+ nullable: col.isNullable,
412
+ isUnique: uniqueFromIndices.has(col.propertyName),
413
+ isPrimary: col.isPrimary,
414
+ default: col.default
415
+ }));
416
+ const uniqueColumns = [...uniqueFromIndices];
417
+ return json({ columns, uniqueColumns });
418
+ },
419
+ async BULK_POST(req, resource) {
420
+ const authError = await requireAuth(req);
421
+ if (authError) return authError;
422
+ const entity = entityMap[resource];
423
+ if (!resource || !entity) {
424
+ return json({ error: "Invalid resource" }, { status: 400 });
425
+ }
426
+ const body = await req.json();
427
+ const { records, upsertKey = "id" } = body;
428
+ if (!Array.isArray(records) || records.length === 0) {
429
+ return json({ error: "Records array is required" }, { status: 400 });
430
+ }
431
+ const repo = dataSource.getRepository(entity);
432
+ for (const record of records) {
433
+ sanitizeBodyForEntity(repo, record);
434
+ }
435
+ try {
436
+ const result = await repo.upsert(records, {
437
+ conflictPaths: [upsertKey],
438
+ skipUpdateIfNoValuesChanged: true
439
+ });
440
+ return json({
441
+ success: true,
442
+ imported: result.identifiers.length,
443
+ identifiers: result.identifiers
444
+ });
445
+ } catch (error) {
446
+ const message = error instanceof Error ? error.message : "Bulk import failed";
447
+ return json({ error: message }, { status: 400 });
448
+ }
449
+ },
450
+ async GET_EXPORT(req, resource) {
451
+ const authError = await requireAuth(req);
452
+ if (authError) return authError;
453
+ const entity = entityMap[resource];
454
+ if (!resource || !entity) {
455
+ return json({ error: "Invalid resource" }, { status: 400 });
456
+ }
457
+ const { searchParams } = new URL(req.url);
458
+ const format = searchParams.get("format") || "csv";
459
+ const repo = dataSource.getRepository(entity);
460
+ const meta = repo.metadata;
461
+ const hasDeleted = meta.columns.some((c) => c.propertyName === "deleted");
462
+ const where = hasDeleted ? { deleted: false } : {};
463
+ const data = await repo.find({ where });
464
+ const excludeCols = /* @__PURE__ */ new Set(["deletedAt", "deletedBy", "deleted"]);
465
+ const columns = meta.columns.filter((c) => !excludeCols.has(c.propertyName)).map((c) => c.propertyName);
466
+ if (format === "json") {
467
+ return json(data);
468
+ }
469
+ const escapeCSV = (val) => {
470
+ if (val === null || val === void 0) return "";
471
+ const str = typeof val === "object" ? JSON.stringify(val) : String(val);
472
+ if (str.includes(",") || str.includes('"') || str.includes("\n")) {
473
+ return `"${str.replace(/"/g, '""')}"`;
474
+ }
475
+ return str;
476
+ };
477
+ const header = columns.join(",");
478
+ const rows = data.map(
479
+ (row) => columns.map((col) => escapeCSV(row[col])).join(",")
480
+ );
481
+ const csv = [header, ...rows].join("\n");
482
+ return new Response(csv, {
483
+ headers: {
484
+ "Content-Type": "text/csv; charset=utf-8",
485
+ "Content-Disposition": `attachment; filename="${resource}.csv"`
486
+ }
487
+ });
245
488
  }
246
489
  };
247
490
  }
@@ -254,6 +497,44 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
254
497
  const entity = entityMap[resource];
255
498
  if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
256
499
  const repo = dataSource.getRepository(entity);
500
+ if (resource === "orders") {
501
+ const order = await repo.findOne({
502
+ where: { id: Number(id) },
503
+ relations: ["contact", "billingAddress", "shippingAddress", "items", "items.product", "items.product.collection", "payments"]
504
+ });
505
+ if (!order) return json({ message: "Not found" }, { status: 404 });
506
+ return json(order);
507
+ }
508
+ if (resource === "contacts") {
509
+ const contact = await repo.findOne({
510
+ where: { id: Number(id) },
511
+ relations: ["form_submissions", "form_submissions.form", "orders", "payments", "addresses"]
512
+ });
513
+ if (!contact) return json({ message: "Not found" }, { status: 404 });
514
+ const orders = contact.orders ?? [];
515
+ const payments = contact.payments ?? [];
516
+ const totalPaid = payments.filter((p) => p.status === "completed").reduce((sum, p) => sum + Number(p.amount ?? 0), 0);
517
+ const lastOrderAt = orders.length > 0 ? orders.reduce((latest, o) => {
518
+ const t = o.createdAt ? new Date(o.createdAt).getTime() : 0;
519
+ return t > latest ? t : latest;
520
+ }, 0) : null;
521
+ return json({
522
+ ...contact,
523
+ summary: {
524
+ totalOrders: orders.length,
525
+ totalPaid,
526
+ lastOrderAt: lastOrderAt ? new Date(lastOrderAt).toISOString() : null
527
+ }
528
+ });
529
+ }
530
+ if (resource === "payments") {
531
+ const payment = await repo.findOne({
532
+ where: { id: Number(id) },
533
+ relations: ["order", "order.contact", "contact"]
534
+ });
535
+ if (!payment) return json({ message: "Not found" }, { status: 404 });
536
+ return json(payment);
537
+ }
257
538
  const item = await repo.findOne({ where: { id: Number(id) } });
258
539
  return item ? json(item) : json({ message: "Not found" }, { status: 404 });
259
540
  },
@@ -1119,6 +1400,17 @@ function createCmsApiHandler(config) {
1119
1400
  if (path.length === 0) return config.json({ error: "Not found" }, { status: 404 });
1120
1401
  const resource = resolveResource(path[0]);
1121
1402
  if (!crudResources.includes(resource)) return config.json({ error: "Invalid resource" }, { status: 400 });
1403
+ if (path.length === 2) {
1404
+ if (path[1] === "metadata" && method === "GET") {
1405
+ return crud.GET_METADATA(req, resource);
1406
+ }
1407
+ if (path[1] === "bulk" && method === "POST") {
1408
+ return crud.BULK_POST(req, resource);
1409
+ }
1410
+ if (path[1] === "export" && method === "GET") {
1411
+ return crud.GET_EXPORT(req, resource);
1412
+ }
1413
+ }
1122
1414
  if (path.length === 1) {
1123
1415
  if (method === "GET") return crud.GET(req, resource);
1124
1416
  if (method === "POST") return crud.POST(req, resource);