@voyantjs/products 0.3.0 → 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.
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/routes-public.d.ts +492 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +44 -0
- package/dist/routes.d.ts +669 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +117 -1
- package/dist/schema-itinerary.d.ts +51 -0
- package/dist/schema-itinerary.d.ts.map +1 -1
- package/dist/schema-itinerary.js +3 -0
- package/dist/schema-relations.d.ts +14 -0
- package/dist/schema-relations.d.ts.map +1 -1
- package/dist/schema-relations.js +28 -1
- package/dist/schema-taxonomy.d.ts +435 -0
- package/dist/schema-taxonomy.d.ts.map +1 -1
- package/dist/schema-taxonomy.js +47 -0
- package/dist/service-catalog.d.ts +237 -0
- package/dist/service-catalog.d.ts.map +1 -0
- package/dist/service-catalog.js +478 -0
- package/dist/service-public.d.ts +383 -0
- package/dist/service-public.d.ts.map +1 -0
- package/dist/service-public.js +365 -0
- package/dist/service.d.ts +292 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +388 -2
- package/dist/tasks/brochure-printers.d.ts +29 -0
- package/dist/tasks/brochure-printers.d.ts.map +1 -0
- package/dist/tasks/brochure-printers.js +94 -0
- package/dist/tasks/brochure-templates.d.ts +36 -0
- package/dist/tasks/brochure-templates.d.ts.map +1 -0
- package/dist/tasks/brochure-templates.js +98 -0
- package/dist/tasks/brochures.d.ts +42 -0
- package/dist/tasks/brochures.d.ts.map +1 -0
- package/dist/tasks/brochures.js +69 -0
- package/dist/tasks/index.d.ts +3 -0
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +3 -0
- package/dist/validation-catalog.d.ts +388 -0
- package/dist/validation-catalog.d.ts.map +1 -0
- package/dist/validation-catalog.js +54 -0
- package/dist/validation-content.d.ts +109 -0
- package/dist/validation-content.d.ts.map +1 -1
- package/dist/validation-content.js +63 -1
- package/dist/validation-public.d.ts +643 -0
- package/dist/validation-public.d.ts.map +1 -0
- package/dist/validation-public.js +167 -0
- package/dist/validation-shared.d.ts +11 -0
- package/dist/validation-shared.d.ts.map +1 -1
- package/dist/validation-shared.js +2 -0
- package/dist/validation.d.ts +1 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1 -0
- package/package.json +14 -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("&", "&")
|
|
5
|
+
.replaceAll("<", "<")
|
|
6
|
+
.replaceAll(">", ">")
|
|
7
|
+
.replaceAll('"', """)
|
|
8
|
+
.replaceAll("'", "'");
|
|
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"}
|