Monorepo for Aesthetic.Computer
aesthetic.computer
1// Migration script to add short codes to existing paintings
2// Run with: npm run migrate:dry (or migrate, migrate:user, migrate:guest)
3
4import { connect } from "../system/backend/database.mjs";
5import { generateUniqueCode } from "../system/backend/painting-codes/generate-code.mjs";
6import { codeExists } from "../system/backend/painting-codes/lookup.mjs";
7import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
8
9const args = process.argv.slice(2);
10const dryRun = args.includes("--dry-run");
11const userOnly = args.includes("--user-only");
12const guestOnly = args.includes("--guest-only");
13
14console.log("🎨 Painting Short Code Migration");
15console.log("================================\n");
16
17if (dryRun) {
18 console.log("🔍 DRY RUN MODE - No changes will be made\n");
19}
20
21// S3 Client setup
22const s3Client = new S3Client({
23 region: process.env.DO_SPACES_REGION || "sfo3",
24 endpoint: process.env.DO_SPACES_ENDPOINT || "https://sfo3.digitaloceanspaces.com",
25 credentials: {
26 accessKeyId: process.env.DO_SPACES_KEY || process.env.ART_KEY,
27 secretAccessKey: process.env.DO_SPACES_SECRET || process.env.ART_SECRET,
28 },
29});
30
31async function listGuestPaintings() {
32 const bucket = process.env.ART_SPACE_NAME || "art-aesthetic-computer";
33 const paintings = [];
34 let continuationToken;
35
36 console.log(`📦 Listing guest paintings from bucket: ${bucket}`);
37
38 do {
39 const command = new ListObjectsV2Command({
40 Bucket: bucket,
41 ContinuationToken: continuationToken,
42 });
43
44 const response = await s3Client.send(command);
45
46 if (response.Contents) {
47 for (const item of response.Contents) {
48 if (item.Key.endsWith(".png")) {
49 paintings.push({
50 key: item.Key,
51 slug: item.Key.replace(".png", ""),
52 lastModified: item.LastModified,
53 size: item.Size,
54 });
55 }
56 }
57 }
58
59 continuationToken = response.NextContinuationToken;
60 } while (continuationToken);
61
62 console.log(` Found ${paintings.length} guest paintings\n`);
63 return paintings;
64}
65
66async function migrateUserPaintings(database) {
67 if (guestOnly) {
68 console.log("⏭️ Skipping user paintings (--guest-only flag)\n");
69 return { processed: 0, updated: 0, skipped: 0 };
70 }
71
72 console.log("👤 Migrating USER paintings");
73 console.log("---------------------------");
74
75 const paintings = database.db.collection("paintings");
76
77 // Find all user paintings without codes
78 const userPaintings = await paintings
79 .find({
80 user: { $exists: true },
81 code: { $exists: false },
82 })
83 .toArray();
84
85 console.log(` Found ${userPaintings.length} user paintings without codes`);
86
87 let processed = 0;
88 let updated = 0;
89 let skipped = 0;
90
91 for (const painting of userPaintings) {
92 processed++;
93
94 try {
95 // Generate unique code
96 const code = await generateUniqueCode(codeExists);
97
98 if (!dryRun) {
99 await paintings.updateOne(
100 { _id: painting._id },
101 {
102 $set: {
103 code,
104 bucket: "user-aesthetic-computer",
105 type: "user",
106 },
107 }
108 );
109 updated++;
110 }
111
112 if (processed % 100 === 0 || dryRun) {
113 console.log(` [${processed}/${userPaintings.length}] ${dryRun ? "Would assign" : "Assigned"} code: ${code} → ${painting.slug}`);
114 }
115 } catch (error) {
116 console.error(` ❌ Error processing ${painting.slug}:`, error.message);
117 skipped++;
118 }
119 }
120
121 console.log(`\n ✅ Processed: ${processed}`);
122 console.log(` ${dryRun ? "📝 Would update" : "💾 Updated"}: ${updated}`);
123 if (skipped > 0) console.log(` ⚠️ Skipped: ${skipped}`);
124 console.log();
125
126 return { processed, updated, skipped };
127}
128
129async function migrateGuestPaintings(database) {
130 if (userOnly) {
131 console.log("⏭️ Skipping guest paintings (--user-only flag)\n");
132 return { processed: 0, created: 0, skipped: 0 };
133 }
134
135 console.log("🎭 Migrating GUEST paintings");
136 console.log("----------------------------");
137
138 const paintings = database.db.collection("paintings");
139
140 // Get list of guest paintings from S3
141 const s3Paintings = await listGuestPaintings();
142
143 let processed = 0;
144 let created = 0;
145 let skipped = 0;
146
147 for (const s3Painting of s3Paintings) {
148 processed++;
149
150 try {
151 // Check if painting already exists in database
152 const existing = await paintings.findOne({ slug: s3Painting.slug });
153
154 if (existing) {
155 // Already has a database record
156 if (!existing.code) {
157 // Needs a code
158 const code = await generateUniqueCode(codeExists);
159
160 if (!dryRun) {
161 await paintings.updateOne(
162 { _id: existing._id },
163 {
164 $set: {
165 code,
166 bucket: "art-aesthetic-computer",
167 type: "guest",
168 },
169 }
170 );
171 created++;
172 }
173
174 if (processed % 100 === 0 || dryRun) {
175 console.log(` [${processed}/${s3Paintings.length}] ${dryRun ? "Would assign" : "Assigned"} code: ${code} → ${s3Painting.slug}`);
176 }
177 } else {
178 skipped++;
179 }
180 } else {
181 // Create new database record
182 const code = await generateUniqueCode(codeExists);
183
184 if (!dryRun) {
185 await paintings.insertOne({
186 code,
187 slug: s3Painting.slug,
188 // user: undefined, // Don't set user field for guest paintings
189 when: s3Painting.lastModified,
190 nuked: false,
191 bucket: "art-aesthetic-computer",
192 type: "guest",
193 });
194 created++;
195 }
196
197 if (processed % 100 === 0 || dryRun) {
198 console.log(` [${processed}/${s3Paintings.length}] ${dryRun ? "Would create" : "Created"} record with code: ${code} → ${s3Painting.slug}`);
199 }
200 }
201 } catch (error) {
202 console.error(` ❌ Error processing ${s3Painting.slug}:`, error.message);
203 skipped++;
204 }
205 }
206
207 console.log(`\n ✅ Processed: ${processed}`);
208 console.log(` ${dryRun ? "📝 Would create" : "💾 Created"}: ${created}`);
209 if (skipped > 0) console.log(` ⚠️ Skipped: ${skipped}`);
210 console.log();
211
212 return { processed, created, skipped };
213}
214
215async function run() {
216 const database = await connect();
217
218 try {
219 const startTime = Date.now();
220
221 // Migrate user paintings
222 const userResults = await migrateUserPaintings(database);
223
224 // Migrate guest paintings
225 const guestResults = await migrateGuestPaintings(database);
226
227 const endTime = Date.now();
228 const duration = ((endTime - startTime) / 1000).toFixed(2);
229
230 console.log("📊 Migration Summary");
231 console.log("===================");
232 console.log(` User paintings: ${userResults.updated} ${dryRun ? "would be updated" : "updated"}`);
233 console.log(` Guest paintings: ${guestResults.created} ${dryRun ? "would be created" : "created"}`);
234 console.log(` Total processed: ${userResults.processed + guestResults.processed}`);
235 console.log(` Duration: ${duration}s`);
236
237 if (dryRun) {
238 console.log(`\n💡 Run without --dry-run to apply changes`);
239 } else {
240 console.log(`\n✅ Migration complete!`);
241 }
242 } finally {
243 await database.disconnect();
244 }
245}
246
247run().catch((error) => {
248 console.error("❌ Migration failed:", error);
249 process.exit(1);
250});