Setting the file. One moment.
Seed Cms · Wix Headless Fast · wix/skills · Skills Docs
ContentsBack to the top of the page This file
Number 7.34
Position 34 of 148
Type JavaScript
Size 14 KB
Lines 324 references/cms/seed/ seed-cms.mjs
JavaScript · 324 lines · 14 KB
15 // "items": [{ "<fieldKey>": value, ... }] }] }
16 // Reference fields: order collections so targets come FIRST. A REFERENCE value is the
17 // target item's index in ITS collection's items array; MULTI_REFERENCE is an array of
18 // indices. An IMAGE value is a verified https url STRING, { "prompt": "..." }
19 // (AI-generated), or { "path": "..." } (a local file the user supplied, uploaded).
20 //
21 // Seeding is ADDITIVE — never deletes or overwrites existing content. Unexpected shapes →
22 // read the live API reference; authoritative source recipe:
23 // wix-headless/references/inline-recipes/setup-cms.md.
24 import { execFileSync } from "node:child_process" ;
25 import { readFileSync } from "node:fs" ;
26 import { resolveItemImages } from "../../shared/seed/images.mjs" ;
27
28 const API = "https://www.wixapis.com" ;
29 const WIX_DATA_APP_ID = "e593b0bd-b783-45b8-97c2-873d42aacaf4" ;
30
31 // Public collection default: read MUST be "ANYONE" or a visitor query silently returns 0
32 // items (the single most common "empty page" cause). Other presets: SEED.md.
33 const DEFAULT_PERMISSIONS = { insert: "ADMIN" , update: "ADMIN" , remove: "ADMIN" , read: "ANYONE" };
34
35 export function makeCtx ({ cwd = process. cwd () } = {}) {
36 const config = JSON . parse ( readFileSync ( `${ cwd }/wix.config.json` , "utf8" ));
37 const siteId = config.siteId ?? config.projectId;
38 if ( ! siteId) throw new Error ( "wix.config.json has no siteId — is this a Wix CLI project?" );
39 const token = execFileSync ( "npx" , [ "@wix/cli@latest" , "token" , "--site" , siteId], {
40 encoding: "utf8" ,
41 cwd,
42 }). trim ();
43 if ( ! token) throw new Error ( "The Wix CLI returned no token — run `npx @wix/cli@latest login` first." );
44 return { token, siteId };
45 }
46
47 async function req ( ctx , path , { method = "POST" , body } = {}) {
48 const res = await fetch ( API + path, {
49 method,
50 headers: {
51 Authorization: `Bearer ${ ctx . token }` ,
52 "wix-site-id" : ctx.siteId,
53 "Content-Type" : "application/json" ,
54 },
55 body: body ? JSON . stringify (body) : undefined ,
56 });
57 const json = await res. json (). catch (() => ({}));
58 if ( ! res.ok) throw new Error ( `${ method } ${ path } -> ${ res . status }: ${ JSON . stringify ( json ). slice ( 0 , 400 ) }` );
59 return json;
60 }
61
62 const sleep = ( ms ) => new Promise (( r ) => setTimeout (r, ms));
63
64 // A fresh site's Wix Data backend can transiently fail its FIRST calls while provisioning —
65 // 403, 400 WDE0117 ("MetaSite not found"), or 5xx. Retry the same body ONCE after ~3s, then
66 // fail loud (never loop).
67 async function reqRetryOnce ( ctx , path , opts ) {
68 try {
69 return await req (ctx, path, opts);
70 } catch (e) {
71 const m = String (e.message);
72 if ( /-> (403 | 5 \d\d ):/ . test (m) || m. includes ( "WDE0117" )) {
73 await sleep ( 3000 );
74 return req (ctx, path, opts);
75 }
76 throw e;
77 }
78 }
79
80 // ---- operations ----------------------------------------------------------------------------------
81
82 // Idempotent; strictly only needed when a data call errors WDE0110 (app not installed).
83 // docs: https://dev.wix.com/docs/api-reference/articles/work-with-wix-apis/platform/about-apps-created-by-wix.md
84 export async function installDataApp ( ctx ) {
85 try {
86 await req (ctx, "/apps-installer-service/v1/app-instance/install" , { body: {
87 tenant: { tenantType: "SITE" , id: ctx.siteId },
88 appInstance: { appDefId: WIX_DATA_APP_ID , enabled: true },
89 } });
90 } catch {
91 /* already installed is fine */
92 }
93 }
94
95 // The binding key is referencedCollectionId — the docs' stale `referencedCollection` is
96 // accepted with a 200 but stores an EMPTY target, leaving every later link silently dead.
97 // MULTI_REFERENCE additionally REQUIRES referencingFieldKey — the auto-created back-reference
98 // field on the referenced collection (400 without it); synthesized from the owning
99 // collection + field when the plan doesn't name one.
100 function buildField ( f , collectionId ) {
101 const out = { key: f.key, displayName: f.displayName ?? f.key, type: f.type };
102 if (f.type === "MULTI_REFERENCE" || f.type === "REFERENCE" ) {
103 if ( ! f.referencedCollectionId) {
104 throw new Error ( `field "${ f . key }": ${ f . type } requires referencedCollectionId` );
105 }
106 out.typeMetadata =
107 f.type === "MULTI_REFERENCE"
108 ? { multiReference: {
109 referencedCollectionId: f.referencedCollectionId,
110 referencingFieldKey: f.referencingFieldKey ?? `${ collectionId }_${ f . key }` . replace ( / [ ^ a-zA-Z0-9_] / g , "_" ),
111 referencingDisplayName: f.referencingDisplayName ?? `${ displayNameOf ( collectionId ) } (${ f . key })` ,
112 } }
113 : { reference: { referencedCollectionId: f.referencedCollectionId } };
114 }
115 return out;
116 }
117
118 const displayNameOf = ( id ) => String (id). replace ( / [-_] + / g , " " ). replace ( / \b \w / g , ( c ) => c. toUpperCase ());
119
120 // The permissions block is MANDATORY. 409 WDE0104 (already exists) is fine — additive.
121 // docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-collections/create-data-collection.md
122 export async function createCollection ( ctx , { id , displayName , fields = [], permissions }) {
123 try {
124 await reqRetryOnce (ctx, "/wix-data/v2/collections" , { body: { collection: {
125 id,
126 displayName: displayName ?? id,
127 fields: fields. map (( f ) => buildField (f, id)),
128 permissions: permissions ?? DEFAULT_PERMISSIONS ,
129 } } });
130 return { id, created: true };
131 } catch (e) {
132 const m = String (e.message);
133 if (m. includes ( "WDE0104" ) || m. includes ( "-> 409:" )) return { id, created: false };
134 throw e;
135 }
136 }
137
138 // An IMAGE field stores a permanent Wix Media URL — an external url must be imported first;
139 // a { "prompt": ... } value is generated (Wix AI, 1 credit) then imported. Both live in the
140 // shared util (parallel, resilient, never blocks the seed).
141 export { importImage } from "../../shared/seed/images.mjs" ;
142
143 // Ids come from results[].dataItem.id (there is no results[].item key).
144 // docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/bulk-insert-data-items.md
145 export async function bulkInsertItems ( ctx , dataCollectionId , dataItems ) {
146 const r = await reqRetryOnce (ctx, "/wix-data/v2/bulk/items/insert" , { body: {
147 dataCollectionId,
148 dataItems: dataItems. map (( data ) => ({ data })),
149 returnEntity: true ,
150 } });
151 const results = [ ... (r.results ?? [])]. sort (
152 ( a , b ) => (a.itemMetadata?.originalIndex ?? 0 ) - (b.itemMetadata?.originalIndex ?? 0 ),
153 );
154 return {
155 ids: results. map (( res ) => res.dataItem?.id ?? res.itemMetadata?.id),
156 failures: r.bulkActionMetadata?.totalFailures ?? 0 ,
157 };
158 }
159
160 // Body key is dataItemReferences with referringItemFieldName/referringItemId/referencedItemId
161 // — the natural-looking `references` shape is rejected with 400 WDE0080.
162 // docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/bulk-insert-data-item-references.md
163 export async function insertReferences ( ctx , dataCollectionId , dataItemReferences ) {
164 if ( ! dataItemReferences. length ) return 0 ;
165 const r = await req (ctx, "/wix-data/v2/bulk/items/insert-references" , {
166 body: { dataCollectionId, dataItemReferences },
167 });
168 return r.bulkActionMetadata?.totalSuccesses ?? dataItemReferences. length ;
169 }
170
171 // A 200 on insert does NOT prove persistence — query back and count.
172 // docs: https://dev.wix.com/docs/api-reference/business-solutions/cms/data-items/query-data-items.md
173 export async function verifyItems ( ctx , dataCollectionId ) {
174 const r = await reqRetryOnce (ctx, "/wix-data/v2/items/query" , { body: { dataCollectionId } });
175 return (r.dataItems ?? []). length ;
176 }
177
178 // plan item -> insert `data`, per field type. MULTI_REFERENCE values are silently dropped by
179 // the insert endpoint (200, no error) — stripped here and wired after; a single REFERENCE is
180 // set at insert as the target item's _id. DATE/DATETIME wrap as { "$date": iso }; RICH_TEXT
181 // is an HTML string stored verbatim; IMAGE values were resolved to Wix Media files up front
182 // (imageFiles, keyed by imageKey) — a failed image leaves the field unset.
183 function buildItemData ( col , item , idsByCollection , multiRefs , itemIndex , counters , imageFiles ) {
184 const fieldsByKey = new Map ((col.fields ?? []). map (( f ) => [f.key, f]));
185 const data = {};
186 for ( const [ key , value ] of Object. entries (item)) {
187 const f = fieldsByKey. get (key);
188 // A key the schema doesn't have is silently dropped by the API — fail loud instead.
189 if ( ! f) throw new Error ( `"${ col . id }" item ${ itemIndex }: key "${ key }" is not in the collection's fields` );
190 if (value == null ) continue ;
191 if (f.type === "MULTI_REFERENCE" ) {
192 for ( const targetIndex of []. concat (value)) {
193 multiRefs. push ({ itemIndex, fieldKey: key, targetCollectionId: f.referencedCollectionId, targetIndex });
194 }
195 continue ;
196 }
197 if (f.type === "REFERENCE" ) {
198 const targetId = (idsByCollection. get (f.referencedCollectionId) ?? [])[value];
199 if ( ! targetId) {
200 throw new Error (
201 `"${ col . id }" item ${ itemIndex }: REFERENCE "${ key }" -> "${ f . referencedCollectionId }"[${ value }] — ` +
202 `target not created yet (order collections so targets come first)` ,
203 );
204 }
205 data[key] = targetId;
206 continue ;
207 }
208 if (f.type === "IMAGE" ) {
209 const file = imageFiles. get ( imageKey (col.id, itemIndex, key));
210 if (file) {
211 data[key] = file.url;
212 counters.imagesImported ++ ;
213 }
214 continue ;
215 }
216 if (f.type === "DATE" || f.type === "DATETIME" ) {
217 data[key] = { $date: new Date (value). toISOString () };
218 continue ;
219 }
220 data[key] = value;
221 }
222 return data;
223 }
224
225 const imageKey = ( colId , itemIndex , fieldKey ) => `${ colId } ${ itemIndex } ${ fieldKey }` ;
226
227 // Every IMAGE value across the plan (a url string or { prompt }), resolved to Wix Media files
228 // in ONE parallel wave before any insert. Returns Map<imageKey, { id, url } | absent>.
229 async function resolveAllImages ( ctx , collections ) {
230 const keys = [];
231 const specs = [];
232 for ( const col of collections) {
233 const imageFields = new Set ((col.fields ?? []). filter (( f ) => f.type === "IMAGE" ). map (( f ) => f.key));
234 (col.items ?? []). forEach (( item , i ) => {
235 for ( const [ key , value ] of Object. entries (item)) {
236 if ( ! imageFields. has (key) || value == null ) continue ;
237 keys. push ( imageKey (col.id, i, key));
238 specs. push ({
239 url: typeof value === "string" ? value : undefined ,
240 path: typeof value === "object" ? value.path : undefined ,
241 prompt: typeof value === "object" ? value.prompt : undefined ,
242 displayName: `${ col . id }-${ i }-${ key }.png` ,
243 });
244 }
245 });
246 }
247 const files = specs. length ? await resolveItemImages (ctx, specs) : [];
248 const out = new Map ();
249 keys. forEach (( k , i ) => { if (files[i]) out. set (k, files[i]); });
250 return out;
251 }
252
253 /**
254 * ONE-CALL seed: install → resolve every IMAGE value in one parallel wave → per collection
255 * (in plan order): create → bulk-insert → wire multi-references → verify; ids threaded in
256 * memory. The default path.
257 */
258 export async function setupCms ( ctx , { collections = [] } = {}) {
259 await installDataApp (ctx);
260
261 const imageFiles = await resolveAllImages (ctx, collections);
262
263 const idsByCollection = new Map ();
264 const out = { collections: [] };
265 for ( const col of collections) {
266 const created = await createCollection (ctx, col);
267
268 const counters = { imagesImported: 0 };
269 const multiRefs = [];
270 const dataItems = [];
271 const planItems = col.items ?? [];
272 for ( let i = 0 ; i < planItems. length ; i ++ ) {
273 dataItems. push ( buildItemData (col, planItems[i], idsByCollection, multiRefs, i, counters, imageFiles));
274 }
275
276 let ids = [];
277 let failures = 0 ;
278 if (dataItems. length ) ({ ids, failures } = await bulkInsertItems (ctx, col.id, dataItems));
279 idsByCollection. set (col.id, ids);
280
281 const refs = multiRefs. map (({ itemIndex , fieldKey , targetCollectionId , targetIndex }) => {
282 const referringItemId = ids[itemIndex];
283 const referencedItemId = (idsByCollection. get (targetCollectionId) ?? [])[targetIndex];
284 if ( ! referringItemId || ! referencedItemId) {
285 throw new Error (
286 `"${ col . id }" item ${ itemIndex }: multi-reference "${ fieldKey }" -> "${ targetCollectionId }"[${ targetIndex }] ` +
287 `has no created id (order collections so targets come first)` ,
288 );
289 }
290 return { referringItemFieldName: fieldKey, referringItemId, referencedItemId };
291 });
292 const referencesLinked = await insertReferences (ctx, col.id, refs);
293
294 out.collections. push ({
295 id: col.id,
296 created: created.created,
297 inserted: ids. length ,
298 failures,
299 imagesImported: counters.imagesImported,
300 referencesLinked,
301 itemsInCollection: await verifyItems (ctx, col.id),
302 });
303 }
304 return out;
305 }
306
307 // ---- CLI entry ----------------------------------------------------------------------------------
308
309 const invokedDirectly = process.argv[ 1 ] && import . meta .url. endsWith (process.argv[ 1 ]. split ( "/" ). pop ());
310 if (invokedDirectly) {
311 const planPath = process.argv[ 2 ];
312 if ( ! planPath) {
313 console. error ( "usage: node seed-cms.mjs <plan.json> (run from the project root)" );
314 process. exit ( 1 );
315 }
316 const plan = JSON . parse ( readFileSync (planPath, "utf8" ));
317 const ctx = makeCtx ();
318 setupCms (ctx, plan)
319 . then (( result ) => console. log ( JSON . stringify (result, null , 2 )))
320 . catch (( e ) => {
321 console. error (e.message);
322 process. exit ( 1 );
323 });
324 }