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