Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251// products.mjs, 24.12.20// Manage product previews and cache them to S3// Collection: aesthetic.products//// Code Prefixes (universal across AC):// # - Paintings (e.g., #hjq)// $ - KidLisp pieces (e.g., $cow)// ! - Tapes (e.g., !abc123)// + - Prints/Products (e.g., +7brmof) - mugs, posters, stickers// @ - Handles/Users (e.g., @jeffrey)
// Schema:// {// _id: ObjectId,// code: string, // Unique short code (6 chars, e.g., "7brmof")// source: {// type: "painting" | "tape" | "upload",// code: string, // e.g., painting code "hjq"// user: string, // auth0 user id (denormalized for queries)// via: string, // KidLisp source code (e.g., "cow" for $cow)// },// product: "mug" | "poster" | "sticker" | etc,// variant: string, // e.g., "blue", "11oz", etc.// preview: string, // Animated WebP preview URL (S3)// printful: {// productId: number,// variantId: number,// },// stats: {// views: number, // Times the preview was viewed// checkouts: number, // Times checkout was started// ordered: number, // Times successfully ordered/paid// },// createdAt: Date,// updatedAt: Date,// }
import { S3Client, PutObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3";import { connect } from "./database.mjs";
const BUCKET = "art-aesthetic-computer";const REGION = "sfo3";const ENDPOINT = `https://${REGION}.digitaloceanspaces.com`;const CDN_URL = "https://art.aesthetic.computer";
// Generate a short unique code for printsfunction generateCode(length = 6) { const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; let code = ""; for (let i = 0; i < length; i++) { code += chars[Math.floor(Math.random() * chars.length)]; } return code;}
// Get S3 client for DO Spacesfunction getS3Client() { return new S3Client({ endpoint: ENDPOINT, region: "us-east-1", // Required for DO Spaces credentials: { accessKeyId: process.env.DO_SPACES_KEY || process.env.ART_KEY, secretAccessKey: process.env.DO_SPACES_SECRET || process.env.ART_SECRET, }, });}
// Upload buffer to S3 and return public URLasync function uploadToS3(buffer, key, contentType) { const s3 = getS3Client(); await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: key, Body: buffer, ContentType: contentType, ACL: "public-read", })); return `${CDN_URL}/${key}`;}
// Check if S3 object existsasync function s3Exists(key) { const s3 = getS3Client(); try { await s3.send(new HeadObjectCommand({ Bucket: BUCKET, Key: key })); return true; } catch (e) { return false; }}
/** * Find existing product or create a new one * @param {Object} params * @param {Object} params.source - { type, code, user, via } * @param {string} params.product - Product type (e.g., "mug") * @param {string} params.variant - Product variant (e.g., "blue") * @returns {Object} Product document (existing or new) */export async function findOrCreateProduct({ source, product, variant }) { const database = await connect(); const products = database.db.collection("products"); // Check for existing product with same source + product + variant + via // (Different via codes create different products for attribution) const query = { "source.type": source.type, "source.code": source.code, product, variant, }; // Only match via if it's specified (undefined via = generic product) if (source.via) { query["source.via"] = source.via; } else { query["source.via"] = { $exists: false }; } const existing = await products.findOne(query); if (existing) { // Increment view count await products.updateOne({ _id: existing._id }, { $inc: { "stats.views": 1 } }); console.log(`📦 Found existing product: ${existing.code}`); return { product: existing, isNew: false, database }; } // Create new product record const code = generateCode(); const now = new Date(); const newProduct = { code, source, product, variant, preview: null, printful: {}, stats: { views: 1, checkouts: 0, ordered: 0 }, createdAt: now, updatedAt: now, }; const result = await products.insertOne(newProduct); newProduct._id = result.insertedId; console.log(`📦 Created new product: ${code}`); return { product: newProduct, isNew: true, database };}
/** * Update product with generated assets * @param {string} code - Product code * @param {Object} updates - Fields to update */export async function updateProduct(code, updates) { const database = await connect(); const products = database.db.collection("products"); await products.updateOne( { code }, { $set: { ...updates, updatedAt: new Date() } } ); console.log(`📦 Updated product: ${code}`);}
/** * Increment a stat counter for a product * @param {string} code - Product code * @param {string} stat - Stat to increment ("views", "checkouts", "ordered") */export async function incrementProductStat(code, stat) { const database = await connect(); const products = database.db.collection("products"); await products.updateOne( { code }, { $inc: { [`stats.${stat}`]: 1 }, $set: { updatedAt: new Date() } } ); console.log(`📦 Incremented ${stat} for product: ${code}`);}
/** * Cache WebP preview to S3 and update product record * @param {string} productCode - Product code * @param {Buffer} webpBuffer - WebP image buffer * @returns {string} Public S3 URL */export async function cacheWebpPreview(productCode, webpBuffer) { const key = `products/${productCode}.webp`; const url = await uploadToS3(webpBuffer, key, "image/webp"); await updateProduct(productCode, { preview: url }); console.log(`📦 Cached WebP preview: ${url}`); return url;}
/** * Get all products for a source media * @param {string} sourceType - "painting", "tape", etc. * @param {string} sourceCode - Source media code * @returns {Array} Array of product documents */export async function getProductsForSource(sourceType, sourceCode) { const database = await connect(); const products = database.db.collection("products"); return products.find({ "source.type": sourceType, "source.code": sourceCode }).toArray();}
/** * Get product by code * @param {string} code - Product code * @returns {Object|null} Product document or null */export async function getProduct(code) { const database = await connect(); const products = database.db.collection("products"); return products.findOne({ code });}
/** * Get recent products of a specific type * @param {string} productType - "mug", "poster", etc. * @param {number} limit - Max number of products to return * @returns {Array} Array of product documents */export async function getRecentProducts(productType, limit = 20) { const database = await connect(); const products = database.db.collection("products"); return products .find({ product: productType }) .sort({ createdAt: -1 }) .limit(limit) .toArray();}
export { uploadToS3, s3Exists, generateCode };