@voyantjs/products 0.3.1 → 0.4.0

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.
Files changed (52) hide show
  1. package/dist/index.d.ts +6 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5 -1
  4. package/dist/routes-public.d.ts +167 -12
  5. package/dist/routes-public.d.ts.map +1 -1
  6. package/dist/routes-public.js +13 -1
  7. package/dist/routes.d.ts +669 -0
  8. package/dist/routes.d.ts.map +1 -1
  9. package/dist/routes.js +117 -1
  10. package/dist/schema-itinerary.d.ts +51 -0
  11. package/dist/schema-itinerary.d.ts.map +1 -1
  12. package/dist/schema-itinerary.js +3 -0
  13. package/dist/schema-relations.d.ts +14 -0
  14. package/dist/schema-relations.d.ts.map +1 -1
  15. package/dist/schema-relations.js +28 -1
  16. package/dist/schema-taxonomy.d.ts +435 -0
  17. package/dist/schema-taxonomy.d.ts.map +1 -1
  18. package/dist/schema-taxonomy.js +47 -0
  19. package/dist/service-catalog.d.ts +237 -0
  20. package/dist/service-catalog.d.ts.map +1 -0
  21. package/dist/service-catalog.js +478 -0
  22. package/dist/service-public.d.ts +136 -12
  23. package/dist/service-public.d.ts.map +1 -1
  24. package/dist/service-public.js +146 -260
  25. package/dist/service.d.ts +292 -1
  26. package/dist/service.d.ts.map +1 -1
  27. package/dist/service.js +388 -2
  28. package/dist/tasks/brochure-printers.d.ts +29 -0
  29. package/dist/tasks/brochure-printers.d.ts.map +1 -0
  30. package/dist/tasks/brochure-printers.js +94 -0
  31. package/dist/tasks/brochure-templates.d.ts +36 -0
  32. package/dist/tasks/brochure-templates.d.ts.map +1 -0
  33. package/dist/tasks/brochure-templates.js +98 -0
  34. package/dist/tasks/brochures.d.ts +42 -0
  35. package/dist/tasks/brochures.d.ts.map +1 -0
  36. package/dist/tasks/brochures.js +69 -0
  37. package/dist/tasks/index.d.ts +3 -0
  38. package/dist/tasks/index.d.ts.map +1 -1
  39. package/dist/tasks/index.js +3 -0
  40. package/dist/validation-catalog.d.ts +388 -0
  41. package/dist/validation-catalog.d.ts.map +1 -0
  42. package/dist/validation-catalog.js +54 -0
  43. package/dist/validation-content.d.ts +109 -0
  44. package/dist/validation-content.d.ts.map +1 -1
  45. package/dist/validation-content.js +63 -1
  46. package/dist/validation-public.d.ts +208 -19
  47. package/dist/validation-public.d.ts.map +1 -1
  48. package/dist/validation-public.js +39 -2
  49. package/dist/validation-shared.d.ts +6 -0
  50. package/dist/validation-shared.d.ts.map +1 -1
  51. package/dist/validation-shared.js +1 -0
  52. package/package.json +6 -4
package/dist/service.js CHANGED
@@ -1,5 +1,5 @@
1
- import { and, asc, desc, eq, ilike, or, sql } from "drizzle-orm";
2
- import { optionUnits, optionUnitTranslations, productActivationSettings, productCapabilities, productCategories, productCategoryProducts, productDayServices, productDays, productDeliveryFormats, productFaqs, productFeatures, productLocations, productMedia, productNotes, productOptions, productOptionTranslations, products, productTagProducts, productTags, productTicketSettings, productTranslations, productTypes, productVersions, productVisibilitySettings, } from "./schema.js";
1
+ import { and, asc, desc, eq, ilike, inArray, or, sql } from "drizzle-orm";
2
+ import { destinations, destinationTranslations, optionUnits, optionUnitTranslations, productActivationSettings, productCapabilities, productCategories, productCategoryProducts, productDayServices, productDays, productDeliveryFormats, productDestinations, productFaqs, productFeatures, productLocations, productMedia, productNotes, productOptions, productOptionTranslations, products, productTagProducts, productTags, productTicketSettings, productTranslations, productTypes, productVersions, productVisibilitySettings, } from "./schema.js";
3
3
  async function recalculateProductCost(db, productId) {
4
4
  const [result] = await db
5
5
  .select({
@@ -650,6 +650,247 @@ export const productsService = {
650
650
  .returning({ id: productLocations.id });
651
651
  return row ?? null;
652
652
  },
653
+ async listDestinations(db, query) {
654
+ const conditions = [];
655
+ if (query.parentId) {
656
+ conditions.push(eq(destinations.parentId, query.parentId));
657
+ }
658
+ if (query.active !== undefined) {
659
+ conditions.push(eq(destinations.active, query.active));
660
+ }
661
+ if (query.search) {
662
+ const term = `%${query.search}%`;
663
+ const translationRows = await db
664
+ .select({ destinationId: destinationTranslations.destinationId })
665
+ .from(destinationTranslations)
666
+ .where(and(query.languageTag
667
+ ? eq(destinationTranslations.languageTag, query.languageTag)
668
+ : undefined, or(ilike(destinationTranslations.name, term), ilike(destinationTranslations.description, term))));
669
+ const destinationIds = translationRows.map((row) => row.destinationId);
670
+ conditions.push(destinationIds.length > 0 ? inArray(destinations.id, destinationIds) : sql `1 = 0`);
671
+ }
672
+ const where = conditions.length > 0 ? and(...conditions) : undefined;
673
+ const [rows, countResult] = await Promise.all([
674
+ db
675
+ .select()
676
+ .from(destinations)
677
+ .where(where)
678
+ .limit(query.limit)
679
+ .offset(query.offset)
680
+ .orderBy(asc(destinations.sortOrder), asc(destinations.slug)),
681
+ db.select({ count: sql `count(*)::int` }).from(destinations).where(where),
682
+ ]);
683
+ const destinationIds = rows.map((row) => row.id);
684
+ const translations = destinationIds.length > 0
685
+ ? await db
686
+ .select()
687
+ .from(destinationTranslations)
688
+ .where(and(inArray(destinationTranslations.destinationId, destinationIds), ...(query.languageTag
689
+ ? [eq(destinationTranslations.languageTag, query.languageTag)]
690
+ : [])))
691
+ .orderBy(asc(destinationTranslations.languageTag), asc(destinationTranslations.createdAt))
692
+ : [];
693
+ const translationsByDestination = new Map();
694
+ for (const row of translations) {
695
+ const existing = translationsByDestination.get(row.destinationId) ?? [];
696
+ existing.push(row);
697
+ translationsByDestination.set(row.destinationId, existing);
698
+ }
699
+ return {
700
+ data: rows.map((row) => ({
701
+ ...row,
702
+ translation: (translationsByDestination.get(row.id) ?? [])[0] ?? null,
703
+ })),
704
+ total: countResult[0]?.count ?? 0,
705
+ limit: query.limit,
706
+ offset: query.offset,
707
+ };
708
+ },
709
+ async getDestinationById(db, id) {
710
+ const [row] = await db.select().from(destinations).where(eq(destinations.id, id)).limit(1);
711
+ if (!row) {
712
+ return null;
713
+ }
714
+ const translations = await db
715
+ .select()
716
+ .from(destinationTranslations)
717
+ .where(eq(destinationTranslations.destinationId, id))
718
+ .orderBy(asc(destinationTranslations.languageTag), asc(destinationTranslations.createdAt));
719
+ return {
720
+ ...row,
721
+ translations,
722
+ };
723
+ },
724
+ async createDestination(db, data) {
725
+ const [row] = await db.insert(destinations).values(data).returning();
726
+ return row ?? null;
727
+ },
728
+ async updateDestination(db, id, data) {
729
+ const [row] = await db
730
+ .update(destinations)
731
+ .set({ ...data, updatedAt: new Date() })
732
+ .where(eq(destinations.id, id))
733
+ .returning();
734
+ return row ?? null;
735
+ },
736
+ async deleteDestination(db, id) {
737
+ const [row] = await db
738
+ .delete(destinations)
739
+ .where(eq(destinations.id, id))
740
+ .returning({ id: destinations.id });
741
+ return row ?? null;
742
+ },
743
+ async listDestinationTranslations(db, query) {
744
+ const conditions = [];
745
+ if (query.destinationId) {
746
+ conditions.push(eq(destinationTranslations.destinationId, query.destinationId));
747
+ }
748
+ if (query.languageTag) {
749
+ conditions.push(eq(destinationTranslations.languageTag, query.languageTag));
750
+ }
751
+ const where = conditions.length > 0 ? and(...conditions) : undefined;
752
+ const [rows, countResult] = await Promise.all([
753
+ db
754
+ .select()
755
+ .from(destinationTranslations)
756
+ .where(where)
757
+ .limit(query.limit)
758
+ .offset(query.offset)
759
+ .orderBy(asc(destinationTranslations.languageTag), asc(destinationTranslations.createdAt)),
760
+ db.select({ count: sql `count(*)::int` }).from(destinationTranslations).where(where),
761
+ ]);
762
+ return {
763
+ data: rows,
764
+ total: countResult[0]?.count ?? 0,
765
+ limit: query.limit,
766
+ offset: query.offset,
767
+ };
768
+ },
769
+ async upsertDestinationTranslation(db, destinationId, data) {
770
+ const [destination] = await db
771
+ .select({ id: destinations.id })
772
+ .from(destinations)
773
+ .where(eq(destinations.id, destinationId))
774
+ .limit(1);
775
+ if (!destination) {
776
+ return null;
777
+ }
778
+ const [existing] = await db
779
+ .select()
780
+ .from(destinationTranslations)
781
+ .where(and(eq(destinationTranslations.destinationId, destinationId), eq(destinationTranslations.languageTag, data.languageTag)))
782
+ .limit(1);
783
+ if (existing) {
784
+ const [row] = await db
785
+ .update(destinationTranslations)
786
+ .set({ ...data, updatedAt: new Date() })
787
+ .where(eq(destinationTranslations.id, existing.id))
788
+ .returning();
789
+ return row ?? null;
790
+ }
791
+ const [row] = await db
792
+ .insert(destinationTranslations)
793
+ .values({ destinationId, ...data })
794
+ .returning();
795
+ return row ?? null;
796
+ },
797
+ async updateDestinationTranslation(db, id, data) {
798
+ const [row] = await db
799
+ .update(destinationTranslations)
800
+ .set({ ...data, updatedAt: new Date() })
801
+ .where(eq(destinationTranslations.id, id))
802
+ .returning();
803
+ return row ?? null;
804
+ },
805
+ async deleteDestinationTranslation(db, id) {
806
+ const [row] = await db
807
+ .delete(destinationTranslations)
808
+ .where(eq(destinationTranslations.id, id))
809
+ .returning({ id: destinationTranslations.id });
810
+ return row ?? null;
811
+ },
812
+ async listProductDestinations(db, query) {
813
+ const conditions = [];
814
+ if (query.productId) {
815
+ conditions.push(eq(productDestinations.productId, query.productId));
816
+ }
817
+ if (query.destinationId) {
818
+ conditions.push(eq(productDestinations.destinationId, query.destinationId));
819
+ }
820
+ const where = conditions.length > 0 ? and(...conditions) : undefined;
821
+ const [rows, countResult] = await Promise.all([
822
+ db
823
+ .select({
824
+ productId: productDestinations.productId,
825
+ destinationId: productDestinations.destinationId,
826
+ sortOrder: productDestinations.sortOrder,
827
+ createdAt: productDestinations.createdAt,
828
+ updatedAt: productDestinations.updatedAt,
829
+ destinationSlug: destinations.slug,
830
+ destinationType: destinations.destinationType,
831
+ destinationActive: destinations.active,
832
+ })
833
+ .from(productDestinations)
834
+ .innerJoin(destinations, eq(destinations.id, productDestinations.destinationId))
835
+ .where(where)
836
+ .limit(query.limit)
837
+ .offset(query.offset)
838
+ .orderBy(asc(productDestinations.sortOrder), asc(destinations.slug)),
839
+ db.select({ count: sql `count(*)::int` }).from(productDestinations).where(where),
840
+ ]);
841
+ return {
842
+ data: rows,
843
+ total: countResult[0]?.count ?? 0,
844
+ limit: query.limit,
845
+ offset: query.offset,
846
+ };
847
+ },
848
+ async assignProductDestination(db, productId, input) {
849
+ const product = await ensureProductExists(db, productId);
850
+ if (!product) {
851
+ return null;
852
+ }
853
+ const [destination] = await db
854
+ .select({ id: destinations.id })
855
+ .from(destinations)
856
+ .where(eq(destinations.id, input.destinationId))
857
+ .limit(1);
858
+ if (!destination) {
859
+ return null;
860
+ }
861
+ const [existing] = await db
862
+ .select()
863
+ .from(productDestinations)
864
+ .where(and(eq(productDestinations.productId, productId), eq(productDestinations.destinationId, input.destinationId)))
865
+ .limit(1);
866
+ if (existing) {
867
+ const [row] = await db
868
+ .update(productDestinations)
869
+ .set({ sortOrder: input.sortOrder ?? existing.sortOrder, updatedAt: new Date() })
870
+ .where(and(eq(productDestinations.productId, productId), eq(productDestinations.destinationId, input.destinationId)))
871
+ .returning();
872
+ return row ?? null;
873
+ }
874
+ const [row] = await db
875
+ .insert(productDestinations)
876
+ .values({
877
+ productId,
878
+ destinationId: input.destinationId,
879
+ sortOrder: input.sortOrder ?? 0,
880
+ })
881
+ .returning();
882
+ return row ?? null;
883
+ },
884
+ async removeProductDestination(db, productId, destinationId) {
885
+ const [row] = await db
886
+ .delete(productDestinations)
887
+ .where(and(eq(productDestinations.productId, productId), eq(productDestinations.destinationId, destinationId)))
888
+ .returning({
889
+ productId: productDestinations.productId,
890
+ destinationId: productDestinations.destinationId,
891
+ });
892
+ return row ?? null;
893
+ },
653
894
  async listOptions(db, query) {
654
895
  const conditions = [];
655
896
  if (query.productId) {
@@ -1384,6 +1625,12 @@ export const productsService = {
1384
1625
  if (query.mediaType) {
1385
1626
  conditions.push(eq(productMedia.mediaType, query.mediaType));
1386
1627
  }
1628
+ if (query.isBrochure !== undefined) {
1629
+ conditions.push(eq(productMedia.isBrochure, query.isBrochure));
1630
+ }
1631
+ if (query.isBrochureCurrent !== undefined) {
1632
+ conditions.push(eq(productMedia.isBrochureCurrent, query.isBrochureCurrent));
1633
+ }
1387
1634
  const where = and(...conditions);
1388
1635
  const [rows, countResult] = await Promise.all([
1389
1636
  db
@@ -1407,6 +1654,12 @@ export const productsService = {
1407
1654
  if (query.mediaType) {
1408
1655
  conditions.push(eq(productMedia.mediaType, query.mediaType));
1409
1656
  }
1657
+ if (query.isBrochure !== undefined) {
1658
+ conditions.push(eq(productMedia.isBrochure, query.isBrochure));
1659
+ }
1660
+ if (query.isBrochureCurrent !== undefined) {
1661
+ conditions.push(eq(productMedia.isBrochureCurrent, query.isBrochureCurrent));
1662
+ }
1410
1663
  const where = and(...conditions);
1411
1664
  const [rows, countResult] = await Promise.all([
1412
1665
  db
@@ -1435,6 +1688,9 @@ export const productsService = {
1435
1688
  return null;
1436
1689
  }
1437
1690
  if (data.dayId) {
1691
+ if (data.isBrochure) {
1692
+ return null;
1693
+ }
1438
1694
  const [day] = await db
1439
1695
  .select({ id: productDays.id, productId: productDays.productId })
1440
1696
  .from(productDays)
@@ -1444,6 +1700,12 @@ export const productsService = {
1444
1700
  return null;
1445
1701
  }
1446
1702
  }
1703
+ if (data.isBrochure) {
1704
+ await db
1705
+ .update(productMedia)
1706
+ .set({ isBrochureCurrent: false, updatedAt: new Date() })
1707
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true), sql `${productMedia.dayId} is null`));
1708
+ }
1447
1709
  const [row] = await db
1448
1710
  .insert(productMedia)
1449
1711
  .values({
@@ -1458,11 +1720,27 @@ export const productsService = {
1458
1720
  altText: data.altText ?? null,
1459
1721
  sortOrder: data.sortOrder,
1460
1722
  isCover: data.isCover,
1723
+ isBrochure: data.isBrochure,
1724
+ isBrochureCurrent: data.isBrochureCurrent,
1725
+ brochureVersion: data.brochureVersion ?? null,
1461
1726
  })
1462
1727
  .returning();
1463
1728
  return row ?? null;
1464
1729
  },
1465
1730
  async updateMedia(db, id, data) {
1731
+ const existing = await this.getMediaById(db, id);
1732
+ if (!existing) {
1733
+ return null;
1734
+ }
1735
+ if (data.isBrochure === true && existing.dayId) {
1736
+ return null;
1737
+ }
1738
+ if (data.isBrochure === true) {
1739
+ await db
1740
+ .update(productMedia)
1741
+ .set({ isBrochureCurrent: false, updatedAt: new Date() })
1742
+ .where(and(eq(productMedia.productId, existing.productId), eq(productMedia.isBrochure, true), sql `${productMedia.dayId} is null`, sql `${productMedia.id} <> ${id}`));
1743
+ }
1466
1744
  const [row] = await db
1467
1745
  .update(productMedia)
1468
1746
  .set({ ...data, updatedAt: new Date() })
@@ -1505,4 +1783,112 @@ export const productsService = {
1505
1783
  }));
1506
1784
  return results.filter((r) => r != null);
1507
1785
  },
1786
+ async getBrochure(db, productId) {
1787
+ const [row] = await db
1788
+ .select()
1789
+ .from(productMedia)
1790
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true), eq(productMedia.isBrochureCurrent, true), sql `${productMedia.dayId} is null`))
1791
+ .orderBy(desc(productMedia.brochureVersion), desc(productMedia.updatedAt), desc(productMedia.createdAt))
1792
+ .limit(1);
1793
+ return row ?? null;
1794
+ },
1795
+ async listBrochures(db, productId) {
1796
+ return db
1797
+ .select()
1798
+ .from(productMedia)
1799
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true), sql `${productMedia.dayId} is null`))
1800
+ .orderBy(desc(productMedia.isBrochureCurrent), desc(productMedia.brochureVersion), desc(productMedia.updatedAt), desc(productMedia.createdAt));
1801
+ },
1802
+ async upsertBrochure(db, productId, data) {
1803
+ const product = await ensureProductExists(db, productId);
1804
+ if (!product) {
1805
+ return null;
1806
+ }
1807
+ const brochures = await this.listBrochures(db, productId);
1808
+ const nextVersion = brochures.reduce((maxVersion, brochure) => {
1809
+ const version = brochure.brochureVersion ?? 0;
1810
+ return version > maxVersion ? version : maxVersion;
1811
+ }, 0) + 1;
1812
+ return this.createMedia(db, productId, {
1813
+ mediaType: "document",
1814
+ dayId: null,
1815
+ name: data.name,
1816
+ url: data.url,
1817
+ storageKey: data.storageKey ?? null,
1818
+ mimeType: data.mimeType ?? "application/pdf",
1819
+ fileSize: data.fileSize ?? null,
1820
+ altText: data.altText ?? null,
1821
+ sortOrder: data.sortOrder,
1822
+ isCover: false,
1823
+ isBrochure: true,
1824
+ isBrochureCurrent: true,
1825
+ brochureVersion: nextVersion,
1826
+ });
1827
+ },
1828
+ async deleteBrochure(db, productId) {
1829
+ const brochure = await this.getBrochure(db, productId);
1830
+ if (!brochure) {
1831
+ return null;
1832
+ }
1833
+ const [row] = await db.delete(productMedia).where(eq(productMedia.id, brochure.id)).returning();
1834
+ const [previous] = await db
1835
+ .select()
1836
+ .from(productMedia)
1837
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true), sql `${productMedia.dayId} is null`))
1838
+ .orderBy(desc(productMedia.brochureVersion), desc(productMedia.updatedAt), desc(productMedia.createdAt))
1839
+ .limit(1);
1840
+ if (previous) {
1841
+ await db
1842
+ .update(productMedia)
1843
+ .set({ isBrochureCurrent: true, updatedAt: new Date() })
1844
+ .where(eq(productMedia.id, previous.id));
1845
+ }
1846
+ return row ?? null;
1847
+ },
1848
+ async setCurrentBrochure(db, productId, brochureId) {
1849
+ const [brochure] = await db
1850
+ .select()
1851
+ .from(productMedia)
1852
+ .where(and(eq(productMedia.id, brochureId), eq(productMedia.productId, productId), eq(productMedia.isBrochure, true), sql `${productMedia.dayId} is null`))
1853
+ .limit(1);
1854
+ if (!brochure) {
1855
+ return null;
1856
+ }
1857
+ await db
1858
+ .update(productMedia)
1859
+ .set({ isBrochureCurrent: false, updatedAt: new Date() })
1860
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true), sql `${productMedia.dayId} is null`));
1861
+ const [row] = await db
1862
+ .update(productMedia)
1863
+ .set({ isBrochureCurrent: true, updatedAt: new Date() })
1864
+ .where(eq(productMedia.id, brochureId))
1865
+ .returning();
1866
+ return row ?? null;
1867
+ },
1868
+ async deleteBrochureVersion(db, productId, brochureId) {
1869
+ const [brochure] = await db
1870
+ .select()
1871
+ .from(productMedia)
1872
+ .where(and(eq(productMedia.id, brochureId), eq(productMedia.productId, productId), eq(productMedia.isBrochure, true), sql `${productMedia.dayId} is null`))
1873
+ .limit(1);
1874
+ if (!brochure) {
1875
+ return null;
1876
+ }
1877
+ const [row] = await db.delete(productMedia).where(eq(productMedia.id, brochureId)).returning();
1878
+ if (brochure.isBrochureCurrent) {
1879
+ const [previous] = await db
1880
+ .select()
1881
+ .from(productMedia)
1882
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true), sql `${productMedia.dayId} is null`))
1883
+ .orderBy(desc(productMedia.brochureVersion), desc(productMedia.updatedAt), desc(productMedia.createdAt))
1884
+ .limit(1);
1885
+ if (previous) {
1886
+ await db
1887
+ .update(productMedia)
1888
+ .set({ isBrochureCurrent: true, updatedAt: new Date() })
1889
+ .where(eq(productMedia.id, previous.id));
1890
+ }
1891
+ }
1892
+ return row ?? null;
1893
+ },
1508
1894
  };
@@ -0,0 +1,29 @@
1
+ import type { ProductBrochureTemplateContext, RenderedProductBrochureTemplate } from "./brochure-templates.js";
2
+ export interface ProductBrochurePrinterContext {
3
+ template: RenderedProductBrochureTemplate;
4
+ context: ProductBrochureTemplateContext;
5
+ }
6
+ export interface PrintedProductBrochureArtifact {
7
+ body: Uint8Array;
8
+ mimeType?: string | null;
9
+ fileSize?: number | null;
10
+ metadata?: Record<string, unknown> | null;
11
+ }
12
+ export type ProductBrochurePrinter = (input: ProductBrochurePrinterContext) => Promise<PrintedProductBrochureArtifact>;
13
+ export interface CloudflareBrowserBrochurePrinterOptions {
14
+ accountId: string;
15
+ apiToken: string;
16
+ apiBaseUrl?: string;
17
+ addStyleTag?: Array<{
18
+ content?: string;
19
+ url?: string;
20
+ }>;
21
+ pdfOptions?: Record<string, unknown>;
22
+ gotoOptions?: Record<string, unknown>;
23
+ viewport?: Record<string, unknown>;
24
+ setExtraHTTPHeaders?: Record<string, string>;
25
+ }
26
+ export declare function createBasicPdfProductBrochurePrinter(): ProductBrochurePrinter;
27
+ export declare function createCloudflareBrowserProductBrochurePrinter(options: CloudflareBrowserBrochurePrinterOptions): ProductBrochurePrinter;
28
+ export declare function createCloudflareBrowserProductBrochurePrinterFromEnv(env?: Record<string, string | undefined>): ProductBrochurePrinter;
29
+ //# sourceMappingURL=brochure-printers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brochure-printers.d.ts","sourceRoot":"","sources":["../../src/tasks/brochure-printers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,8BAA8B,EAC9B,+BAA+B,EAChC,MAAM,yBAAyB,CAAA;AAEhC,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,+BAA+B,CAAA;IACzC,OAAO,EAAE,8BAA8B,CAAA;CACxC;AAED,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,UAAU,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAC1C;AAED,MAAM,MAAM,sBAAsB,GAAG,CACnC,KAAK,EAAE,6BAA6B,KACjC,OAAO,CAAC,8BAA8B,CAAC,CAAA;AAE5C,MAAM,WAAW,uCAAuC;IACtD,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC7C;AA8BD,wBAAgB,oCAAoC,IAAI,sBAAsB,CAmB7E;AAED,wBAAgB,6CAA6C,CAC3D,OAAO,EAAE,uCAAuC,GAC/C,sBAAsB,CA6CxB;AAED,wBAAgB,oDAAoD,CAClE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACvC,sBAAsB,CAkBxB"}
@@ -0,0 +1,94 @@
1
+ import { renderPdfDocument } from "@voyantjs/utils/pdf-renderer";
2
+ function escapeHtml(value) {
3
+ return value
4
+ .replaceAll("&", "&amp;")
5
+ .replaceAll("<", "&lt;")
6
+ .replaceAll(">", "&gt;")
7
+ .replaceAll('"', "&quot;")
8
+ .replaceAll("'", "&#39;");
9
+ }
10
+ function brochureBodyToHtml(body, bodyFormat, title) {
11
+ const content = bodyFormat === "html"
12
+ ? body
13
+ : `<pre style="white-space: pre-wrap; font-family: system-ui, sans-serif;">${escapeHtml(body)}</pre>`;
14
+ return [
15
+ "<!doctype html>",
16
+ '<html lang="en">',
17
+ "<head>",
18
+ '<meta charset="utf-8" />',
19
+ `<title>${escapeHtml(title)}</title>`,
20
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
21
+ "</head>",
22
+ `<body>${content}</body>`,
23
+ "</html>",
24
+ ].join("");
25
+ }
26
+ export function createBasicPdfProductBrochurePrinter() {
27
+ return async ({ template, context }) => {
28
+ const body = await renderPdfDocument({
29
+ title: template.title,
30
+ content: template.body,
31
+ format: template.bodyFormat,
32
+ metadataLines: [`Product ID: ${context.product.id}`, ...template.metadataLines],
33
+ });
34
+ return {
35
+ body,
36
+ mimeType: "application/pdf",
37
+ fileSize: body.byteLength,
38
+ metadata: {
39
+ renderer: "voyant-basic-pdf",
40
+ bodyFormat: template.bodyFormat,
41
+ },
42
+ };
43
+ };
44
+ }
45
+ export function createCloudflareBrowserProductBrochurePrinter(options) {
46
+ const apiBaseUrl = options.apiBaseUrl?.replace(/\/$/, "") || "https://api.cloudflare.com/client/v4";
47
+ return async ({ template, context }) => {
48
+ const response = await fetch(`${apiBaseUrl}/accounts/${options.accountId}/browser-rendering/pdf`, {
49
+ method: "POST",
50
+ headers: {
51
+ Authorization: `Bearer ${options.apiToken}`,
52
+ "Content-Type": "application/json",
53
+ },
54
+ body: JSON.stringify({
55
+ html: brochureBodyToHtml(template.body, template.bodyFormat, template.title),
56
+ addStyleTag: options.addStyleTag,
57
+ pdfOptions: options.pdfOptions,
58
+ gotoOptions: options.gotoOptions,
59
+ viewport: options.viewport,
60
+ setExtraHTTPHeaders: options.setExtraHTTPHeaders,
61
+ }),
62
+ });
63
+ if (!response.ok) {
64
+ const errorBody = await response.text().catch(() => "");
65
+ throw new Error(`Cloudflare Browser brochure print failed: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`);
66
+ }
67
+ const body = new Uint8Array(await response.arrayBuffer());
68
+ return {
69
+ body,
70
+ mimeType: "application/pdf",
71
+ fileSize: body.byteLength,
72
+ metadata: {
73
+ renderer: "cloudflare-browser",
74
+ bodyFormat: template.bodyFormat,
75
+ browserMsUsed: response.headers.get("X-Browser-Ms-Used"),
76
+ productId: context.product.id,
77
+ },
78
+ };
79
+ };
80
+ }
81
+ export function createCloudflareBrowserProductBrochurePrinterFromEnv(env) {
82
+ const resolvedEnv = env ??
83
+ globalThis.process?.env ??
84
+ {};
85
+ const accountId = resolvedEnv.CLOUDFLARE_ACCOUNT_ID?.trim();
86
+ const apiToken = resolvedEnv.CLOUDFLARE_API_TOKEN?.trim();
87
+ if (!accountId || !apiToken) {
88
+ throw new Error("createCloudflareBrowserProductBrochurePrinterFromEnv requires CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN.");
89
+ }
90
+ return createCloudflareBrowserProductBrochurePrinter({
91
+ accountId,
92
+ apiToken,
93
+ });
94
+ }
@@ -0,0 +1,36 @@
1
+ import { type StructuredTemplateBodyFormat } from "@voyantjs/utils/template-renderer";
2
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+ import { productDayServices, productDays, products } from "../schema.js";
4
+ type ProductDayRecord = typeof productDays.$inferSelect;
5
+ type ProductDayServiceRecord = typeof productDayServices.$inferSelect;
6
+ type ProductRecord = typeof products.$inferSelect;
7
+ export interface ProductBrochureDayContext extends ProductDayRecord {
8
+ services: Array<ProductDayServiceRecord>;
9
+ }
10
+ export interface ProductBrochureTemplateContext {
11
+ product: ProductRecord;
12
+ days: ProductBrochureDayContext[];
13
+ generatedAt: Date;
14
+ }
15
+ type TemplateResolver<T> = T | ((context: ProductBrochureTemplateContext) => Promise<T> | T);
16
+ export interface ProductBrochureTemplateDefinition {
17
+ bodyFormat: StructuredTemplateBodyFormat;
18
+ body: TemplateResolver<string>;
19
+ variables?: Record<string, unknown> | ((context: ProductBrochureTemplateContext) => Promise<Record<string, unknown>> | Record<string, unknown>);
20
+ title?: TemplateResolver<string>;
21
+ filename?: TemplateResolver<string>;
22
+ metadataLines?: TemplateResolver<string[]>;
23
+ }
24
+ export interface RenderedProductBrochureTemplate {
25
+ body: string;
26
+ bodyFormat: StructuredTemplateBodyFormat;
27
+ title: string;
28
+ filename: string;
29
+ variables: Record<string, unknown>;
30
+ metadataLines: string[];
31
+ }
32
+ export declare function loadProductBrochureTemplateContext(db: PostgresJsDatabase, productId: string): Promise<ProductBrochureTemplateContext>;
33
+ export declare function createDefaultProductBrochureTemplate(): ProductBrochureTemplateDefinition;
34
+ export declare function renderProductBrochureTemplate(template: ProductBrochureTemplateDefinition, context: ProductBrochureTemplateContext): Promise<RenderedProductBrochureTemplate>;
35
+ export {};
36
+ //# sourceMappingURL=brochure-templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brochure-templates.d.ts","sourceRoot":"","sources":["../../src/tasks/brochure-templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,mCAAmC,CAAA;AAE1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAExE,KAAK,gBAAgB,GAAG,OAAO,WAAW,CAAC,YAAY,CAAA;AACvD,KAAK,uBAAuB,GAAG,OAAO,kBAAkB,CAAC,YAAY,CAAA;AACrE,KAAK,aAAa,GAAG,OAAO,QAAQ,CAAC,YAAY,CAAA;AAEjD,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IACjE,QAAQ,EAAE,KAAK,CAAC,uBAAuB,CAAC,CAAA;CACzC;AAED,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,aAAa,CAAA;IACtB,IAAI,EAAE,yBAAyB,EAAE,CAAA;IACjC,WAAW,EAAE,IAAI,CAAA;CAClB;AAED,KAAK,gBAAgB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,8BAA8B,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;AAE5F,MAAM,WAAW,iCAAiC;IAChD,UAAU,EAAE,4BAA4B,CAAA;IACxC,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAA;IAC9B,SAAS,CAAC,EACN,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACvB,CAAC,CACC,OAAO,EAAE,8BAA8B,KACpC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACpE,KAAK,CAAC,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAA;IAChC,QAAQ,CAAC,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAA;IACnC,aAAa,CAAC,EAAE,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAA;CAC3C;AAED,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,4BAA4B,CAAA;IACxC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,aAAa,EAAE,MAAM,EAAE,CAAA;CACxB;AAmBD,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,8BAA8B,CAAC,CAiCzC;AAED,wBAAgB,oCAAoC,IAAI,iCAAiC,CAqCxF;AAED,wBAAsB,6BAA6B,CACjD,QAAQ,EAAE,iCAAiC,EAC3C,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,+BAA+B,CAAC,CAuB1C"}