@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.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { A as AnalyticsHandlerConfig, a as AuthHandlersConfig, B as BlogBySlugConfig, C as ChangePasswordConfig, b as CmsApiHandlerConfig, c as CmsGetter, d as CrudHandlerOptions, D as DashboardStatsConfig, E as EntityMap, F as ForgotPasswordConfig, e as FormBySlugConfig, I as InviteAcceptConfig, f as SetPasswordConfig, g as SettingsApiConfig, U as UploadHandlerConfig, h as UserAuthApiConfig, i as UserAvatarConfig, j as UserProfileConfig, k as UsersApiConfig, l as createAnalyticsHandlers, m as createBlogBySlugHandler, n as createChangePasswordHandler, o as createCmsApiHandler, p as createCrudByIdHandler, q as createCrudHandler, r as createDashboardStatsHandler, s as createForgotPasswordHandler, t as createFormBySlugHandler, u as createInviteAcceptHandler, v as createSetPasswordHandler, w as createSettingsApiHandlers, x as createUploadHandler, y as createUserAuthApiRouter, z as createUserAvatarHandler, G as createUserProfileHandler, H as createUsersApiHandlers } from './index-DP3LK1XN.js';
1
+ export { A as AnalyticsHandlerConfig, a as AuthHandlersConfig, B as BlogBySlugConfig, C as ChangePasswordConfig, b as CmsApiHandlerConfig, c as CmsGetter, d as CrudHandlerOptions, D as DashboardStatsConfig, E as EntityMap, F as ForgotPasswordConfig, e as FormBySlugConfig, I as InviteAcceptConfig, f as SetPasswordConfig, g as SettingsApiConfig, U as UploadHandlerConfig, h as UserAuthApiConfig, i as UserAvatarConfig, j as UserProfileConfig, k as UsersApiConfig, l as createAnalyticsHandlers, m as createBlogBySlugHandler, n as createChangePasswordHandler, o as createCmsApiHandler, p as createCrudByIdHandler, q as createCrudHandler, r as createDashboardStatsHandler, s as createForgotPasswordHandler, t as createFormBySlugHandler, u as createInviteAcceptHandler, v as createSetPasswordHandler, w as createSettingsApiHandlers, x as createUploadHandler, y as createUserAuthApiRouter, z as createUserAvatarHandler, G as createUserProfileHandler, H as createUsersApiHandlers } from './index-BPnATEXW.js';
2
2
  import 'typeorm';
package/dist/api.js CHANGED
@@ -111,7 +111,7 @@ This link expires in 1 hour.`
111
111
  });
112
112
 
113
113
  // src/api/crud.ts
114
- import { ILike, Like } from "typeorm";
114
+ import { ILike, Like, MoreThan } from "typeorm";
115
115
  var DATE_COLUMN_TYPES = /* @__PURE__ */ new Set([
116
116
  "date",
117
117
  "datetime",
@@ -156,14 +156,156 @@ function createCrudHandler(dataSource, entityMap, options) {
156
156
  if (!resource || !entity) {
157
157
  return json({ error: "Invalid resource" }, { status: 400 });
158
158
  }
159
- const repo = dataSource.getRepository(entity);
160
159
  const { searchParams } = new URL(req.url);
161
160
  const page = Number(searchParams.get("page")) || 1;
162
- const limit = Number(searchParams.get("limit")) || 10;
161
+ const limit = Math.min(Number(searchParams.get("limit")) || 10, 100);
163
162
  const skip = (page - 1) * limit;
164
- const sortField = searchParams.get("sortField") || "createdAt";
163
+ const sortFieldRaw = searchParams.get("sortField") || "createdAt";
165
164
  const sortOrder = searchParams.get("sortOrder") === "desc" ? "DESC" : "ASC";
166
165
  const search = searchParams.get("search");
166
+ if (resource === "orders") {
167
+ const repo2 = dataSource.getRepository(entity);
168
+ const allowedSort = ["id", "orderNumber", "contactId", "status", "total", "currency", "createdAt", "updatedAt"];
169
+ const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
170
+ const sortOrderOrders = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
171
+ const statusFilter = searchParams.get("status")?.trim();
172
+ const dateFrom = searchParams.get("dateFrom")?.trim();
173
+ const dateTo = searchParams.get("dateTo")?.trim();
174
+ const paymentRef = searchParams.get("paymentRef")?.trim();
175
+ let orderIdsFromPayment = null;
176
+ if (paymentRef && entityMap["payments"]) {
177
+ const paymentRepo = dataSource.getRepository(entityMap["payments"]);
178
+ const payments = await paymentRepo.createQueryBuilder("p").select("p.orderId").where("p.externalReference = :ref", { ref: paymentRef }).orWhere("p.metadata->>'razorpayPaymentId' = :ref", { ref: paymentRef }).getRawMany();
179
+ orderIdsFromPayment = payments.map((r) => r.orderId);
180
+ if (orderIdsFromPayment.length === 0) {
181
+ return json({ total: 0, page, limit, totalPages: 0, data: [] });
182
+ }
183
+ }
184
+ 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);
185
+ if (search && typeof search === "string" && search.trim()) {
186
+ const term = `%${search.trim()}%`;
187
+ qb.andWhere(
188
+ "(order.orderNumber ILIKE :term OR contact.name ILIKE :term OR contact.email ILIKE :term)",
189
+ { term }
190
+ );
191
+ }
192
+ if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
193
+ if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
194
+ if (dateTo) qb.andWhere("order.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
195
+ if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
196
+ const [rows, total2] = await qb.getManyAndCount();
197
+ const data2 = rows.map((order) => {
198
+ const contact = order.contact;
199
+ const items = order.items ?? [];
200
+ const itemsSummary = items.map((i) => {
201
+ const label = i.product?.collection?.name ?? i.product?.name ?? "Product";
202
+ return `${label} \xD7 ${i.quantity}`;
203
+ }).join(", ") || "\u2014";
204
+ return {
205
+ ...order,
206
+ contact: contact ? { id: contact.id, name: contact.name, email: contact.email, phone: contact.phone } : null,
207
+ itemsSummary
208
+ };
209
+ });
210
+ return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
211
+ }
212
+ if (resource === "payments") {
213
+ const repo2 = dataSource.getRepository(entity);
214
+ const allowedSort = ["id", "orderId", "amount", "currency", "status", "method", "paidAt", "createdAt", "updatedAt"];
215
+ const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
216
+ const sortOrderPayments = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
217
+ const statusFilter = searchParams.get("status")?.trim();
218
+ const dateFrom = searchParams.get("dateFrom")?.trim();
219
+ const dateTo = searchParams.get("dateTo")?.trim();
220
+ const methodFilter = searchParams.get("method")?.trim();
221
+ const orderNumberParam = searchParams.get("orderNumber")?.trim();
222
+ 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);
223
+ if (search && typeof search === "string" && search.trim()) {
224
+ const term = `%${search.trim()}%`;
225
+ qb.andWhere(
226
+ "(orderContact.name ILIKE :term OR orderContact.email ILIKE :term OR contact.name ILIKE :term OR contact.email ILIKE :term)",
227
+ { term }
228
+ );
229
+ }
230
+ if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
231
+ if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
232
+ if (dateTo) qb.andWhere("payment.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
233
+ if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
234
+ if (orderNumberParam) qb.andWhere("ord.orderNumber ILIKE :orderNumber", { orderNumber: `%${orderNumberParam}%` });
235
+ const [rows, total2] = await qb.getManyAndCount();
236
+ const data2 = rows.map((payment) => {
237
+ const order = payment.order;
238
+ const orderContact = order?.contact;
239
+ const contact = payment.contact;
240
+ const customer = orderContact ?? contact;
241
+ return {
242
+ ...payment,
243
+ order: order ? { id: order.id, orderNumber: order.orderNumber, contact: orderContact ? { name: orderContact.name, email: orderContact.email } : null } : null,
244
+ contact: customer ? { id: customer.id, name: customer.name, email: customer.email } : null
245
+ };
246
+ });
247
+ return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
248
+ }
249
+ if (resource === "products") {
250
+ const repo2 = dataSource.getRepository(entity);
251
+ const statusFilter = searchParams.get("status")?.trim();
252
+ const inventory = searchParams.get("inventory")?.trim();
253
+ const productWhere = {};
254
+ if (statusFilter) productWhere.status = statusFilter;
255
+ if (inventory === "in_stock") productWhere.quantity = MoreThan(0);
256
+ if (inventory === "out_of_stock") productWhere.quantity = 0;
257
+ if (search && typeof search === "string" && search.trim()) {
258
+ productWhere.name = ILike(`%${search.trim()}%`);
259
+ }
260
+ const [data2, total2] = await repo2.findAndCount({
261
+ where: Object.keys(productWhere).length ? productWhere : void 0,
262
+ skip,
263
+ take: limit,
264
+ order: { [sortFieldRaw]: sortOrder }
265
+ });
266
+ return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
267
+ }
268
+ if (resource === "contacts") {
269
+ const repo2 = dataSource.getRepository(entity);
270
+ const allowedSort = ["id", "name", "email", "createdAt", "type"];
271
+ const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
272
+ const sortOrderContacts = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
273
+ const typeFilter2 = searchParams.get("type")?.trim();
274
+ const orderIdParam = searchParams.get("orderId")?.trim();
275
+ const includeSummary = searchParams.get("includeSummary") === "1";
276
+ const qb = repo2.createQueryBuilder("contact").orderBy(`contact.${sortField}`, sortOrderContacts).skip(skip).take(limit);
277
+ if (search && typeof search === "string" && search.trim()) {
278
+ const term = `%${search.trim()}%`;
279
+ qb.andWhere("(contact.name ILIKE :term OR contact.email ILIKE :term OR contact.phone ILIKE :term)", { term });
280
+ }
281
+ if (typeFilter2) qb.andWhere("contact.type = :type", { type: typeFilter2 });
282
+ if (orderIdParam) {
283
+ const orderId = Number(orderIdParam);
284
+ if (!Number.isNaN(orderId)) {
285
+ qb.andWhere('contact.id IN (SELECT "contactId" FROM orders WHERE id = :orderId)', { orderId });
286
+ }
287
+ }
288
+ if (includeSummary && entityMap["orders"] && entityMap["payments"]) {
289
+ qb.loadRelationCountAndMap("contact._orderCount", "contact.orders");
290
+ const [rows, total3] = await qb.getManyAndCount();
291
+ const contactIds = rows.map((c) => c.id);
292
+ const paymentRepo = dataSource.getRepository(entityMap["payments"]);
293
+ 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();
294
+ const totalPaidMap = new Map(paidByContact.map((r) => [r.contactId, Number(r.total)]));
295
+ const data3 = rows.map((c) => {
296
+ const { _orderCount, ...rest } = c;
297
+ return {
298
+ ...rest,
299
+ orderCount: _orderCount ?? 0,
300
+ totalPaid: totalPaidMap.get(rest.id) ?? 0
301
+ };
302
+ });
303
+ return json({ total: total3, page, limit, totalPages: Math.ceil(total3 / limit), data: data3 });
304
+ }
305
+ const [data2, total2] = await qb.getManyAndCount();
306
+ return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
307
+ }
308
+ const repo = dataSource.getRepository(entity);
167
309
  const typeFilter = searchParams.get("type");
168
310
  let where = {};
169
311
  if (resource === "media") {
@@ -177,7 +319,7 @@ function createCrudHandler(dataSource, entityMap, options) {
177
319
  const [data, total] = await repo.findAndCount({
178
320
  skip,
179
321
  take: limit,
180
- order: { [sortField]: sortOrder },
322
+ order: { [sortFieldRaw]: sortOrder },
181
323
  where
182
324
  });
183
325
  return json({ total, page, limit, totalPages: Math.ceil(total / limit), data });
@@ -197,6 +339,107 @@ function createCrudHandler(dataSource, entityMap, options) {
197
339
  sanitizeBodyForEntity(repo, body);
198
340
  const created = await repo.save(repo.create(body));
199
341
  return json(created, { status: 201 });
342
+ },
343
+ async GET_METADATA(req, resource) {
344
+ const authError = await requireAuth(req);
345
+ if (authError) return authError;
346
+ const entity = entityMap[resource];
347
+ if (!resource || !entity) {
348
+ return json({ error: "Invalid resource" }, { status: 400 });
349
+ }
350
+ const repo = dataSource.getRepository(entity);
351
+ const meta = repo.metadata;
352
+ const uniqueFromIndices = /* @__PURE__ */ new Set();
353
+ for (const idx of meta.indices) {
354
+ if (idx.isUnique && idx.columns.length === 1) {
355
+ uniqueFromIndices.add(idx.columns[0].propertyName);
356
+ }
357
+ }
358
+ for (const uniq of meta.uniques) {
359
+ if (uniq.columns.length === 1) {
360
+ uniqueFromIndices.add(uniq.columns[0].propertyName);
361
+ }
362
+ }
363
+ const columns = meta.columns.map((col) => ({
364
+ name: col.propertyName,
365
+ type: typeof col.type === "string" ? col.type : col.type?.name ?? "unknown",
366
+ nullable: col.isNullable,
367
+ isUnique: uniqueFromIndices.has(col.propertyName),
368
+ isPrimary: col.isPrimary,
369
+ default: col.default
370
+ }));
371
+ const uniqueColumns = [...uniqueFromIndices];
372
+ return json({ columns, uniqueColumns });
373
+ },
374
+ async BULK_POST(req, resource) {
375
+ const authError = await requireAuth(req);
376
+ if (authError) return authError;
377
+ const entity = entityMap[resource];
378
+ if (!resource || !entity) {
379
+ return json({ error: "Invalid resource" }, { status: 400 });
380
+ }
381
+ const body = await req.json();
382
+ const { records, upsertKey = "id" } = body;
383
+ if (!Array.isArray(records) || records.length === 0) {
384
+ return json({ error: "Records array is required" }, { status: 400 });
385
+ }
386
+ const repo = dataSource.getRepository(entity);
387
+ for (const record of records) {
388
+ sanitizeBodyForEntity(repo, record);
389
+ }
390
+ try {
391
+ const result = await repo.upsert(records, {
392
+ conflictPaths: [upsertKey],
393
+ skipUpdateIfNoValuesChanged: true
394
+ });
395
+ return json({
396
+ success: true,
397
+ imported: result.identifiers.length,
398
+ identifiers: result.identifiers
399
+ });
400
+ } catch (error) {
401
+ const message = error instanceof Error ? error.message : "Bulk import failed";
402
+ return json({ error: message }, { status: 400 });
403
+ }
404
+ },
405
+ async GET_EXPORT(req, resource) {
406
+ const authError = await requireAuth(req);
407
+ if (authError) return authError;
408
+ const entity = entityMap[resource];
409
+ if (!resource || !entity) {
410
+ return json({ error: "Invalid resource" }, { status: 400 });
411
+ }
412
+ const { searchParams } = new URL(req.url);
413
+ const format = searchParams.get("format") || "csv";
414
+ const repo = dataSource.getRepository(entity);
415
+ const meta = repo.metadata;
416
+ const hasDeleted = meta.columns.some((c) => c.propertyName === "deleted");
417
+ const where = hasDeleted ? { deleted: false } : {};
418
+ const data = await repo.find({ where });
419
+ const excludeCols = /* @__PURE__ */ new Set(["deletedAt", "deletedBy", "deleted"]);
420
+ const columns = meta.columns.filter((c) => !excludeCols.has(c.propertyName)).map((c) => c.propertyName);
421
+ if (format === "json") {
422
+ return json(data);
423
+ }
424
+ const escapeCSV = (val) => {
425
+ if (val === null || val === void 0) return "";
426
+ const str = typeof val === "object" ? JSON.stringify(val) : String(val);
427
+ if (str.includes(",") || str.includes('"') || str.includes("\n")) {
428
+ return `"${str.replace(/"/g, '""')}"`;
429
+ }
430
+ return str;
431
+ };
432
+ const header = columns.join(",");
433
+ const rows = data.map(
434
+ (row) => columns.map((col) => escapeCSV(row[col])).join(",")
435
+ );
436
+ const csv = [header, ...rows].join("\n");
437
+ return new Response(csv, {
438
+ headers: {
439
+ "Content-Type": "text/csv; charset=utf-8",
440
+ "Content-Disposition": `attachment; filename="${resource}.csv"`
441
+ }
442
+ });
200
443
  }
201
444
  };
202
445
  }
@@ -209,6 +452,44 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
209
452
  const entity = entityMap[resource];
210
453
  if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
211
454
  const repo = dataSource.getRepository(entity);
455
+ if (resource === "orders") {
456
+ const order = await repo.findOne({
457
+ where: { id: Number(id) },
458
+ relations: ["contact", "billingAddress", "shippingAddress", "items", "items.product", "items.product.collection", "payments"]
459
+ });
460
+ if (!order) return json({ message: "Not found" }, { status: 404 });
461
+ return json(order);
462
+ }
463
+ if (resource === "contacts") {
464
+ const contact = await repo.findOne({
465
+ where: { id: Number(id) },
466
+ relations: ["form_submissions", "form_submissions.form", "orders", "payments", "addresses"]
467
+ });
468
+ if (!contact) return json({ message: "Not found" }, { status: 404 });
469
+ const orders = contact.orders ?? [];
470
+ const payments = contact.payments ?? [];
471
+ const totalPaid = payments.filter((p) => p.status === "completed").reduce((sum, p) => sum + Number(p.amount ?? 0), 0);
472
+ const lastOrderAt = orders.length > 0 ? orders.reduce((latest, o) => {
473
+ const t = o.createdAt ? new Date(o.createdAt).getTime() : 0;
474
+ return t > latest ? t : latest;
475
+ }, 0) : null;
476
+ return json({
477
+ ...contact,
478
+ summary: {
479
+ totalOrders: orders.length,
480
+ totalPaid,
481
+ lastOrderAt: lastOrderAt ? new Date(lastOrderAt).toISOString() : null
482
+ }
483
+ });
484
+ }
485
+ if (resource === "payments") {
486
+ const payment = await repo.findOne({
487
+ where: { id: Number(id) },
488
+ relations: ["order", "order.contact", "contact"]
489
+ });
490
+ if (!payment) return json({ message: "Not found" }, { status: 404 });
491
+ return json(payment);
492
+ }
212
493
  const item = await repo.findOne({ where: { id: Number(id) } });
213
494
  return item ? json(item) : json({ message: "Not found" }, { status: 404 });
214
495
  },
@@ -1074,6 +1355,17 @@ function createCmsApiHandler(config) {
1074
1355
  if (path.length === 0) return config.json({ error: "Not found" }, { status: 404 });
1075
1356
  const resource = resolveResource(path[0]);
1076
1357
  if (!crudResources.includes(resource)) return config.json({ error: "Invalid resource" }, { status: 400 });
1358
+ if (path.length === 2) {
1359
+ if (path[1] === "metadata" && method === "GET") {
1360
+ return crud.GET_METADATA(req, resource);
1361
+ }
1362
+ if (path[1] === "bulk" && method === "POST") {
1363
+ return crud.BULK_POST(req, resource);
1364
+ }
1365
+ if (path[1] === "export" && method === "GET") {
1366
+ return crud.GET_EXPORT(req, resource);
1367
+ }
1368
+ }
1077
1369
  if (path.length === 1) {
1078
1370
  if (method === "GET") return crud.GET(req, resource);
1079
1371
  if (method === "POST") return crud.POST(req, resource);