Setting the file. One moment.
Seed Events · Wix Headless Fast · wix/skills · Skills Docs
ContentsBack to the top of the page — line 188
This file
Number 7.49
Position 49 of 148
Type JavaScript
Size 12 KB
Lines 276 references/events/seed/ seed-events.mjs
JavaScript · 276 lines · 12 KB
// "startDate", "endDate" (future ISO-8601 UTC), "timeZoneId",
15 // "location" ({name,type:"VENUE",address} | {name,type:"ONLINE"} | {locationTbd:true,name}),
16 // "ticketTiers"?: [{ "name" (≤30 chars), "price" (decimal STRING), "description"?, "initialLimit"? }],
17 // "category"? (name), "imageUrl"? | "imagePrompt"?, "rsvpResponseType"? }] }
18 //
19 // Seeding is ADDITIVE — never deletes or overwrites existing content. Unexpected shapes →
20 // read the live API reference; authoritative source recipe:
21 // wix-headless/references/inline-recipes/setup-events.md.
22 import { execFileSync } from "node:child_process" ;
23 import { readFileSync } from "node:fs" ;
24 import { resolveItemImages } from "../../shared/seed/images.mjs" ;
25
26 const API = "https://www.wixapis.com" ;
27 const EVENTS_APP_ID = "140603ad-af8d-84a5-2c80-a0f60cb47351" ;
28
29 export function makeCtx ({ cwd = process. cwd () } = {}) {
30 const config = JSON . parse ( readFileSync ( `${ cwd }/wix.config.json` , "utf8" ));
31 const siteId = config.siteId ?? config.projectId;
32 if ( ! siteId) throw new Error ( "wix.config.json has no siteId — is this a Wix CLI project?" );
33 const token = execFileSync ( "npx" , [ "@wix/cli@latest" , "token" , "--site" , siteId], {
34 encoding: "utf8" ,
35 cwd,
36 }). trim ();
37 if ( ! token) throw new Error ( "The Wix CLI returned no token — run `npx @wix/cli@latest login` first." );
38 return { token, siteId };
39 }
40
41 async function req ( ctx , path , { method = "POST" , body } = {}) {
42 const res = await fetch ( API + path, {
43 method,
44 headers: {
45 Authorization: `Bearer ${ ctx . token }` ,
46 "wix-site-id" : ctx.siteId,
47 "Content-Type" : "application/json" ,
48 },
49 body: body ? JSON . stringify (body) : undefined ,
50 });
51 const json = await res. json (). catch (() => ({}));
52 if ( ! res.ok) throw new Error ( `${ method } ${ path } -> ${ res . status }: ${ JSON . stringify ( json ). slice ( 0 , 400 ) }` );
53 return json;
54 }
55
56 // {type:"TICKETING"|"RSVP", …} -> the registration block. TICKETING carries a tickets{}
57 // config; RSVP carries rsvp{responseType} and seeds NO form fields (name+email is built-in).
58 // initialType is IMMUTABLE after create — set from the plan, never plan to convert.
59 function buildRegistration ( ev ) {
60 if (ev.type === "TICKETING" ) {
61 return {
62 initialType: "TICKETING" ,
63 tickets: {
64 ticketLimitPerOrder: ev.ticketLimitPerOrder ?? 8 ,
65 currency: ev.currency ?? "USD" , // setupEvents threads the site currency; USD is the last resort
66 reservationDurationInMinutes: ev.reservationDurationInMinutes ?? 20 ,
67 },
68 };
69 }
70 return {
71 initialType: "RSVP" ,
72 rsvp: { responseType: ev.rsvpResponseType ?? "YES_ONLY" }, // "YES_ONLY" | "YES_AND_NO"
73 };
74 }
75
76 // ---- operations ----------------------------------------------------------------------------------
77
78 // Idempotent: re-installing an already-installed app returns 200.
79 // docs: https://dev.wix.com/docs/api-reference/articles/work-with-wix-apis/platform/about-apps-created-by-wix.md
80 export async function installEventsApp ( ctx ) {
81 try {
82 await req (ctx, "/apps-installer-service/v1/app-instance/install" , { body: {
83 tenant: { tenantType: "SITE" , id: ctx.siteId },
84 appInstance: { appDefId: EVENTS_APP_ID , enabled: true },
85 } });
86 } catch {
87 /* already installed is fine */
88 }
89 }
90
91 // The ticket-definitions API REQUIRES currency and uses whatever you pass verbatim (it does
92 // NOT fall back to the site currency) — resolve it once, or a non-USD site gets mis-priced.
93 // docs: https://dev.wix.com/docs/api-reference/business-management/site-properties/properties/get-site-properties.md
94 export async function getSiteCurrency ( ctx ) {
95 try {
96 const r = await req (ctx, "/site-properties/v4/properties" , { method: "GET" });
97 return r?.properties?.paymentCurrency || "USD" ;
98 } catch {
99 return "USD" ;
100 }
101 }
102
103 /**
104 * STEP 1 — create ONE event as a draft (no bulk endpoint; loop for multiple). Dates MUST be
105 * in the future — a past event isn't registerable and won't show in the live listing.
106 * Returns { id, slug } — id feeds the tier/publish steps; slug is the URL identifier.
107 * docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/events-v3/create-event.md
108 */
109 export async function createEvent ( ctx , ev ) {
110 const body = {
111 draft: true ,
112 event: {
113 title: ev.title,
114 shortDescription: ev.shortDescription,
115 location: ev.location,
116 dateAndTimeSettings: {
117 startDate: ev.startDate,
118 endDate: ev.endDate,
119 timeZoneId: ev.timeZoneId,
120 showTimeZone: ev.showTimeZone ?? true ,
121 },
122 registration: buildRegistration (ev),
123 },
124 fields: [ "DETAILS" , "TEXTS" , "REGISTRATION" , "URLS" ],
125 };
126 const r = await req (ctx, "/events/v3/events" , { body });
127 return { id: r.event?.id, slug: r.event?.slug };
128 }
129
130 /**
131 * STEP 2 — ticket tiers for a TICKETING event (skip for RSVP). Must run BEFORE publish —
132 * publishing a ticketed event with no tiers ships an event with nothing to buy, and there is
133 * no un-publish. price is a decimal STRING ("65.00", never a number); name ≤ 30 chars; omit
134 * initialLimit for unlimited. Tiers are independent — fired as one parallel batch.
135 * docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/ticket-definitions-v3/create-ticket-definition.md
136 */
137 export async function createTicketTiers ( ctx , eventId , tiers ) {
138 return Promise . all (tiers. map ( async ( t ) => {
139 const body = {
140 ticketDefinition: {
141 eventId,
142 name: t.name,
143 description: t.description,
144 ... (t.initialLimit != null ? { initialLimit: t.initialLimit } : {}),
145 pricingMethod: { fixedPrice: { value: String (t.price), currency: t.currency ?? "USD" } },
146 feeType: t.feeType ?? "FEE_INCLUDED" ,
147 },
148 fields: [ "SALES_DETAILS" ],
149 };
150 const r = await req (ctx, "/events/v3/ticket-definitions" , { body });
151 return { id: r.ticketDefinition?.id };
152 }));
153 }
154
155 // STEP 3 — publish (one-way; for TICKETING only after its tiers exist).
156 // docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/events-v3/publish-draft-event.md
157 export async function publishEvent ( ctx , eventId ) {
158 return req (ctx, `/events/v3/events/${ eventId }/publish` , { body: {} });
159 }
160
161 // STEP 4 (optional) — group events by a format/track. Categories are the v1 API (NOT v3).
162 // docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/categories/create-category.md
163 export async function createEventCategories ( ctx , names ) {
164 const out = [];
165 for ( const name of names) {
166 const r = await req (ctx, "/events/v1/categories" , { body: { category: { name } } });
167 out. push ({ id: r.category?.id, name });
168 }
169 return out;
170 }
171
172 // Assign events to a category. Path is /{categoryId}/events (NOT /assign); body key is
173 // `eventId` — an ARRAY despite the singular name.
174 // docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/categories/assign-events.md
175 export async function assignEventsToCategory ( ctx , categoryId , eventIds ) {
176 return req (ctx, `/events/v1/categories/${ categoryId }/events` , { body: { eventId: eventIds } });
177 }
178
179 // Events binds mainImage by Wix Media file ID — an external url must be imported first
180 // (a raw url as the id stores 200 but renders nothing); a plan `imagePrompt` is generated
181 // (Wix AI, 1 credit) then imported. Both live in the shared util (parallel, resilient,
182 // never blocks the seed).
183 export { importImage } from "../../shared/seed/images.mjs" ;
184
185 // mainImage is an Image OBJECT; height/width are REQUIRED or it won't render. Events V3 uses
186 // NO revision — partial PATCH keyed by event.id. Works before OR after publish.
187 // docs: https://dev.wix.com/docs/api-reference/business-solutions/events/event-management/events-v3/update-event.md
188 export async function setEventMainImage ( ctx , it ) {
189 return req (ctx, `/events/v3/events/${ it . eventId }` , {
190 method: "PATCH" ,
191 body: {
192 event: {
193 id: it.eventId,
194 mainImage: { id: it.id, url: it.url, height: it.height ?? 1024 , width: it.width ?? 1024 , altText: it.altText },
195 },
196 fields: [ "DETAILS" ], // mainImage reads back only under DETAILS
197 },
198 });
199 }
200
201 /**
202 * ONE-CALL seed: install → site currency → per event create DRAFT → tiers → publish →
203 * categories → images, ids threaded in memory. The default path.
204 */
205 export async function setupEvents ( ctx , { events = [] } = {}) {
206 await installEventsApp (ctx);
207 const siteCurrency = await getSiteCurrency (ctx);
208
209 const created = [];
210 for ( const ev of events) {
211 const e = await createEvent (ctx, { ... ev, currency: ev.currency ?? siteCurrency });
212 if ( ! e.id) throw new Error ( `Event "${ ev . title }" was not created — no id returned.` );
213 const tiers = ev.type === "TICKETING" && ev.ticketTiers?. length
214 ? await createTicketTiers (ctx, e.id, ev.ticketTiers. map (( t ) => ({ ... t, currency: t.currency ?? siteCurrency })))
215 : [];
216 await publishEvent (ctx, e.id);
217 created. push ({ ... e, category: ev.category, imageUrl: ev.imageUrl, imagePrompt: ev.imagePrompt, ticketCount: tiers. length });
218 }
219
220 const names = [ ...new Set (created. map (( e ) => e.category). filter (Boolean))];
221 const categories = names. length ? await createEventCategories (ctx, names) : [];
222 for ( const c of categories) {
223 const eventIds = created. filter (( e ) => e.category === c.name). map (( e ) => e.id);
224 if (eventIds. length ) await assignEventsToCategory (ctx, c.id, eventIds);
225 }
226
227 // Pass 2 — images: resolve (import by url / generate by prompt) in one parallel wave, then
228 // attach. Failures leave the event text-only; the seed's exit never depends on images.
229 const files = await resolveItemImages (ctx, created. map (( e ) => ({
230 url: e.imageUrl,
231 path: e.imagePath,
232 prompt: e.imagePrompt,
233 displayName: `${ e . slug || "event"}.png` ,
234 })));
235 let imagesAttached = 0 ;
236 for ( let i = 0 ; i < created. length ; i ++ ) {
237 const e = created[i];
238 if ( ! files[i]) continue ;
239 try {
240 await setEventMainImage (ctx, { eventId: e.id, id: files[i].id, url: files[i].url, height: 1024 , width: 1024 , altText: e.slug });
241 imagesAttached ++ ;
242 } catch {
243 /* never block on image failure — the event stays text-only */
244 }
245 }
246
247 return {
248 events: created. map (( e ) => ({ id: e.id, slug: e.slug, ticketCount: e.ticketCount, category: e.category ?? null })),
249 categories,
250 imagesAttached,
251 // Completing a PAID purchase needs a premium plan + a configured payment method in the
252 // dashboard — not a seeding failure; surface it to the owner. Free/RSVP need neither.
253 notes: created. some (( e ) => e.ticketCount > 0 )
254 ? [ "Paid tickets require a premium plan + a configured payment method in the dashboard to complete a purchase." ]
255 : [],
256 };
257 }
258
259 // ---- CLI entry ----------------------------------------------------------------------------------
260
261 const invokedDirectly = process.argv[ 1 ] && import . meta .url. endsWith (process.argv[ 1 ]. split ( "/" ). pop ());
262 if (invokedDirectly) {
263 const planPath = process.argv[ 2 ];
264 if ( ! planPath) {
265 console. error ( "usage: node seed-events.mjs <plan.json> (run from the project root)" );
266 process. exit ( 1 );
267 }
268 const plan = JSON . parse ( readFileSync (planPath, "utf8" ));
269 const ctx = makeCtx ();
270 setupEvents (ctx, plan)
271 . then (( result ) => console. log ( JSON . stringify (result, null , 2 )))
272 . catch (( e ) => {
273 console. error (e.message);
274 process. exit ( 1 );
275 });
276 }