Setting the file. One moment.
Dry Run Target · Rp Target Wix · wix/skills · Skills Docs
ContentsBack to the top of the page 41.10
Multilingual Restore
— line 57
This file
Number 41.140
Position 140 of 174
Type JavaScript
Size 15 KB
Lines 212 lib/ dry-run-target.js
JavaScript · 212 lines · 15 KB
// of the endpoints below (the generic shapes in wix-writers.js then apply).
14 const crypto = require ( 'node:crypto' );
15
16 const MAX_SITES = 16 ;
17 const sites = new Map ();
18 function siteFor ( scope ) {
19 let site = sites. get (scope);
20 if ( ! site) {
21 site = new Map ();
22 sites. set (scope, site);
23 while (sites.size > MAX_SITES ) sites. delete (sites. keys (). next ().value);
24 }
25 return site;
26 }
27 function table ( site , kind ) {
28 if ( ! site. has (kind)) site. set (kind, new Map ());
29 return site. get (kind);
30 }
31 const clone = value => (value === undefined ? undefined : structuredClone (value));
32 // Every primitive in the filter equals the entity's value; `{ $eq: v }` counts as v.
33 function matching ( rows , filter ) {
34 const wanted = Object. entries (filter || {}). map (([ key , value ]) => [key, value && typeof value === 'object' && '$eq' in value ? value.$eq : value])
35 . filter (([, value ]) => value === null || typeof value !== 'object' );
36 return rows. filter ( row => wanted. every (([ key , value ]) => row[key] === value));
37 }
38 const nextRevision = revision => String ( Number (revision || 0 ) + 1 );
39
40 function handlers () {
41 // Helpers bound per call through `c`: c.body, c.query, c.id(), c.get/put/del/list(kind, ...).
42 const create = ( kind , field , extra = () => ({})) => c => {
43 const entity = { ... clone ((c.body && c.body[field]) || {}), id: c. id (), ... extra (c) };
44 c. put (kind, entity.id, entity);
45 return { [field]: entity };
46 };
47 const read = ( kind , field ) => ( c , [, id ]) => ({ [field]: c. get (kind, decodeURIComponent (id)) || { id: decodeURIComponent (id), _dryRunPlaceholder: true } });
48 const update = ( kind , field , change ) => ( c , [, id ]) => {
49 const key = decodeURIComponent (id), before = c. get (kind, key) || { id: key };
50 const entity = { ... before, ... change (c, before), id: key, revision: nextRevision (before.revision) };
51 c. put (kind, key, entity);
52 return { [field]: entity };
53 };
54 const list = ( kind , field , filterOf = c => c.body && c.body.query && c.body.query.filter, extra = {}) => c => ({ [field]: matching (c. list (kind), filterOf (c)), ... extra });
55 const lastPage = { pagingMetadata: { hasNext: false } };
56 const addOnGroupDetails = group => ({ groupId: group.id, name: group.name, addOns: group.addOnIds. map ( addOnId => ({ addOnId })) });
57 const noContactTags = id => ({ id, tags: { privateTags: { tagIds: [] } } });
58 const ITEM_SEO_EMPTY = { tags: [], resolvedTags: [], focusKeywords: [] };
59 return [
60 // Events
61 [ 'POST' , / ^ \/ events \/ v1 \/ categories $ / , create ( 'event-category' , 'category' )],
62 [ 'POST' , / ^ \/ events \/ v1 \/ categories \/ query $ / , c => ({ categories: c. list ( 'event-category' ) })],
63 [ 'GET' , / ^ \/ events \/ v1 \/ categories \/ ( [ ^ /] + ) $ / , ( c , [, eventId ]) => ({ categories: (c. get ( 'event-categories' , decodeURIComponent (eventId)) || []). map ( id => c. get ( 'event-category' , id) || { id }) })],
64 [ 'POST' , / ^ \/ events \/ v1 \/ bulk \/ categories \/ events $ / , c => {
65 for ( const eventId of c.body.eventId || []) c. put ( 'event-categories' , eventId, [ ...new Set ([ ... (c. get ( 'event-categories' , eventId) || []), ... (c.body.categoryId || [])])]);
66 return {};
67 }],
68 [ 'POST' , / ^ \/ events \/ v3 \/ events $ / , create ( 'event' , 'event' , c => ({ status: c.body.draft ? 'DRAFT' : 'UPCOMING' }))],
69 [ 'POST' , / ^ \/ events \/ v3 \/ events \/ query $ / , c => ({ events: matching (c. list ( 'event' ). filter ( event => event.status !== 'DRAFT' ), c.body.query && c.body.query.filter) })],
70 [ 'GET' , / ^ \/ events \/ v3 \/ events \/ ( [ ^ /] + ) $ / , read ( 'event' , 'event' )],
71 [ 'POST' , / ^ \/ events \/ v3 \/ ticket-definitions $ / , create ( 'ticket-definition' , 'ticketDefinition' )],
72 [ 'POST' , / ^ \/ events \/ v3 \/ ticket-definitions \/ query $ / , list ( 'ticket-definition' , 'ticketDefinitions' )],
73 [ 'GET' , / ^ \/ events \/ v3 \/ ticket-definitions \/ ( [ ^ /] + ) $ / , read ( 'ticket-definition' , 'ticketDefinition' )],
74 [ 'POST' , / ^ \/ events \/ v2 \/ rsvps $ / , create ( 'rsvp' , 'rsvp' )],
75 [ 'POST' , / ^ \/ events \/ v2 \/ rsvps \/ query $ / , list ( 'rsvp' , 'rsvps' , undefined , lastPage)],
76 [ 'GET' , / ^ \/ events \/ v2 \/ rsvps \/ ( [ ^ /] + ) $ / , read ( 'rsvp' , 'rsvp' )],
77 // Forms. An entry answers CONFIRMED: there is no spam pass to wait for in a dry run.
78 [ 'POST' , / ^ \/ form-schema-service \/ v4 \/ forms $ / , create ( 'form' , 'form' , () => ({ revision: '1' }))],
79 [ 'POST' , / ^ \/ form-schema-service \/ v4 \/ forms \/ query $ / , list ( 'form' , 'forms' , undefined , lastPage)],
80 [ 'POST' , / ^ \/ forms \/ v4 \/ submissions $ / , create ( 'form-submission' , 'submission' , () => ({ status: 'CONFIRMED' }))],
81 [ 'GET' , / ^ \/ forms \/ v4 \/ submissions \/ ( [ ^ /] + ) $ / , read ( 'form-submission' , 'submission' )],
82 [ 'POST' , / ^ \/ form-spam-submission-report-service \/ v4 \/ form-spam-submission-reports \/ namespace \/ query $ / , () => ({ formSpamSubmissionReports: [], ... lastPage })],
83 // Gift cards
84 [ 'POST' , / ^ \/ gift-cards \/ v1 \/ gift-cards $ / , create ( 'gift-card' , 'giftCard' )],
85 [ 'POST' , / ^ \/ gift-cards \/ v1 \/ gift-cards \/ query $ / , list ( 'gift-card' , 'giftCards' )],
86 [ 'POST' , / ^ \/ gift-cards \/ v1 \/ gift-card-products $ / , create ( 'gift-card-product' , 'giftCardProduct' , () => ({ revision: '1' }))],
87 [ 'POST' , / ^ \/ gift-cards \/ v1 \/ gift-card-products \/ query $ / , c => ({ giftCardProducts: c. list ( 'gift-card-product' ) })],
88 [ 'PATCH' , / ^ \/ gift-cards \/ v1 \/ gift-card-products \/ ( [ ^ /] + ) $ / , update ( 'gift-card-product' , 'giftCardProduct' , c => clone (c.body.giftCardProduct || {}))],
89 [ 'GET' , / ^ \/ gift-cards \/ v1 \/ gift-card-products \/ ( [ ^ /] + ) $ / , read ( 'gift-card-product' , 'giftCardProduct' )],
90 // Bookings add-ons and their service groups. These group paths sit under /bookings/v2/services,
91 // so they must be answered here, before the generic Bookings service shape.
92 [ 'POST' , / ^ \/ addons \/ v1 \/ add-ons $ / , create ( 'add-on' , 'addOn' )],
93 [ 'POST' , / ^ \/ addons \/ v1 \/ add-ons \/ query $ / , list ( 'add-on' , 'addOns' )],
94 [ 'GET' , / ^ \/ addons \/ v1 \/ add-ons \/ ( [ ^ /] + ) $ / , read ( 'add-on' , 'addOn' )],
95 [ 'DELETE' , / ^ \/ addons \/ v1 \/ add-ons \/ ( [ ^ /] + ) $ / , ( c , [, id ]) => { c. del ( 'add-on' , decodeURIComponent (id)); return {}; }],
96 [ 'POST' , / ^ \/ bookings \/ v2 \/ services \/ add-on-groups \/ create $ / , c => {
97 const group = { id: c. id (), serviceId: c.body.serviceId, name: c.body.addOnGroup && c.body.addOnGroup.name, addOnIds: [ ... ((c.body.addOnGroup && c.body.addOnGroup.addOnIds) || [])] };
98 c. put ( 'add-on-group' , group.id, group);
99 return { addOnGroup: { id: group.id, name: group.name, addOnIds: [ ... group.addOnIds] } };
100 }],
101 [ 'POST' , / ^ \/ bookings \/ v2 \/ services \/ add-on-groups \/ set-add-ons-for-group $ / , c => {
102 const group = { ... (c. get ( 'add-on-group' , c.body.groupId) || { id: c.body.groupId, serviceId: c.body.serviceId }), addOnIds: [ ... (c.body.addOnIds || [])] };
103 c. put ( 'add-on-group' , group.id, group);
104 return { addOnGroup: { id: group.id, name: group.name, addOnIds: [ ... group.addOnIds] } };
105 }],
106 [ 'POST' , / ^ \/ bookings \/ v2 \/ services \/ add-on-groups \/ list-add-on-groups-by-service-id $ / , c => ({ addOnGroupsDetails: c. list ( 'add-on-group' ). filter ( group => group.serviceId === c.body.serviceId). map (addOnGroupDetails) })],
107 [ 'POST' , / ^ \/ bookings \/ v2 \/ services \/ add-on-groups \/ delete $ / , c => { c. del ( 'add-on-group' , c.body.addOnGroupId); return {}; }],
108 // Bookings: create, confirm, cancel, and Query Extended Bookings (the only read).
109 [ 'POST' , / ^ \/ bookings \/ bookings-reader \/ v2 \/ extended-bookings \/ query $ / , c => ({ extendedBookings: matching (c. list ( 'booking' ), c.body.query && c.body.query.filter). map ( booking => ({ booking })) })],
110 [ 'POST' , / ^ \/ bookings \/ v2 \/ bookings $ / , create ( 'booking' , 'booking' , () => ({ revision: '1' }))],
111 [ 'POST' , / ^ \/ bookings \/ v2 \/ bookings \/ ( [ ^ /] + ) \/ confirm $ / , update ( 'booking' , 'booking' , c => ({ status: 'CONFIRMED' , ... (c.body.paymentStatus ? { paymentStatus: c.body.paymentStatus } : {}) }))],
112 [ 'POST' , / ^ \/ bookings \/ v2 \/ bookings \/ ( [ ^ /] + ) \/ cancel $ / , update ( 'booking' , 'booking' , () => ({ status: 'CANCELED' }))],
113 // Pricing Plans orders. The create answers settled (no DRAFT): there is nothing to wait for.
114 [ 'POST' , / ^ \/ pricing-plans \/ v2 \/ checkout \/ orders \/ offline $ / , c => {
115 const id = c. id (), { planId , memberId , startDate , paid } = c.body;
116 const order = { id, planId, buyer: { memberId }, startDate, subscriptionId: `${ id }-subscription` , status: Date. parse (startDate) > Date. now () ? 'PENDING' : 'ACTIVE' , lastPaymentStatus: paid ? 'PAID' : 'UNPAID' };
117 c. put ( 'plan-order' , id, order);
118 return { order };
119 }],
120 [ 'GET' , / ^ \/ pricing-plans \/ v2 \/ orders $ / , c => ({ orders: c. list ( 'plan-order' ). filter ( order => order.buyer.memberId === c.query. get ( 'buyerIds' ) && order.planId === c.query. get ( 'planIds' )) })],
121 [ 'GET' , / ^ \/ pricing-plans \/ v2 \/ orders \/ ( [ ^ /] + ) $ / , read ( 'plan-order' , 'order' )],
122 [ 'POST' , / ^ \/ pricing-plans \/ v2 \/ orders \/ ( [ ^ /] + ) \/ pause $ / , ( c , match ) => { update ( 'plan-order' , 'order' , () => ({ status: 'PAUSED' }))(c, match); return {}; }],
123 // Contact labels (find-or-create by name) and labels on a contact.
124 [ 'POST' , / ^ \/ contacts \/ v4 \/ labels $ / , c => {
125 const displayName = String (c.body.displayName || '' );
126 const key = `custom.${ displayName . toLowerCase (). replace ( / [ ^ a-z0-9] + / g , '-' ). replace ( / ^ - +| - +$ / g , '' ) || 'label'}` ;
127 const existing = c. get ( 'label' , key);
128 if (existing) return { label: existing, newLabel: false };
129 const label = { key, tagId: crypto. createHash ( 'sha256' ). update ( `${ c . scope }:${ key }` ). digest ( 'hex' ). slice ( 0 , 5 ), displayName, labelType: 'USER_DEFINED' };
130 c. put ( 'label' , key, label);
131 return { label, newLabel: true };
132 }],
133 [ 'GET' , / ^ \/ contacts \/ v4 \/ labels \/ ( [ ^ /] + ) $ / , ( c , [, key ]) => ({ label: c. get ( 'label' , decodeURIComponent (key)) || { key: decodeURIComponent (key), _dryRunPlaceholder: true } })],
134 [ 'POST' , / ^ \/ contacts \/ v5 \/ contacts \/ ( [ ^ /] + ) \/ update-tags $ / , ( c , [, id ]) => {
135 const contactId = decodeURIComponent (id), contact = c. get ( 'contact-tags' , contactId) || noContactTags (contactId);
136 const add = (c.body.assignTags && c.body.assignTags.privateTags && c.body.assignTags.privateTags.tagIds) || [];
137 contact.tags.privateTags.tagIds = [ ...new Set ([ ... contact.tags.privateTags.tagIds, ... add])];
138 c. put ( 'contact-tags' , contactId, contact);
139 return { contact };
140 }],
141 [ 'GET' , / ^ \/ contacts \/ v5 \/ contacts \/ ( [ ^ /] + ) $ / , ( c , [, id ]) => ( String (c.query. get ( 'fields' ) || '' ). split ( ',' ). includes ( 'TAGS' )
142 ? { contact: c. get ( 'contact-tags' , decodeURIComponent (id)) || noContactTags ( decodeURIComponent (id)) } : null )],
143 // Member About
144 [ 'POST' , / ^ \/ members \/ v2 \/ abouts \/ query $ / , list ( 'member-about' , 'memberAbouts' )],
145 [ 'POST' , / ^ \/ members \/ v2 \/ abouts $ / , create ( 'member-about' , 'memberAbout' , () => ({ revision: '1' }))],
146 [ 'PATCH' , / ^ \/ members \/ v2 \/ abouts \/ ( [ ^ /] + ) $ / , update ( 'member-about' , 'memberAbout' , c => clone (c.body.memberAbout || {}))],
147 [ 'GET' , / ^ \/ members \/ v2 \/ abouts \/ ( [ ^ /] + ) $ / , read ( 'member-about' , 'memberAbout' )],
148 // SEO (Promote SEO V1). Product SEO keeps the generic shape: it reads STORES_PRODUCT items
149 // that this module never writes.
150 [ 'GET' , / ^ \/ promote \/ seo \/ v1 \/ item-seo-tags \/ ( [ ^ /] + ) \/ ( [ ^ /] + ) $ / , ( c , [, itemType , itemId ]) => ( decodeURIComponent (itemType) === 'STORES_PRODUCT' ? null
151 : { itemSeoTags: c. get ( 'item-seo-tags' , `${ decodeURIComponent ( itemType ) }/${ decodeURIComponent ( itemId ) }` ) || clone ( ITEM_SEO_EMPTY ) })],
152 [ 'POST' , / ^ \/ promote \/ seo \/ v1 \/ bulk \/ item-seo-tags \/ set $ / , c => {
153 const results = (c.body.entries || []). map (( entry , originalIndex ) => {
154 const key = `${ c . body . itemType }/${ entry . itemId }` , stored = c. get ( 'item-seo-tags' , key) || clone ( ITEM_SEO_EMPTY );
155 const mask = String (entry.fieldMask || '' ). split ( ',' );
156 if (mask. includes ( 'tags' )) Object. assign (stored, { tags: clone (entry.itemSeoTags.tags || []), resolvedTags: (entry.itemSeoTags.tags || []). map ( tag => ({ tag: clone (tag), source: 'TAG_SOURCE_ITEM' })) });
157 if (mask. includes ( 'focusKeywords' )) stored.focusKeywords = clone (entry.itemSeoTags.focusKeywords || []);
158 c. put ( 'item-seo-tags' , key, stored);
159 return { itemMetadata: { id: entry.itemId, originalIndex, success: true } };
160 });
161 return { results, bulkActionMetadata: { totalSuccesses: results. length , totalFailures: 0 , undetailedFailures: 0 } };
162 }],
163 // The site's allowed pattern variables are unknown without a live read: the answer says so.
164 [ 'GET' , / ^ \/ promote \/ seo \/ v1 \/ seo-patterns \/ ( [ ^ /] + ) \/ variables $ / , () => ({ variables: [], stateKnown: false , kind: 'wix_call_skipped' })],
165 [ 'GET' , / ^ \/ promote \/ seo \/ v1 \/ seo-patterns \/ ( [ ^ /] + ) $ / , ( c , [, pageType ]) => ({ seoPattern: c. get ( 'seo-pattern' , decodeURIComponent (pageType)) || {} })],
166 ... [ 'POST' , 'PATCH' ]. map ( method => [method, / ^ \/ promote \/ seo \/ v1 \/ seo-patterns \/ ( [ ^ /] + ) $ / , ( c , [, pageType ]) => {
167 const key = decodeURIComponent (pageType), before = c. get ( 'seo-pattern' , key) || {};
168 const seoPattern = { ... before, source: 'PATTERN_SOURCE_USER' , pattern: clone ((c.body.seoPattern && c.body.seoPattern.pattern) || {}) };
169 c. put ( 'seo-pattern' , key, seoPattern);
170 return { seoPattern };
171 }]),
172 [ 'GET' , / ^ \/ promote \/ seo \/ v1 \/ site-seo-tags $ / , c => ({ siteSeoTags: c. get ( 'site-seo-tags' , 'site' ) || { tags: [] } })],
173 [ 'PATCH' , / ^ \/ promote \/ seo \/ v1 \/ site-seo-tags $ / , c => {
174 const siteSeoTags = { tags: clone ((c.body.siteSeoTags && c.body.siteSeoTags.tags) || []) };
175 c. put ( 'site-seo-tags' , 'site' , siteSeoTags);
176 return { siteSeoTags };
177 }],
178 ];
179 }
180 const HANDLERS = handlers ();
181
182 function locate ( request ) {
183 let url;
184 try { url = new URL ( String (request.url || '' )); } catch { return null ; }
185 const method = String (request.method || '' ). toUpperCase (), pathname = url.pathname. replace ( / \/ +$ / , '' );
186 for ( const [ verb , pattern , answer ] of HANDLERS ) {
187 if (verb !== method) continue ;
188 const match = pattern. exec (pathname);
189 if (match) return { answer, match, query: url.searchParams };
190 }
191 return null ;
192 }
193 // `scope` names one dry run's site; `placeholderId()` mints the id a create answers with.
194 function respond ( request , { scope , placeholderId }) {
195 const found = locate (request);
196 if ( ! found) return null ;
197 const site = siteFor ( String (scope));
198 const c = {
199 scope: String (scope),
200 body: request.body || {},
201 query: found.query,
202 id: placeholderId,
203 get : ( kind , key ) => clone ( table (site, kind). get (key)),
204 put : ( kind , key , value ) => { table (site, kind). set (key, clone (value)); },
205 del : ( kind , key ) => { table (site, kind). delete (key); },
206 list : kind => [ ... table (site, kind). values ()]. map (clone),
207 };
208 const answer = found. answer (c, found.match);
209 return answer ? clone (answer) : null ;
210 }
211
212 module . exports = { respond };