@jskit-ai/connectors-catalog 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/docs/ai.md +3 -3
- package/docs/airtable.md +1 -1
- package/docs/algolia.md +2 -2
- package/docs/amazon-redshift.md +2 -2
- package/docs/apollo-io.md +2 -2
- package/docs/asana.md +2 -2
- package/docs/ashby.md +1 -1
- package/docs/attention.md +2 -2
- package/docs/aws-athena.md +1 -1
- package/docs/aws-credentials.md +3 -3
- package/docs/aws-s3.md +1 -1
- package/docs/bigquery.md +2 -2
- package/docs/brevo.md +2 -2
- package/docs/calendly.md +3 -3
- package/docs/canva.md +2 -2
- package/docs/chargebee.md +2 -2
- package/docs/clay.md +2 -2
- package/docs/clickhouse.md +1 -1
- package/docs/confidence-flags.md +1 -1
- package/docs/contentful.md +1 -1
- package/docs/dbt-semantic-layer.md +2 -2
- package/docs/elevenlabs.md +2 -2
- package/docs/firebase-cloud-messaging.md +3 -3
- package/docs/firecrawl.md +4 -4
- package/docs/fireflies.md +1 -1
- package/docs/gatewayapi.md +1 -1
- package/docs/gemini-enterprise.md +1 -1
- package/docs/github-api.md +1 -1
- package/docs/gitlab-api.md +1 -1
- package/docs/gmail.md +1 -1
- package/docs/gong.md +1 -1
- package/docs/google-ads.md +3 -3
- package/docs/google-analytics.md +2 -2
- package/docs/google-docs.md +1 -1
- package/docs/google-drive.md +1 -1
- package/docs/google-maps-platform.md +2 -2
- package/docs/google-search-console.md +1 -1
- package/docs/google-sheets.md +1 -1
- package/docs/google-slides.md +2 -2
- package/docs/granola.md +2 -2
- package/docs/hex.md +2 -2
- package/docs/heygen.md +2 -2
- package/docs/inngest.md +1 -1
- package/docs/klipy.md +1 -1
- package/docs/lightspeed.md +2 -2
- package/docs/linear.md +1 -1
- package/docs/linkedin.md +2 -2
- package/docs/mailgun.md +1 -1
- package/docs/microsoft-excel.md +2 -2
- package/docs/microsoft-fabric.md +1 -1
- package/docs/microsoft-outlook.md +1 -1
- package/docs/microsoft-powerpoint.md +1 -1
- package/docs/microsoft-teams.md +1 -1
- package/docs/microsoft-word.md +1 -1
- package/docs/notion.md +1 -1
- package/docs/oura.md +3 -3
- package/docs/paddle.md +3 -3
- package/docs/pipedrive.md +2 -2
- package/docs/prestashop.md +1 -1
- package/docs/replicate.md +1 -1
- package/docs/resend.md +1 -1
- package/docs/salesforce.md +1 -1
- package/docs/sanity.md +3 -3
- package/docs/semrush.md +2 -2
- package/docs/sentry.md +1 -1
- package/docs/slack.md +1 -1
- package/docs/snowflake.md +5 -5
- package/docs/storyblok.md +1 -1
- package/docs/stripe.md +2 -2
- package/docs/tally.md +1 -1
- package/docs/telegram.md +2 -2
- package/docs/tiktok.md +3 -3
- package/docs/twilio.md +3 -3
- package/docs/twitch.md +2 -2
- package/docs/wave.md +3 -3
- package/docs/wix.md +13 -13
- package/docs/wordpress-com.md +1 -1
- package/docs/wordpress-self-hosted.md +1 -1
- package/docs/workday.md +2 -2
- package/docs/x-twitter.md +3 -3
- package/docs/xero.md +3 -3
- package/docs/zoho-books.md +2 -2
- package/docs/zoho-crm.md +2 -2
- package/package.json +4 -3
- package/patterns/ai-connections/PATTERN.md +16 -1
- package/patterns/ai-connections/example/ai-model.js +12 -3
- package/patterns/ai-connections/example/perplexity-answer.js +15 -4
- package/patterns/api-key-connection/PATTERN.md +9 -1
- package/patterns/api-key-connection/example/connections.js +5 -2
- package/patterns/aws-storage-queries/PATTERN.md +13 -3
- package/patterns/aws-storage-queries/example/s3-transfer.js +14 -3
- package/patterns/event-delivery/PATTERN.md +1 -1
- package/patterns/firebase-messaging/PATTERN.md +10 -0
- package/patterns/firebase-messaging/example/server/notifications.js +8 -0
- package/patterns/google-ads-search/PATTERN.md +18 -7
- package/patterns/google-ads-search/example/ads-setup.js +13 -5
- package/patterns/oauth-connection/PATTERN.md +1 -1
- package/patterns/paddle-catalogue/PATTERN.md +11 -2
- package/patterns/paddle-catalogue/example/create-products.js +5 -2
- package/patterns/public-image/PATTERN.md +10 -1
- package/patterns/public-image/example/logo-url.js +4 -1
- package/patterns/redshift-queries/PATTERN.md +1 -1
- package/patterns/source-scanning/PATTERN.md +13 -2
- package/patterns/source-scanning/example/source-scanner.js +9 -0
- package/src/server/ai.js +19 -5
- package/src/server/apify.js +2 -2
- package/src/server/asana.js +6 -6
- package/src/server/ashby.js +2 -1
- package/src/server/attention.js +12 -11
- package/src/server/awsTransport.js +6 -0
- package/src/server/bigquery.js +5 -4
- package/src/server/brevo.js +2 -1
- package/src/server/clay.js +2 -1
- package/src/server/clickhouse.js +2 -1
- package/src/server/confidence.js +6 -1
- package/src/server/connectorValues.js +33 -0
- package/src/server/databricks.js +7 -7
- package/src/server/dbt-semantic-layer.js +5 -5
- package/src/server/elevenlabs.js +2 -2
- package/src/server/firebase-cloud-messaging.js +2 -2
- package/src/server/gemini-enterprise.js +5 -5
- package/src/server/google-ads-search.js +68 -48
- package/src/server/google-ads.js +15 -17
- package/src/server/google-drive.js +7 -7
- package/src/server/google-maps-platform.js +4 -2
- package/src/server/google-search-console.js +4 -3
- package/src/server/google.js +8 -4
- package/src/server/googleAdsApi.js +14 -0
- package/src/server/granola.js +2 -2
- package/src/server/hubspot.js +2 -1
- package/src/server/incident-io.js +3 -2
- package/src/server/lexware.js +5 -4
- package/src/server/lightspeed.js +7 -5
- package/src/server/linkedin.js +2 -2
- package/src/server/mcpTransport.js +8 -4
- package/src/server/microsoft-fabric.js +3 -3
- package/src/server/microsoft-onedrive.js +2 -2
- package/src/server/microsoft-onenote.js +4 -3
- package/src/server/microsoft-outlook.js +6 -6
- package/src/server/microsoft-sharepoint.js +4 -2
- package/src/server/microsoft-teams.js +2 -1
- package/src/server/microsoft.js +20 -13
- package/src/server/notion.js +2 -1
- package/src/server/pipedrive.js +2 -1
- package/src/server/salesforce.js +12 -12
- package/src/server/semrush.js +9 -8
- package/src/server/snowflake.js +20 -20
- package/src/server/storyblok.js +2 -1
- package/src/server/tally.js +2 -1
- package/src/server/telegram.js +2 -6
- package/src/server/tiktok.js +3 -3
- package/src/server/twilio.js +3 -2
- package/src/server/twitch.js +5 -3
- package/src/server/types.js +13 -0
- package/src/server/wix.js +20 -878
- package/src/server/wixBookings.js +449 -0
- package/src/server/wixCms.js +132 -0
- package/src/server/wixStores.js +296 -0
- package/src/server/wixValues.js +35 -0
- package/src/server/wiz.js +18 -4
- package/src/server/woocommerce.js +1 -1
- package/src/server/wordpress-com.js +2 -1
- package/src/server/workday.js +9 -9
- package/src/server/x-twitter.js +2 -1
- package/src/server/xero.js +40 -40
- package/src/server/zoho-books.js +6 -6
- package/src/server/zoho-crm.js +5 -5
- package/src/shared/confidence.js +6 -1
- package/src/shared/dbt-semantic-layer.js +1 -1
- package/src/shared/figma.js +1 -1
- package/src/shared/google.js +8 -4
- package/src/shared/googleScope.js +4 -0
- package/src/shared/microsoft-fabric.js +4 -5
- package/src/shared/microsoft.js +8 -3
- package/src/shared/salesforce.js +1 -1
- package/src/shared/tokens.js +7 -3
- package/src/shared/uuid.js +13 -0
- package/src/shared/wix.js +4 -3
- package/src/shared/zoho-books.js +3 -4
- package/src/shared/zoho-crm.js +1 -1
- package/src/shared/{zoho-regions.js → zoho.js} +4 -1
- package/test/ai.test.js +1 -1
- package/test/brevo.test.js +1 -1
- package/test/chargebee.test.js +1 -1
- package/test/google-ads.test.js +8 -4
- package/test/linkedin.test.js +1 -1
- package/test/resend.test.js +2 -2
- package/test/salesforce.test.js +1 -1
- package/test/snowflake.test.js +7 -7
- package/test/tiktok.test.js +1 -1
- package/test/wiz.test.js +1 -1
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { ConnectorError } from "@jskit-ai/connectors-core/server";
|
|
2
|
+
import { createSchema } from "json-rest-schema";
|
|
3
|
+
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
4
|
+
import { serializeNativeJson, requestPath, sameRecord, cursor, querySchema, contactIdField, nativeJsonObjectField } from "./wixValues.js";
|
|
5
|
+
import { isUuid } from "../shared/uuid.js";
|
|
6
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
7
|
+
|
|
8
|
+
// Wix Bookings operations: bookings, services, availability, staff, categories and service forms.
|
|
9
|
+
|
|
10
|
+
function bookingDirectoryOperation(kind) {
|
|
11
|
+
const staff = kind === "staff";
|
|
12
|
+
return {
|
|
13
|
+
scopes: [],
|
|
14
|
+
request(input) {
|
|
15
|
+
const cursorPaging = validateSchemaPayload({ schema: querySchema, mode: "replace" }, input, { statusCode: 422 });
|
|
16
|
+
return { method: "POST", url: staff ? "https://www.wixapis.com/bookings/v1/staff-members/query" : "https://www.wixapis.com/bookings/v2/categories/query",
|
|
17
|
+
body: {
|
|
18
|
+
query: { cursorPaging, sort: [{ fieldName: "id", order: "ASC" }], ...(staff ? { filter: { serviceProvider: true } } : {}) },
|
|
19
|
+
...(staff ? { fields: ["RESOURCE_DETAILS"] } : {})
|
|
20
|
+
} };
|
|
21
|
+
},
|
|
22
|
+
validateResult(value) {
|
|
23
|
+
const values = staff ? value?.staffMembers : value?.categories;
|
|
24
|
+
const paging = value?.pagingMetadata;
|
|
25
|
+
return Array.isArray(values) && values.length <= 100 && values.every(item => isUuid(item?.id) && typeof item.name === "string" &&
|
|
26
|
+
(!staff || isUuid(item.resourceId))) && (paging === undefined || isRecord(paging) &&
|
|
27
|
+
(paging.hasNext === undefined || typeof paging.hasNext === "boolean") &&
|
|
28
|
+
(paging.cursors === undefined || isRecord(paging.cursors) &&
|
|
29
|
+
["next", "prev"].every(key => paging.cursors[key] === undefined || cursor(paging.cursors[key]))));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const servicePagingSchema = createSchema({ limit: { type: "integer", min: 1, max: 100, defaultTo: 20 }, offset: { type: "integer", min: 0, max: 100000, defaultTo: 0 } });
|
|
34
|
+
const serviceIdSchema = createSchema({ serviceId: contactIdField });
|
|
35
|
+
const serviceUpdateSchema = createSchema({
|
|
36
|
+
serviceId: contactIdField,
|
|
37
|
+
revision: { type: "none", required: true, validator: value => typeof value === "string" && /^\d{1,19}$/u.test(value) && BigInt(value) <= 9223372036854775807n || "Use the returned service revision string." },
|
|
38
|
+
name: { type: "string", minLength: 1, maxLength: 400 }, description: { type: "string", maxLength: 7000 }, tagLine: { type: "string", maxLength: 6000 },
|
|
39
|
+
payment: nativeJsonObjectField,
|
|
40
|
+
onlineBooking: { type: "object", schema: createSchema({ enabled: { type: "boolean" }, requireManualApproval: { type: "boolean" }, allowMultipleRequests: { type: "boolean" } }) },
|
|
41
|
+
defaultCapacity: { type: "integer", min: 1, max: 1000 },
|
|
42
|
+
staffMemberIds: { type: "array", validator: values => values.length > 0 && values.length <= 100 || "Select 1–100 staff resource IDs.", items: { type: "string", validator: value => isUuid(value) || "Use a staff resource ID." } },
|
|
43
|
+
schedule: { type: "object", schema: createSchema({ availabilityConstraints: { type: "object", required: true, schema: createSchema({
|
|
44
|
+
sessionDurations: { type: "array", validator: values => values.length > 0 && values.length <= 50 || "Use 1–50 appointment durations.", items: { type: "integer", min: 1 } },
|
|
45
|
+
timeBetweenSessions: { type: "integer", min: 0 }
|
|
46
|
+
}) } }) }
|
|
47
|
+
});
|
|
48
|
+
const validService = value => isUuid(value?.id) && typeof value.name === "string" && typeof value.revision === "string" && /^\d{1,19}$/u.test(value.revision) &&
|
|
49
|
+
["APPOINTMENT", "CLASS", "COURSE"].includes(value.type);
|
|
50
|
+
const localDate = value => typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/u.test(value) &&
|
|
51
|
+
Number.isFinite(Date.parse(`${value}Z`)) && new Date(`${value}Z`).toISOString().slice(0, 19) === value;
|
|
52
|
+
const validSlot = value => isUuid(value?.serviceId) && localDate(value.localStartDate) && localDate(value.localEndDate) &&
|
|
53
|
+
value.localStartDate < value.localEndDate && typeof value.bookable === "boolean";
|
|
54
|
+
const availabilityLocationSchema = createSchema({
|
|
55
|
+
id: { type: "string", validator: value => isUuid(value) || "Use the Wix business location ID." },
|
|
56
|
+
locationType: { type: "string", required: true, enum: ["BUSINESS", "CUSTOM", "CUSTOMER"] },
|
|
57
|
+
name: { type: "string", maxLength: 1000 }, formattedAddress: { type: "string", maxLength: 4000 }
|
|
58
|
+
});
|
|
59
|
+
// Availability must answer for the requested service, local time window and time zone.
|
|
60
|
+
function requestedAvailability(value, requested, events) {
|
|
61
|
+
const slots = value.timeSlots ?? [value.timeSlot];
|
|
62
|
+
sameRecord(!(requested.timeZone && value.timeZone !== requested.timeZone || slots.some(slot => requested.serviceId && slot.serviceId !== requested.serviceId ||
|
|
63
|
+
requested.localStartDate && (slot.localStartDate !== requested.localStartDate || slot.localEndDate !== requested.localEndDate))),
|
|
64
|
+
"Wix returned availability for a different service, time or time zone.", { messageCode: "connectors.wix.returnedAvailabilityDifferentServiceTime" });
|
|
65
|
+
return !events || !requested.serviceIds || sameRecord(slots.every(slot => requested.serviceIds.includes(slot.serviceId)), "Wix returned a class slot for a different service.", { messageCode: "connectors.wix.returnedClassSlotDifferentService" });
|
|
66
|
+
}
|
|
67
|
+
function availabilityOperation(action, events = false) {
|
|
68
|
+
const schema = createSchema({
|
|
69
|
+
...(!events ? {
|
|
70
|
+
customerChoices: { type: "object", schema: createSchema({
|
|
71
|
+
durationInMinutes: { type: "integer", min: 1, max: 57600 },
|
|
72
|
+
addOnIds: { type: "array", validator: values => values.length > 0 && values.length <= 21 && new Set(values).size === values.length || "Select 1–21 distinct add-ons.",
|
|
73
|
+
items: { type: "string", validator: value => isUuid(value) || "Use the add-on ID returned by Wix." } }
|
|
74
|
+
}) },
|
|
75
|
+
includeResourceTypeIds: { type: "array", validator: values => values.length <= 100 || "Choose at most 100 resource types.", items: { type: "string", validator: value => isUuid(value) || "Use a resource type ID." } },
|
|
76
|
+
...(action === "list" ? { locations: { type: "array", validator: values => values.length <= 5 || "Choose at most five locations.", items: { type: "object", schema: availabilityLocationSchema } } } : {
|
|
77
|
+
location: { type: "object", schema: availabilityLocationSchema },
|
|
78
|
+
resourceTypes: { type: "array", validator: values => values.length <= 8 || "Choose at most eight resource types.", items: { type: "object", schema: createSchema({
|
|
79
|
+
resourceTypeId: { type: "string", required: true, validator: value => isUuid(value) || "Use a resource type ID." },
|
|
80
|
+
resourceIds: { type: "array", required: true, validator: values => values.length > 0 && values.length <= 135 || "Choose 1–135 resource IDs.", items: { type: "string", validator: value => isUuid(value) || "Use a resource ID." } }
|
|
81
|
+
}) } }
|
|
82
|
+
})
|
|
83
|
+
} : {}),
|
|
84
|
+
...(events ? { minBookableCapacity: { type: "integer", min: 1, max: 1000 } } : {}),
|
|
85
|
+
serviceId: { type: "string", validator: value => isUuid(value) || "Use the appointment service ID." },
|
|
86
|
+
...(action === "list" ? {
|
|
87
|
+
fromLocalDate: { type: "string", validator: value => localDate(value) || "Use YYYY-MM-DDThh:mm:ss without an offset." },
|
|
88
|
+
toLocalDate: { type: "string", validator: value => localDate(value) || "Use YYYY-MM-DDThh:mm:ss without an offset." },
|
|
89
|
+
limit: { type: "integer", min: 1, max: 1000, defaultTo: 100 },
|
|
90
|
+
cursor: { type: "string", validator: value => cursor(value) || "Use the returned page cursor." }
|
|
91
|
+
} : {
|
|
92
|
+
localStartDate: { type: "string", required: true, validator: value => localDate(value) || "Use the returned local start date." },
|
|
93
|
+
localEndDate: { type: "string", required: true, validator: value => localDate(value) || "Use the returned local end date." }
|
|
94
|
+
}),
|
|
95
|
+
timeZone: { type: "string", maxLength: events ? 100 : 150, validator: value => {
|
|
96
|
+
try { new Intl.DateTimeFormat("en", { timeZone: value }); return true; } catch { return "Use an IANA time zone."; }
|
|
97
|
+
} }
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
scopes: [],
|
|
101
|
+
request(input) {
|
|
102
|
+
const { limit, cursor: next, ...body } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
|
|
103
|
+
if (next && Object.keys(body).length) throw new ConnectorError("connector_input_invalid", "Use only cursor and limit for the next availability page.", { statusCode: 422, messageCode: "connectors.wix.useCursorLimitNextAvailability" });
|
|
104
|
+
if (body.customerChoices && !Object.keys(body.customerChoices).length) throw new ConnectorError("connector_input_invalid", "Select a duration or add-ons before supplying customer choices.", { statusCode: 422, messageCode: "connectors.wix.selectDurationAddOnsSupplying" });
|
|
105
|
+
if (!next && (!body.serviceId || !body.timeZone || (action === "list" ? !body.fromLocalDate || !body.toLocalDate || body.fromLocalDate >= body.toLocalDate : body.localStartDate >= body.localEndDate))) {
|
|
106
|
+
throw new ConnectorError("connector_input_invalid", "Choose the service, time zone and an increasing local date range.", { statusCode: 422, messageCode: "connectors.wix.chooseServiceTimeZoneIncreasing" });
|
|
107
|
+
}
|
|
108
|
+
const locations = body.locations ?? (body.location ? [body.location] : []);
|
|
109
|
+
if (locations.some(location => location.locationType === "BUSINESS" && !location.id)) throw new ConnectorError("connector_input_invalid", "Choose the business location ID.", { statusCode: 422, messageCode: "connectors.wix.chooseBusinessLocationId" });
|
|
110
|
+
const { serviceId, ...eventBody } = body;
|
|
111
|
+
return { method: "POST", url: `https://www.wixapis.com/_api/service-availability/v2/time-slots${events ? "/event" : action === "get" ? "/get" : ""}`,
|
|
112
|
+
body: action === "get" ? body : next ? { cursorPaging: { limit, cursor: next } } : { ...(events ? { ...eventBody, serviceIds: [serviceId], includeNonBookable: false } : { ...body, bookable: true }), cursorPaging: { limit } } };
|
|
113
|
+
},
|
|
114
|
+
validateResult: (value, request) => typeof value?.timeZone === "string" && (action === "get" ? validSlot(value.timeSlot) :
|
|
115
|
+
Array.isArray(value.timeSlots) && value.timeSlots.length <= 1000 && value.timeSlots.every(slot => validSlot(slot) && (!events || typeof slot.eventInfo?.eventId === "string" && slot.eventInfo.eventId.length >= 36 && slot.eventInfo.eventId.length <= 250)) &&
|
|
116
|
+
(value[events ? "pagingMetadata" : "cursorPagingMetadata"] === undefined || value[events ? "pagingMetadata" : "cursorPagingMetadata"] && typeof value[events ? "pagingMetadata" : "cursorPagingMetadata"] === "object" && !Array.isArray(value[events ? "pagingMetadata" : "cursorPagingMetadata"]) &&
|
|
117
|
+
(value[events ? "pagingMetadata" : "cursorPagingMetadata"].cursors === undefined || value[events ? "pagingMetadata" : "cursorPagingMetadata"].cursors && typeof value[events ? "pagingMetadata" : "cursorPagingMetadata"].cursors === "object" && !Array.isArray(value[events ? "pagingMetadata" : "cursorPagingMetadata"].cursors) &&
|
|
118
|
+
["next", "prev"].every(key => value[events ? "pagingMetadata" : "cursorPagingMetadata"].cursors[key] === undefined || cursor(value[events ? "pagingMetadata" : "cursorPagingMetadata"].cursors[key]))))) &&
|
|
119
|
+
requestedAvailability(value, request.body, events)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const eventSlotSchema = createSchema({
|
|
123
|
+
eventId: { type: "string", required: true, minLength: 36, maxLength: 250, validator: value => !/[\p{Cc}]/u.test(value) || "Use the returned event ID." },
|
|
124
|
+
timeZone: { type: "string", required: true, maxLength: 100, validator: value => {
|
|
125
|
+
try { new Intl.DateTimeFormat("en", { timeZone: value }); return true; } catch { return "Use an IANA time zone."; }
|
|
126
|
+
} }
|
|
127
|
+
});
|
|
128
|
+
const serviceLocationSchema = createSchema({
|
|
129
|
+
type: { type: "string", required: true, enum: ["BUSINESS", "CUSTOM", "CUSTOMER"] },
|
|
130
|
+
business: { type: "object", schema: createSchema({ id: contactIdField }) },
|
|
131
|
+
custom: { type: "object", schema: createSchema({ address: { ...nativeJsonObjectField, required: true } }) }
|
|
132
|
+
});
|
|
133
|
+
const setServiceLocationsSchema = createSchema({
|
|
134
|
+
serviceId: contactIdField,
|
|
135
|
+
locations: { type: "array", required: true, validator: values => values.length <= 100 || "Use at most 100 locations.", items: { type: "object", schema: serviceLocationSchema } },
|
|
136
|
+
removedLocationAction: { type: "string", required: true, enum: ["KEEP_AT_CURRENT_LOCATION", "MOVE_TO_LOCATION"] },
|
|
137
|
+
moveToLocation: { type: "object", schema: serviceLocationSchema },
|
|
138
|
+
notifyParticipants: { type: "boolean", defaultTo: false }, notificationMessage: { type: "string", maxLength: 2000 }
|
|
139
|
+
});
|
|
140
|
+
const serviceCreateSchema = createSchema({ service: { ...nativeJsonObjectField, required: true } });
|
|
141
|
+
const formSummarySchema = createSchema({ formId: { type: "string", required: true, validator: value => isUuid(value) || "Use the service form ID." } });
|
|
142
|
+
const bookingCreateSchema = createSchema({
|
|
143
|
+
serviceId: { type: "string", required: true, validator: value => isUuid(value) || "Use the Wix service ID." },
|
|
144
|
+
kind: { type: "string", required: true, enum: ["APPOINTMENT", "CLASS", "COURSE"] },
|
|
145
|
+
timeZone: { type: "string", required: true, maxLength: 150, validator: value => {
|
|
146
|
+
try { new Intl.DateTimeFormat("en", { timeZone: value }); return true; } catch { return "Use an IANA time zone."; }
|
|
147
|
+
} },
|
|
148
|
+
startDate: { type: "string", validator: value => localDate(value) || "Use the selected slot's local start date." },
|
|
149
|
+
endDate: { type: "string", validator: value => localDate(value) || "Use the selected slot's local end date." },
|
|
150
|
+
eventId: { type: "string", minLength: 36, maxLength: 250, validator: value => !/[\p{Cc}]/u.test(value) || "Use the returned event ID." },
|
|
151
|
+
scheduleId: { type: "string", validator: value => isUuid(value) || "Use the course schedule ID." },
|
|
152
|
+
resourceId: { type: "string", validator: value => isUuid(value) || "Use the selected resource ID." },
|
|
153
|
+
location: { type: "object", schema: createSchema({
|
|
154
|
+
id: { type: "string", validator: value => isUuid(value) || "Use the business location ID." },
|
|
155
|
+
locationType: { type: "string", required: true, enum: ["OWNER_BUSINESS", "OWNER_CUSTOM", "CUSTOM"] },
|
|
156
|
+
name: { type: "string", maxLength: 1000 }, formattedAddress: { type: "string", maxLength: 4000 }
|
|
157
|
+
}) },
|
|
158
|
+
totalParticipants: { type: "integer", min: 1, max: 1000 },
|
|
159
|
+
participantsChoices: { type: "object", schema: createSchema({
|
|
160
|
+
serviceChoices: { type: "array", required: true, validator: values => values.length > 0 && values.length <= 20 || "Use 1–20 participant groups.",
|
|
161
|
+
items: { type: "object", schema: createSchema({
|
|
162
|
+
numberOfParticipants: { type: "integer", required: true, min: 1, max: 1000 },
|
|
163
|
+
choices: { type: "array", required: true, validator: values => values.length > 0 && values.length <= 5 || "Use 1–5 service choices per group.",
|
|
164
|
+
items: { type: "object", schema: createSchema({
|
|
165
|
+
optionId: { type: "string", required: true, validator: value => isUuid(value) || "Use the option ID returned by Wix." },
|
|
166
|
+
custom: { type: "string", minLength: 1, maxLength: 1000 },
|
|
167
|
+
duration: { type: "object", schema: createSchema({ minutes: { type: "integer", required: true, min: 1, max: 44639 }, name: { type: "string", maxLength: 255 } }) }
|
|
168
|
+
}) } }
|
|
169
|
+
}) } }
|
|
170
|
+
}) },
|
|
171
|
+
bookedAddOns: { type: "array", validator: values => values.length > 0 && values.length <= 21 && new Set(values.map(value => value?.id)).size === values.length || "Select 1–21 distinct add-ons.",
|
|
172
|
+
items: { type: "object", schema: createSchema({
|
|
173
|
+
id: { type: "string", required: true, validator: value => isUuid(value) || "Use the add-on ID returned by Wix." },
|
|
174
|
+
groupId: { type: "string", validator: value => isUuid(value) || "Use the add-on group ID." },
|
|
175
|
+
quantity: { type: "integer", min: 1, max: 1000 }
|
|
176
|
+
}) } },
|
|
177
|
+
formSubmission: { type: "none", required: true, validator: value => {
|
|
178
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "Provide the booking form's field-value object.";
|
|
179
|
+
try {
|
|
180
|
+
const json = serializeNativeJson(value);
|
|
181
|
+
return Buffer.byteLength(json) <= 100000 || "Keep booking form data within 100KB.";
|
|
182
|
+
} catch { return "Use serializable JSON booking form data."; }
|
|
183
|
+
} },
|
|
184
|
+
notifyParticipants: { type: "boolean", defaultTo: false }, sendSmsReminder: { type: "boolean", defaultTo: false }
|
|
185
|
+
});
|
|
186
|
+
const bookingQuerySchema = createSchema({
|
|
187
|
+
bookingId: { type: "string", validator: value => isUuid(value) || "Use the Wix booking ID." },
|
|
188
|
+
limit: { type: "integer", min: 1, max: 100, defaultTo: 20 },
|
|
189
|
+
offset: { type: "integer", min: 0, max: 100000, defaultTo: 0 }
|
|
190
|
+
});
|
|
191
|
+
const bookingCancelSchema = createSchema({
|
|
192
|
+
bookingId: { type: "string", required: true, validator: value => isUuid(value) || "Use the Wix booking ID." },
|
|
193
|
+
revision: { type: "none", required: true, validator: value => typeof value === "string" && /^\d{1,19}$/u.test(value) && BigInt(value) <= 9223372036854775807n || "Use the current booking revision string." },
|
|
194
|
+
notifyParticipants: { type: "boolean", defaultTo: false },
|
|
195
|
+
message: { type: "string", maxLength: 2000 },
|
|
196
|
+
waiveCharges: { type: "boolean", defaultTo: false }
|
|
197
|
+
});
|
|
198
|
+
const bookingRescheduleSchema = createSchema({
|
|
199
|
+
bookingId: { type: "string", required: true, validator: value => isUuid(value) || "Use the Wix booking ID." },
|
|
200
|
+
revision: { type: "none", required: true, validator: value => typeof value === "string" && /^\d{1,19}$/u.test(value) && BigInt(value) <= 9223372036854775807n || "Use the current booking revision string." },
|
|
201
|
+
slot: { ...nativeJsonObjectField, required: true },
|
|
202
|
+
notifyParticipants: { type: "boolean", defaultTo: false }, message: { type: "string", maxLength: 2000 }
|
|
203
|
+
});
|
|
204
|
+
function bookingConfirmationOperation(action) {
|
|
205
|
+
const automatic = action === "confirmOrDecline";
|
|
206
|
+
const schema = createSchema({
|
|
207
|
+
bookingId: { type: "string", required: true, validator: value => isUuid(value) || "Use the Wix booking ID." },
|
|
208
|
+
...(automatic ? {
|
|
209
|
+
paymentStatus: { type: "string", required: true, enum: ["UNDEFINED", "NOT_PAID", "PAID", "PARTIALLY_PAID", "REFUNDED", "EXEMPT"] }
|
|
210
|
+
} : {
|
|
211
|
+
revision: { type: "none", required: true, validator: value => typeof value === "string" && /^\d{1,19}$/u.test(value) && BigInt(value) <= 9223372036854775807n || "Use the current booking revision string." },
|
|
212
|
+
notifyParticipants: { type: "boolean", defaultTo: false }, message: { type: "string", maxLength: 2000 }
|
|
213
|
+
})
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
scopes: [],
|
|
217
|
+
request(input) {
|
|
218
|
+
const { bookingId, paymentStatus, revision, notifyParticipants, message } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
|
|
219
|
+
if (message !== undefined && !notifyParticipants) throw new ConnectorError("connector_input_invalid", "Enable participant notification before supplying a message.", { statusCode: 422, messageCode: "connectors.wix.enableParticipantNotificationSupplyingMessage" });
|
|
220
|
+
return { method: "POST", url: automatic ? `https://www.wixapis.com/bookings/v2/confirmation/${bookingId}:confirmOrDecline` :
|
|
221
|
+
`https://www.wixapis.com/_api/bookings-service/v2/bookings/${bookingId}/${action}`,
|
|
222
|
+
body: automatic ? { paymentStatus } : { revision, participantNotification: { notifyParticipants, ...(message === undefined ? {} : { message }) } } };
|
|
223
|
+
},
|
|
224
|
+
validateResult: (value, request) => isUuid(value?.booking?.id) &&
|
|
225
|
+
sameRecord(value.booking.id === (automatic ? requestPath(request, -1).split(":")[0] : requestPath(request, -2)), "Wix returned a different booking. Check the operation outcome before retrying.", { messageCode: "connectors.wix.returnedDifferentBookingCheckOperation" }) &&
|
|
226
|
+
typeof value.booking.revision === "string" && /^\d{1,19}$/u.test(value.booking.revision) &&
|
|
227
|
+
BigInt(value.booking.revision) <= 9223372036854775807n && (automatic ? ["CONFIRMED", "PENDING", "DECLINED"].includes(value.booking.status) : value.booking.status === (action === "confirm" ? "CONFIRMED" : "DECLINED"))
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const wixBookingOperations = {
|
|
231
|
+
"bookings.reschedule": {
|
|
232
|
+
scopes: [],
|
|
233
|
+
request(input) {
|
|
234
|
+
const { bookingId, revision, slot, notifyParticipants, message } = validateSchemaPayload({ schema: bookingRescheduleSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
235
|
+
if (message !== undefined && !notifyParticipants) throw new ConnectorError("connector_input_invalid", "Enable participant notification before supplying a message.", { statusCode: 422, messageCode: "connectors.wix.enableParticipantNotificationSupplyingMessage" });
|
|
236
|
+
if (slot.eventId !== undefined) {
|
|
237
|
+
if (Object.keys(slot).length !== 1 || typeof slot.eventId !== "string" || slot.eventId.length < 36 || slot.eventId.length > 250 || /[\p{Cc}]/u.test(slot.eventId)) {
|
|
238
|
+
throw new ConnectorError("connector_input_invalid", "For a class, supply only the selected eventId in slot.", { statusCode: 422, messageCode: "connectors.wix.classSupplySelectedEventidSlot" });
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
let validTimeZone = false;
|
|
242
|
+
try { validTimeZone = typeof slot.timezone === "string" && !!new Intl.DateTimeFormat("en", { timeZone: slot.timezone }); } catch {}
|
|
243
|
+
if (!isUuid(slot.serviceId) || !isUuid(slot.scheduleId) || !localDate(slot.startDate) || !localDate(slot.endDate) || slot.startDate >= slot.endDate || !validTimeZone) {
|
|
244
|
+
throw new ConnectorError("connector_input_invalid", "For an appointment, supply its service/schedule IDs, increasing local slot dates and IANA timezone. Courses cannot be rescheduled.", { statusCode: 422, messageCode: "connectors.wix.appointmentSupplyServiceScheduleIds" });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { method: "POST", url: `https://www.wixapis.com/_api/bookings-service/v2/bookings/${bookingId}/reschedule`,
|
|
248
|
+
body: { revision, slot, participantNotification: { notifyParticipants, ...(message === undefined ? {} : { message }) } } };
|
|
249
|
+
},
|
|
250
|
+
validateResult: (value, request) => isUuid(value?.booking?.id) && sameRecord(value.booking.id === requestPath(request, -2), "Wix returned a different booking. Check the operation outcome before retrying.", { messageCode: "connectors.wix.returnedDifferentBookingCheckOperation" }) && typeof value.booking.revision === "string" && /^\d{1,19}$/u.test(value.booking.revision) &&
|
|
251
|
+
BigInt(value.booking.revision) <= 9223372036854775807n && typeof value.booking.status === "string" && value.booking.status.length > 0
|
|
252
|
+
},
|
|
253
|
+
"bookings.confirm": bookingConfirmationOperation("confirm"),
|
|
254
|
+
"bookings.decline": bookingConfirmationOperation("decline"),
|
|
255
|
+
"bookings.confirmOrDecline": bookingConfirmationOperation("confirmOrDecline"),
|
|
256
|
+
"bookingServices.listAddOnGroups": {
|
|
257
|
+
scopes: [],
|
|
258
|
+
request(input) {
|
|
259
|
+
const value = validateSchemaPayload({ schema: serviceIdSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
260
|
+
return { method: "POST", url: "https://www.wixapis.com/_api/bookings/v2/services/add-on-groups/list-add-on-groups-by-service-id",
|
|
261
|
+
body: value };
|
|
262
|
+
},
|
|
263
|
+
validateResult: value => Array.isArray(value?.addOnGroupsDetails) && value.addOnGroupsDetails.length <= 3 &&
|
|
264
|
+
value.addOnGroupsDetails.every(group => isUuid(group?.groupId) && typeof group.groupName === "string" &&
|
|
265
|
+
Number.isInteger(group.maxNumberOfAddOns) && group.maxNumberOfAddOns >= 0 &&
|
|
266
|
+
Array.isArray(group.addOns) && group.addOns.length <= 7 && group.addOns.every(addOn => isUuid(addOn?.addOnId) && typeof addOn.name === "string"))
|
|
267
|
+
},
|
|
268
|
+
"bookingServices.getVariants": {
|
|
269
|
+
scopes: [],
|
|
270
|
+
request(input) {
|
|
271
|
+
const { serviceId } = validateSchemaPayload({ schema: serviceIdSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
272
|
+
return { method: "GET", url: `https://www.wixapis.com/bookings/v1/serviceOptionsAndVariants/service_id/${serviceId}` };
|
|
273
|
+
},
|
|
274
|
+
validateResult(value, request) {
|
|
275
|
+
const variants = value?.serviceVariants;
|
|
276
|
+
return isUuid(variants?.id) && isUuid(variants.serviceId) && sameRecord(variants.serviceId === requestPath(request, -1), "Wix returned variants for a different service.", { messageCode: "connectors.wix.returnedVariantsDifferentService" }) &&
|
|
277
|
+
Array.isArray(variants.options?.values) && variants.options.values.every(option => isUuid(option?.id) && typeof option.type === "string") &&
|
|
278
|
+
Array.isArray(variants.variants?.values) && variants.variants.values.every(variant =>
|
|
279
|
+
Array.isArray(variant?.choices) && variant.choices.every(choice => isUuid(choice?.optionId)));
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
"bookingCategories.list": bookingDirectoryOperation("categories"),
|
|
283
|
+
"bookingStaff.list": bookingDirectoryOperation("staff"),
|
|
284
|
+
"bookingServices.setLocations": {
|
|
285
|
+
scopes: [],
|
|
286
|
+
request(input) {
|
|
287
|
+
const { serviceId, locations, removedLocationAction: action, moveToLocation, notifyParticipants, notificationMessage } = validateSchemaPayload({ schema: setServiceLocationsSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
288
|
+
if ((action === "MOVE_TO_LOCATION") !== (moveToLocation !== undefined)) throw new ConnectorError("connector_input_invalid", "Provide a destination only when moving future sessions.", { statusCode: 422, messageCode: "connectors.wix.provideDestinationMovingFutureSessions" });
|
|
289
|
+
if (notificationMessage !== undefined && !notifyParticipants) throw new ConnectorError("connector_input_invalid", "Enable participant notification before supplying a message.", { statusCode: 422, messageCode: "connectors.wix.enableParticipantNotificationSupplyingMessage" });
|
|
290
|
+
for (const location of [...locations, ...(moveToLocation ? [moveToLocation] : [])]) {
|
|
291
|
+
if (location.type === "BUSINESS" && (!location.business || location.custom) ||
|
|
292
|
+
location.type === "CUSTOM" && (!location.custom || location.business || !Object.keys(location.custom.address).length) ||
|
|
293
|
+
location.type === "CUSTOMER" && (location.business || location.custom)) {
|
|
294
|
+
throw new ConnectorError("connector_input_invalid", "Supply the details matching each service location type.", { statusCode: 422, messageCode: "connectors.wix.supplyDetailsMatchingServiceLocation" });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { method: "POST", url: `https://www.wixapis.com/_api/bookings/v2/services/${serviceId}/locations`, body: {
|
|
298
|
+
locations, removedLocationSessionsAction: { action, ...(moveToLocation ? { moveToLocationOptions: { newLocation: moveToLocation } } : {}) },
|
|
299
|
+
participantNotification: { notifyParticipants, ...(notificationMessage !== undefined ? { message: notificationMessage } : {}) }
|
|
300
|
+
} };
|
|
301
|
+
},
|
|
302
|
+
validateResult: (value, request) => validService(value?.service) && sameRecord(value.service.id === requestPath(request, -2), "Wix returned a different booking service. Check the outcome before retrying.", { messageCode: "connectors.wix.returnedDifferentBookingServiceCheck" })
|
|
303
|
+
},
|
|
304
|
+
"bookingServices.create": {
|
|
305
|
+
scopes: [],
|
|
306
|
+
request(input) {
|
|
307
|
+
const { service } = validateSchemaPayload({ schema: serviceCreateSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
308
|
+
const invalid = (message, codes) => { throw new ConnectorError("connector_input_invalid", message, { statusCode: 422, ...codes }); };
|
|
309
|
+
if (!["APPOINTMENT", "CLASS", "COURSE"].includes(service.type) || typeof service.name !== "string" || !service.name.trim() || service.name.length > 400) invalid("Provide a service type and name of up to 400 characters.", { messageCode: "connectors.wix.provideServiceTypeNameUp" });
|
|
310
|
+
if (["id", "revision", "createdByAppId", "createdDate", "updatedDate", "addOnGroups"].some(key => Object.hasOwn(service, key))) invalid("Do not send generated service identity fields or existing add-on groups when creating a service.", { messageCode: "connectors.wix.doNotSendGeneratedService" });
|
|
311
|
+
if (!Number.isInteger(service.defaultCapacity) || service.defaultCapacity < 1 || service.defaultCapacity > 1000 || service.type === "APPOINTMENT" && service.defaultCapacity !== 1) invalid("Set appointment capacity to 1, or class/course capacity from 1 to 1000.", { messageCode: "connectors.wix.setAppointmentCapacity1Class" });
|
|
312
|
+
const online = service.onlineBooking;
|
|
313
|
+
if (!online || typeof online.enabled !== "boolean" || ["requireManualApproval", "allowMultipleRequests"].some(key => online[key] !== undefined && typeof online[key] !== "boolean")) invalid("Provide explicit online-booking settings.", { messageCode: "connectors.wix.provideExplicitOnlineBookingSettings" });
|
|
314
|
+
const payment = service.payment;
|
|
315
|
+
if (!payment || !["FIXED", "VARIED", "CUSTOM", "NO_FEE"].includes(payment.rateType)) invalid("Use FIXED, VARIED, CUSTOM or NO_FEE service payment settings.", { messageCode: "connectors.wix.useFixedVariedCustomNo" });
|
|
316
|
+
const options = payment.options;
|
|
317
|
+
if (!options || typeof options !== "object" || ["online", "inPerson", "deposit", "pricingPlan"].some(key => options[key] !== undefined && typeof options[key] !== "boolean") || !(options.online === true || options.inPerson === true)) invalid("Select in-person or online payment, including for a free service.", { messageCode: "connectors.wix.selectPersonOnlinePaymentIncluding" });
|
|
318
|
+
if (online.requireManualApproval && options.pricingPlan || !online.enabled && options.online) invalid("Manual approval cannot use pricing plans; online payment requires online booking.", { messageCode: "connectors.wix.manualApprovalCannotUsePricing" });
|
|
319
|
+
const priced = ["FIXED", "VARIED"].includes(payment.rateType);
|
|
320
|
+
if ((options.online || options.deposit) && !priced) invalid("Online and deposit payments require fixed or varied pricing.", { messageCode: "connectors.wix.onlineDepositPaymentsRequireFixed" });
|
|
321
|
+
const rate = payment.rateType === "FIXED" ? payment.fixed : payment.varied;
|
|
322
|
+
const money = value => value && typeof value.value === "string" && value.value.length <= 30 && /^\d+(?:\.\d{1,2})?$/u.test(value.value) && /[1-9]/u.test(value.value) && /^[A-Z]{3}$/u.test(value.currency);
|
|
323
|
+
if (priced && !money(payment.rateType === "FIXED" ? rate?.price : rate?.defaultPrice)) invalid("Provide a positive decimal price string and three-letter currency code.", { messageCode: "connectors.wix.providePositiveDecimalPriceString" });
|
|
324
|
+
if (payment.rateType === "CUSTOM" && (typeof payment.custom?.description !== "string" || !payment.custom.description.length || payment.custom.description.length > 50)) invalid("Describe the custom payment in up to 50 characters.", { messageCode: "connectors.wix.describeCustomPaymentUp50" });
|
|
325
|
+
if (options.deposit && (!options.online || options.inPerson || !money(rate?.deposit))) invalid("A deposit requires its amount, online payment and no in-person payment.", { messageCode: "connectors.wix.depositRequiresAmountOnlinePayment" });
|
|
326
|
+
const durations = service.schedule?.availabilityConstraints?.sessionDurations;
|
|
327
|
+
if (service.type === "APPOINTMENT") {
|
|
328
|
+
if (!Array.isArray(service.staffMemberIds) || !service.staffMemberIds.length || !service.staffMemberIds.every(isUuid)) invalid("Select at least one staff resource ID for the appointment service.", { messageCode: "connectors.wix.selectLeastStaffResourceId" });
|
|
329
|
+
if (!Array.isArray(durations) || !durations.length || !durations.every(value => Number.isInteger(value) && value > 0)) invalid("Provide appointment session durations in minutes.", { messageCode: "connectors.wix.provideAppointmentSessionDurationsMinutes" });
|
|
330
|
+
} else if (durations !== undefined) invalid("Class/course session durations belong to their sessions, not service availability constraints.", { messageCode: "connectors.wix.classCourseSessionDurationsBelong" });
|
|
331
|
+
return { method: "POST", url: "https://www.wixapis.com/_api/bookings/v2/services", body: { service } };
|
|
332
|
+
},
|
|
333
|
+
validateResult: value => validService(value?.service)
|
|
334
|
+
},
|
|
335
|
+
"classAvailability.list": availabilityOperation("list", true),
|
|
336
|
+
"classAvailability.get": {
|
|
337
|
+
scopes: [],
|
|
338
|
+
request(input) {
|
|
339
|
+
const { eventId, timeZone } = validateSchemaPayload({ schema: eventSlotSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
340
|
+
const url = new URL(`https://www.wixapis.com/_api/service-availability/v2/time-slots/event/${encodeURIComponent(eventId)}`);
|
|
341
|
+
url.searchParams.set("timeZone", timeZone);
|
|
342
|
+
return { method: "GET", url: url.href };
|
|
343
|
+
}, validateResult: (value, request) => validSlot(value?.timeSlot) && typeof value.timeSlot.eventInfo?.eventId === "string" && typeof value.timeZone === "string" &&
|
|
344
|
+
sameRecord(value.timeSlot.eventInfo.eventId === requestPath(request, -1) && value.timeZone === new URL(request.url).searchParams.get("timeZone"), "Wix returned a different event slot or time zone.", { messageCode: "connectors.wix.returnedDifferentEventSlotTime" })
|
|
345
|
+
},
|
|
346
|
+
"forms.summary": {
|
|
347
|
+
scopes: [],
|
|
348
|
+
request(input) {
|
|
349
|
+
const { formId } = validateSchemaPayload({ schema: formSummarySchema, mode: "replace" }, input, { statusCode: 422 });
|
|
350
|
+
return { method: "GET", url: `https://www.wixapis.com/form-schema-service/v4/forms/${formId}/summary` };
|
|
351
|
+
},
|
|
352
|
+
validateResult: (value, request) => isUuid(value?.formSummary?.id) && sameRecord(value.formSummary.id === requestPath(request, -2), "Wix returned a different form summary.", { messageCode: "connectors.wix.returnedDifferentFormSummary" }) &&
|
|
353
|
+
Array.isArray(value.formSummary.fields) && value.formSummary.fields.length <= 500 &&
|
|
354
|
+
value.formSummary.fields.every(field => isUuid(field?.id) && typeof field.target === "string" && field.target.length > 0 && field.target.length <= 200 &&
|
|
355
|
+
typeof field.type === "string" && field.type.length > 0 && (field.label === undefined || typeof field.label === "string" && field.label.length <= 350) &&
|
|
356
|
+
(field.deleted === undefined || typeof field.deleted === "boolean") && (field.options === undefined || Array.isArray(field.options) && field.options.length <= 400))
|
|
357
|
+
},
|
|
358
|
+
"bookings.create": {
|
|
359
|
+
scopes: [],
|
|
360
|
+
request(input) {
|
|
361
|
+
const value = validateSchemaPayload({ schema: bookingCreateSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
362
|
+
const { kind, serviceId, timeZone, startDate, endDate, eventId, scheduleId, resourceId, location, totalParticipants, participantsChoices, bookedAddOns, formSubmission, notifyParticipants, sendSmsReminder } = value;
|
|
363
|
+
if (participantsChoices) {
|
|
364
|
+
if (totalParticipants !== undefined) throw new ConnectorError("connector_input_invalid", "Use participant choices or a fixed participant total, not both.", { statusCode: 422, messageCode: "connectors.wix.useParticipantChoicesFixedParticipant" });
|
|
365
|
+
if (participantsChoices.serviceChoices.reduce((total, group) => total + group.numberOfParticipants, 0) > 1000 ||
|
|
366
|
+
participantsChoices.serviceChoices.some(group => new Set(group.choices.map(choice => choice.optionId)).size !== group.choices.length ||
|
|
367
|
+
group.choices.some(choice => (choice.custom !== undefined) === (choice.duration !== undefined)))) {
|
|
368
|
+
throw new ConnectorError("connector_input_invalid", "Use at most 1000 participants, distinct options per group and exactly one custom or duration value per choice.", { statusCode: 422, messageCode: "connectors.wix.useMost1000ParticipantsDistinct" });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (kind === "APPOINTMENT" ? !startDate || !endDate || startDate >= endDate || !resourceId || !location || eventId || scheduleId :
|
|
372
|
+
kind === "CLASS" ? !eventId || startDate || endDate || resourceId || location || scheduleId : !scheduleId || startDate || endDate || resourceId || location || eventId) {
|
|
373
|
+
throw new ConnectorError("connector_input_invalid", "Use appointment dates/resource/location, a class event ID, or a course schedule ID, matching the selected service type.", { statusCode: 422, messageCode: "connectors.wix.useAppointmentDatesResourceLocation" });
|
|
374
|
+
}
|
|
375
|
+
if (location?.locationType === "OWNER_BUSINESS" && !location.id) throw new ConnectorError("connector_input_invalid", "Choose the business location ID.", { statusCode: 422, messageCode: "connectors.wix.chooseBusinessLocationId" });
|
|
376
|
+
const bookedEntity = kind === "COURSE" ? { schedule: { scheduleId, serviceId, timezone: timeZone } } :
|
|
377
|
+
{ slot: { serviceId, timezone: timeZone, ...(kind === "CLASS" ? { eventId } : { startDate, endDate, resource: { id: resourceId }, location }) } };
|
|
378
|
+
return { method: "POST", url: "https://www.wixapis.com/_api/bookings-service/v2/bookings",
|
|
379
|
+
body: { booking: { bookedEntity, ...(participantsChoices ? { participantsChoices } : { totalParticipants: totalParticipants ?? 1 }),
|
|
380
|
+
...(bookedAddOns ? { bookedAddOns } : {}) }, formSubmission, participantNotification: { notifyParticipants }, sendSmsReminder } };
|
|
381
|
+
},
|
|
382
|
+
validateResult: value => isUuid(value?.booking?.id) && typeof value.booking.revision === "string" && /^\d{1,19}$/u.test(value.booking.revision) &&
|
|
383
|
+
BigInt(value.booking.revision) <= 9223372036854775807n && typeof value.booking.status === "string" && value.booking.status.length > 0
|
|
384
|
+
},
|
|
385
|
+
"availability.list": availabilityOperation("list"),
|
|
386
|
+
"availability.get": availabilityOperation("get"),
|
|
387
|
+
"bookings.list": {
|
|
388
|
+
scopes: [],
|
|
389
|
+
request(input) {
|
|
390
|
+
const { bookingId, ...paging } = validateSchemaPayload({ schema: bookingQuerySchema, mode: "replace" }, input, { statusCode: 422 });
|
|
391
|
+
return { method: "POST", url: "https://www.wixapis.com/_api/bookings-reader/v2/extended-bookings/query",
|
|
392
|
+
body: { query: { paging, ...(bookingId ? { filter: { id: bookingId } } : {}) }, withBookingAllowedActions: true } };
|
|
393
|
+
},
|
|
394
|
+
validateResult: (value, request) => Array.isArray(value?.extendedBookings) && value.extendedBookings.length <= 100 && value.extendedBookings.every(record =>
|
|
395
|
+
isUuid(record?.booking?.id) && typeof record.booking.revision === "string" && /^\d{1,19}$/u.test(record.booking.revision) && BigInt(record.booking.revision) <= 9223372036854775807n &&
|
|
396
|
+
typeof record.booking.status === "string" && record.booking.status.length > 0 &&
|
|
397
|
+
(record.allowedActions === undefined || isRecord(record.allowedActions) &&
|
|
398
|
+
["cancel", "reschedule"].every(key => record.allowedActions[key] === undefined || typeof record.allowedActions[key] === "boolean"))) &&
|
|
399
|
+
(value.pagingMetadata === undefined || isRecord(value.pagingMetadata) &&
|
|
400
|
+
["count", "offset", "total"].every(key => value.pagingMetadata[key] === undefined || Number.isSafeInteger(value.pagingMetadata[key]) && value.pagingMetadata[key] >= 0) &&
|
|
401
|
+
(value.pagingMetadata.count === undefined || value.pagingMetadata.count === value.extendedBookings.length)) &&
|
|
402
|
+
(!request.body.query.filter?.id || sameRecord(value.extendedBookings.length <= 1 && value.extendedBookings.every(record => record.booking.id === request.body.query.filter.id),
|
|
403
|
+
"Wix returned an unexpected booking for the requested ID.", { messageCode: "connectors.wix.returnedUnexpectedBookingRequestedId" }))
|
|
404
|
+
},
|
|
405
|
+
"bookings.cancel": {
|
|
406
|
+
scopes: [],
|
|
407
|
+
request(input) {
|
|
408
|
+
const { bookingId, revision, notifyParticipants, message, waiveCharges } = validateSchemaPayload({ schema: bookingCancelSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
409
|
+
if (message && !notifyParticipants) throw new ConnectorError("connector_input_invalid", "Enable participant notification to send a cancellation message.", { statusCode: 422, messageCode: "connectors.wix.enableParticipantNotificationSendCancellation" });
|
|
410
|
+
return { method: "POST", url: `https://www.wixapis.com/_api/bookings-service/v2/bookings/${bookingId}/cancel`,
|
|
411
|
+
body: { revision, waiveCharges,
|
|
412
|
+
participantNotification: { notifyParticipants, ...(message === undefined ? {} : { message }) } } };
|
|
413
|
+
},
|
|
414
|
+
validateResult: (value, request) => isUuid(value?.booking?.id) && sameRecord(value.booking.id === requestPath(request, -2), "Wix returned a different booking. Check the operation outcome before retrying.", { messageCode: "connectors.wix.returnedDifferentBookingCheckOperation" }) && value.booking.status === "CANCELED" &&
|
|
415
|
+
typeof value.booking.revision === "string" && /^\d{1,19}$/u.test(value.booking.revision) && BigInt(value.booking.revision) <= 9223372036854775807n
|
|
416
|
+
},
|
|
417
|
+
"bookingServices.list": {
|
|
418
|
+
scopes: [],
|
|
419
|
+
request(input) {
|
|
420
|
+
const paging = validateSchemaPayload({ schema: servicePagingSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
421
|
+
return { method: "POST", url: "https://www.wixapis.com/_api/bookings/v2/services/query", body: { query: { paging } } };
|
|
422
|
+
},
|
|
423
|
+
validateResult: value => Array.isArray(value?.services) && value.services.length <= 100 && value.services.every(validService) &&
|
|
424
|
+
(value.pagingMetadata === undefined || isRecord(value.pagingMetadata) &&
|
|
425
|
+
["count", "offset", "total"].every(key => value.pagingMetadata[key] === undefined || Number.isSafeInteger(value.pagingMetadata[key]) && value.pagingMetadata[key] >= 0) &&
|
|
426
|
+
(value.pagingMetadata.count === undefined || value.pagingMetadata.count === value.services.length))
|
|
427
|
+
},
|
|
428
|
+
"bookingServices.get": {
|
|
429
|
+
scopes: [],
|
|
430
|
+
request(input) {
|
|
431
|
+
const { serviceId } = validateSchemaPayload({ schema: serviceIdSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
432
|
+
return { method: "GET", url: `https://www.wixapis.com/_api/bookings/v2/services/${serviceId}` };
|
|
433
|
+
}, validateResult: (value, request) => validService(value?.service) && sameRecord(value.service.id === requestPath(request, -1), "Wix returned a different booking service. Check the outcome before retrying.", { messageCode: "connectors.wix.returnedDifferentBookingServiceCheck" })
|
|
434
|
+
},
|
|
435
|
+
"bookingServices.update": {
|
|
436
|
+
scopes: [],
|
|
437
|
+
request(input) {
|
|
438
|
+
const { serviceId, ...service } = validateSchemaPayload({ schema: serviceUpdateSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
439
|
+
if (Object.keys(service).length < 2) throw new ConnectorError("connector_input_invalid", "Choose a service field to update.", { statusCode: 422, messageCode: "connectors.wix.chooseServiceFieldUpdate" });
|
|
440
|
+
if (["payment", "onlineBooking"].some(key => service[key] !== undefined && !Object.keys(service[key]).length) ||
|
|
441
|
+
service.schedule && !Object.keys(service.schedule.availabilityConstraints).length) {
|
|
442
|
+
throw new ConnectorError("connector_input_invalid", "Provide the selected service settings instead of an empty object.", { statusCode: 422, messageCode: "connectors.wix.provideSelectedServiceSettingsInstead" });
|
|
443
|
+
}
|
|
444
|
+
return { method: "PATCH", url: `https://www.wixapis.com/_api/bookings/v2/services/${serviceId}`, body: { service: { id: serviceId, ...service } } };
|
|
445
|
+
}, validateResult: (value, request) => validService(value?.service) && sameRecord(value.service.id === requestPath(request, -1), "Wix returned a different booking service. Check the outcome before retrying.", { messageCode: "connectors.wix.returnedDifferentBookingServiceCheck" })
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
export { wixBookingOperations };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createSchema } from "json-rest-schema";
|
|
2
|
+
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
3
|
+
import { serializeNativeJson, requestPath, sameRecord } from "./wixValues.js";
|
|
4
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
|
+
|
|
6
|
+
// Wix CMS operations: data collections and their items.
|
|
7
|
+
|
|
8
|
+
const collectionId = value => typeof value === "string" && value.length > 0 && value.length <= 256 &&
|
|
9
|
+
!/[\p{Cc}]/u.test(value) && value.split("/").every(part => part.length > 0 && ![".", ".."].includes(part));
|
|
10
|
+
const collectionListSchema = createSchema({
|
|
11
|
+
limit: { type: "integer", min: 1, max: 100, defaultTo: 20 }, offset: { type: "integer", min: 0, max: 100000, defaultTo: 0 },
|
|
12
|
+
consistentRead: { type: "boolean", defaultTo: false }
|
|
13
|
+
});
|
|
14
|
+
const cmsFilterField = { type: "none", validator: value => {
|
|
15
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "Use a Wix filter object.";
|
|
16
|
+
try {
|
|
17
|
+
const json = serializeNativeJson(value);
|
|
18
|
+
return Buffer.byteLength(json) <= 100000 || "Keep the Wix filter within 100KB.";
|
|
19
|
+
} catch { return "Use serializable JSON filter data."; }
|
|
20
|
+
} };
|
|
21
|
+
const cmsItemQuerySchema = createSchema({
|
|
22
|
+
collectionId: { type: "string", required: true, validator: value => collectionId(value) || "Use the returned Wix collection ID." },
|
|
23
|
+
limit: { type: "integer", min: 1, max: 100, defaultTo: 20 }, offset: { type: "integer", min: 0, max: 100000, defaultTo: 0 },
|
|
24
|
+
consistentRead: { type: "boolean", defaultTo: false },
|
|
25
|
+
filter: cmsFilterField,
|
|
26
|
+
sort: { type: "array", validator: values => values.length <= 100 || "Use at most 100 sort fields.", items: { type: "object", schema: createSchema({
|
|
27
|
+
fieldName: { type: "string", required: true, minLength: 1, maxLength: 1000 }, order: { type: "string", required: true, enum: ["ASC", "DESC"] }
|
|
28
|
+
}) } },
|
|
29
|
+
fields: { type: "array", validator: values => values.length <= 1000 || "Use at most 1000 projected fields.", items: { type: "string", minLength: 1, maxLength: 5000 } },
|
|
30
|
+
returnTotalCount: { type: "boolean" },
|
|
31
|
+
includeReferences: { type: "array", validator: values => values.length <= 100 || "Use at most 100 reference configurations.", items: { type: "object", schema: createSchema({
|
|
32
|
+
field: { type: "string", required: true, minLength: 1, maxLength: 1000 }, limit: { type: "integer", min: 1, max: 1000 }
|
|
33
|
+
}) } }
|
|
34
|
+
});
|
|
35
|
+
const cmsItemIdField = { type: "string", required: true, minLength: 1, maxLength: 128,
|
|
36
|
+
validator: value => !/[\p{Cc}]/u.test(value) && ![".", ".."].includes(value) || "Use a returned Wix item ID." };
|
|
37
|
+
const cmsItemGetSchema = createSchema({
|
|
38
|
+
collectionId: { type: "string", required: true, validator: value => collectionId(value) || "Use the returned Wix collection ID." },
|
|
39
|
+
itemId: cmsItemIdField, consistentRead: { type: "boolean", defaultTo: false }
|
|
40
|
+
});
|
|
41
|
+
const cmsItemPatchSchema = createSchema({
|
|
42
|
+
collectionId: { type: "string", required: true, validator: value => collectionId(value) || "Use the returned Wix collection ID." },
|
|
43
|
+
itemId: cmsItemIdField, conditionFilter: cmsFilterField,
|
|
44
|
+
fieldModifications: { type: "none", required: true, validator: values => {
|
|
45
|
+
if (!Array.isArray(values) || values.length < 1 || values.length > 100) return "Provide 1–100 Wix field modifications.";
|
|
46
|
+
const options = { SET_FIELD: "setFieldOptions", INCREMENT_FIELD: "incrementFieldOptions", APPEND_TO_ARRAY: "appendToArrayOptions", REMOVE_FROM_ARRAY: "removeFromArrayOptions" };
|
|
47
|
+
for (const value of values) {
|
|
48
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.fieldPath !== "string" ||
|
|
49
|
+
!value.fieldPath.length || value.fieldPath.length > 1000 || /[\p{Cc}]/u.test(value.fieldPath)) return "Use a Wix field path for each modification.";
|
|
50
|
+
if (value.action === "REMOVE_FIELD") {
|
|
51
|
+
if (Object.keys(value).some(key => !["fieldPath", "action"].includes(key))) return "REMOVE_FIELD accepts no value options.";
|
|
52
|
+
} else {
|
|
53
|
+
const key = Object.hasOwn(options, value.action) ? options[value.action] : undefined;
|
|
54
|
+
if (!key || Object.keys(value).some(name => !["fieldPath", "action", key].includes(name))) return "Use the options matching the Wix patch action.";
|
|
55
|
+
const config = value[key];
|
|
56
|
+
if (!config || typeof config !== "object" || Array.isArray(config) || !Object.hasOwn(config, "value") || Object.keys(config).length !== 1) return "Provide the patch action's value.";
|
|
57
|
+
if (value.action === "INCREMENT_FIELD" && (typeof config.value !== "number" || !Number.isFinite(config.value))) return "Use a finite increment value.";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const json = serializeNativeJson(values);
|
|
62
|
+
return Buffer.byteLength(json) <= 100000 || "Keep field modifications within 100KB.";
|
|
63
|
+
} catch { return "Use serializable JSON field values."; }
|
|
64
|
+
} }
|
|
65
|
+
});
|
|
66
|
+
const collectionGetSchema = createSchema({
|
|
67
|
+
collectionId: { type: "string", required: true, validator: value => collectionId(value) || "Use the returned Wix collection ID." },
|
|
68
|
+
consistentRead: { type: "boolean", defaultTo: false }
|
|
69
|
+
});
|
|
70
|
+
const validCollection = value => collectionId(value?.id) &&
|
|
71
|
+
(value.fields === undefined || Array.isArray(value.fields)) && (value.displayName === undefined || typeof value.displayName === "string");
|
|
72
|
+
const wixCmsOperations = {
|
|
73
|
+
"cmsItems.get": {
|
|
74
|
+
scopes: [],
|
|
75
|
+
request(input) {
|
|
76
|
+
const { collectionId: dataCollectionId, itemId, consistentRead } = validateSchemaPayload({ schema: cmsItemGetSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
77
|
+
const url = new URL(`https://www.wixapis.com/wix-data/v2/items/${encodeURIComponent(itemId)}`);
|
|
78
|
+
url.searchParams.set("dataCollectionId", dataCollectionId); url.searchParams.set("consistentRead", String(consistentRead));
|
|
79
|
+
return { method: "GET", url: url.href };
|
|
80
|
+
},
|
|
81
|
+
validateResult: (value, request) => isRecord(value?.dataItem?.data) &&
|
|
82
|
+
sameRecord(value.dataItem.id === requestPath(request, -1) && value.dataItem.dataCollectionId === new URL(request.url).searchParams.get("dataCollectionId"), "Wix returned a different CMS item or collection. Check write outcomes before retrying.", { messageCode: "connectors.wix.returnedDifferentCmsItemCollection" })
|
|
83
|
+
},
|
|
84
|
+
"cmsItems.patch": {
|
|
85
|
+
scopes: [],
|
|
86
|
+
request(input) {
|
|
87
|
+
const { collectionId: dataCollectionId, itemId, fieldModifications, conditionFilter } = validateSchemaPayload({ schema: cmsItemPatchSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
88
|
+
return { method: "PATCH", url: `https://www.wixapis.com/wix-data/v2/items/${encodeURIComponent(itemId)}`,
|
|
89
|
+
body: { dataCollectionId, patch: { dataItemId: itemId, fieldModifications }, ...(conditionFilter !== undefined ? { condition: { filter: conditionFilter } } : {}) } };
|
|
90
|
+
},
|
|
91
|
+
validateResult: (value, request) => isRecord(value?.dataItem?.data) &&
|
|
92
|
+
sameRecord(value.dataItem.id === requestPath(request, -1) && value.dataItem.dataCollectionId === request.body.dataCollectionId, "Wix returned a different CMS item or collection. Check write outcomes before retrying.", { messageCode: "connectors.wix.returnedDifferentCmsItemCollection" })
|
|
93
|
+
},
|
|
94
|
+
"cmsItems.list": {
|
|
95
|
+
scopes: [],
|
|
96
|
+
request(input) {
|
|
97
|
+
const { collectionId: dataCollectionId, consistentRead, filter, sort, fields, returnTotalCount, includeReferences, ...paging } = validateSchemaPayload({ schema: cmsItemQuerySchema, mode: "replace" }, input, { statusCode: 422 });
|
|
98
|
+
return { method: "POST", url: "https://www.wixapis.com/wix-data/v2/items/query",
|
|
99
|
+
body: { dataCollectionId, consistentRead,
|
|
100
|
+
...(returnTotalCount !== undefined ? { returnTotalCount } : {}), ...(includeReferences !== undefined ? { includeReferences } : {}),
|
|
101
|
+
query: { paging, ...(filter !== undefined ? { filter } : {}), ...(sort !== undefined ? { sort } : {}), ...(fields !== undefined ? { fields } : {}) } } };
|
|
102
|
+
},
|
|
103
|
+
validateResult: (value, request) => Array.isArray(value?.dataItems) && value.dataItems.length <= 100 && value.dataItems.every(item =>
|
|
104
|
+
typeof item?.id === "string" && item.id.length > 0 && collectionId(item.dataCollectionId) && isRecord(item.data)) &&
|
|
105
|
+
(value.pagingMetadata === undefined || isRecord(value.pagingMetadata)) &&
|
|
106
|
+
sameRecord(value.dataItems.every(item => item.dataCollectionId === request.body.dataCollectionId), "Wix returned items from a different CMS collection.", { messageCode: "connectors.wix.returnedItemsDifferentCmsCollection" })
|
|
107
|
+
},
|
|
108
|
+
"cmsCollections.list": {
|
|
109
|
+
scopes: [],
|
|
110
|
+
request(input) {
|
|
111
|
+
const value = validateSchemaPayload({ schema: collectionListSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
112
|
+
const url = new URL("https://www.wixapis.com/wix-data/v2/collections");
|
|
113
|
+
url.searchParams.set("paging.limit", String(value.limit)); url.searchParams.set("paging.offset", String(value.offset));
|
|
114
|
+
url.searchParams.set("consistentRead", String(value.consistentRead));
|
|
115
|
+
return { method: "GET", url: url.href };
|
|
116
|
+
},
|
|
117
|
+
validateResult: value => Array.isArray(value?.collections) && value.collections.length <= 100 && value.collections.every(validCollection) &&
|
|
118
|
+
(value.pagingMetadata === undefined || isRecord(value.pagingMetadata))
|
|
119
|
+
},
|
|
120
|
+
"cmsCollections.get": {
|
|
121
|
+
scopes: [],
|
|
122
|
+
request(input) {
|
|
123
|
+
const value = validateSchemaPayload({ schema: collectionGetSchema, mode: "replace" }, input, { statusCode: 422 });
|
|
124
|
+
const id = value.collectionId.split("/").map(encodeURIComponent).join("/");
|
|
125
|
+
return { method: "GET", url: `https://www.wixapis.com/wix-data/v2/collections/${id}?consistentRead=${value.consistentRead}` };
|
|
126
|
+
},
|
|
127
|
+
validateResult: (value, request) => validCollection(value?.collection) &&
|
|
128
|
+
sameRecord(value.collection.id === new URL(request.url).pathname.split("/").slice(4).map(decodeURIComponent).join("/"), "Wix returned a different CMS collection.", { messageCode: "connectors.wix.returnedDifferentCmsCollection" })
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export { wixCmsOperations };
|