@thejob/schema 2.1.7 → 2.1.9
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/dist/index.cjs +375 -19
- package/dist/index.d.cts +846 -13
- package/dist/index.d.ts +846 -13
- package/dist/index.js +365 -26
- package/package.json +1 -1
- package/src/marketing/marketing.constant.ts +98 -0
- package/src/marketing/marketing.schema.ts +481 -8
- package/src/user/general-detail.schema.ts +7 -0
package/dist/index.js
CHANGED
|
@@ -1929,7 +1929,7 @@ var PostSchema = object17({
|
|
|
1929
1929
|
}).concat(DbDefaultSchema).noUnknown().label("Post");
|
|
1930
1930
|
|
|
1931
1931
|
// src/marketing/marketing.schema.ts
|
|
1932
|
-
import { array as array10, boolean as boolean11, number as number7, object as object18, string as string15 } from "yup";
|
|
1932
|
+
import { array as array10, boolean as boolean11, lazy as lazy2, mixed as mixed4, number as number7, object as object18, string as string15 } from "yup";
|
|
1933
1933
|
|
|
1934
1934
|
// src/user/user.constant.ts
|
|
1935
1935
|
var UserRole = /* @__PURE__ */ ((UserRole2) => {
|
|
@@ -2048,10 +2048,79 @@ var PRIVATE_PROFILE_FIELDS = [
|
|
|
2048
2048
|
"additionalInfo"
|
|
2049
2049
|
];
|
|
2050
2050
|
|
|
2051
|
+
// src/marketing/marketing.constant.ts
|
|
2052
|
+
var CampaignStatus = /* @__PURE__ */ ((CampaignStatus2) => {
|
|
2053
|
+
CampaignStatus2["Draft"] = "draft";
|
|
2054
|
+
CampaignStatus2["Sending"] = "sending";
|
|
2055
|
+
CampaignStatus2["Sent"] = "sent";
|
|
2056
|
+
CampaignStatus2["Failed"] = "failed";
|
|
2057
|
+
return CampaignStatus2;
|
|
2058
|
+
})(CampaignStatus || {});
|
|
2059
|
+
var SupportedCampaignStatuses = Object.values(CampaignStatus);
|
|
2060
|
+
var CampaignTriggerKind = /* @__PURE__ */ ((CampaignTriggerKind2) => {
|
|
2061
|
+
CampaignTriggerKind2["Manual"] = "manual";
|
|
2062
|
+
CampaignTriggerKind2["Once"] = "once";
|
|
2063
|
+
CampaignTriggerKind2["Schedule"] = "schedule";
|
|
2064
|
+
CampaignTriggerKind2["Lifecycle"] = "lifecycle";
|
|
2065
|
+
return CampaignTriggerKind2;
|
|
2066
|
+
})(CampaignTriggerKind || {});
|
|
2067
|
+
var SupportedCampaignTriggerKinds = Object.values(CampaignTriggerKind);
|
|
2068
|
+
var CampaignIntervalUnit = /* @__PURE__ */ ((CampaignIntervalUnit2) => {
|
|
2069
|
+
CampaignIntervalUnit2["Day"] = "day";
|
|
2070
|
+
CampaignIntervalUnit2["Week"] = "week";
|
|
2071
|
+
CampaignIntervalUnit2["Month"] = "month";
|
|
2072
|
+
return CampaignIntervalUnit2;
|
|
2073
|
+
})(CampaignIntervalUnit || {});
|
|
2074
|
+
var SupportedCampaignIntervalUnits = Object.values(CampaignIntervalUnit);
|
|
2075
|
+
var CampaignEndKind = /* @__PURE__ */ ((CampaignEndKind2) => {
|
|
2076
|
+
CampaignEndKind2["Never"] = "never";
|
|
2077
|
+
CampaignEndKind2["AfterOccurrences"] = "afterOccurrences";
|
|
2078
|
+
CampaignEndKind2["OnDate"] = "onDate";
|
|
2079
|
+
return CampaignEndKind2;
|
|
2080
|
+
})(CampaignEndKind || {});
|
|
2081
|
+
var SupportedCampaignEndKinds = Object.values(CampaignEndKind);
|
|
2082
|
+
var CampaignAnchorField = /* @__PURE__ */ ((CampaignAnchorField2) => {
|
|
2083
|
+
CampaignAnchorField2["CreatedAt"] = "createdAt";
|
|
2084
|
+
CampaignAnchorField2["OnboardingCompletedAt"] = "onboardingCompletedAt";
|
|
2085
|
+
CampaignAnchorField2["MarketingConsentUpdatedAt"] = "marketingConsent.updatedAt";
|
|
2086
|
+
return CampaignAnchorField2;
|
|
2087
|
+
})(CampaignAnchorField || {});
|
|
2088
|
+
var SupportedCampaignAnchorFields = Object.values(CampaignAnchorField);
|
|
2089
|
+
var BindingKind = /* @__PURE__ */ ((BindingKind2) => {
|
|
2090
|
+
BindingKind2["Static"] = "static";
|
|
2091
|
+
BindingKind2["Recipient"] = "recipient";
|
|
2092
|
+
BindingKind2["Url"] = "url";
|
|
2093
|
+
BindingKind2["Source"] = "source";
|
|
2094
|
+
return BindingKind2;
|
|
2095
|
+
})(BindingKind || {});
|
|
2096
|
+
var SupportedBindingKinds = Object.values(BindingKind);
|
|
2097
|
+
var OccurrenceStatus = /* @__PURE__ */ ((OccurrenceStatus2) => {
|
|
2098
|
+
OccurrenceStatus2["Pending"] = "pending";
|
|
2099
|
+
OccurrenceStatus2["Running"] = "running";
|
|
2100
|
+
OccurrenceStatus2["Sent"] = "sent";
|
|
2101
|
+
OccurrenceStatus2["Skipped"] = "skipped";
|
|
2102
|
+
OccurrenceStatus2["Failed"] = "failed";
|
|
2103
|
+
return OccurrenceStatus2;
|
|
2104
|
+
})(OccurrenceStatus || {});
|
|
2105
|
+
var SupportedOccurrenceStatuses = Object.values(OccurrenceStatus);
|
|
2106
|
+
var CAMPAIGN_MAX_RECIPIENTS_CEILING = 5e3;
|
|
2107
|
+
|
|
2051
2108
|
// src/marketing/marketing.schema.ts
|
|
2052
2109
|
var tokens = (label) => array10().of(string15().trim().required()).optional().default([]).label(label);
|
|
2053
2110
|
var enumFacet = (values, label) => array10().of(string15().oneOf(values).required()).optional().default([]).label(label);
|
|
2054
|
-
var
|
|
2111
|
+
var days = (label) => number7().integer().min(0).max(3650).optional().default(void 0).label(label);
|
|
2112
|
+
var months = (label) => number7().integer().min(0).max(1200).optional().default(void 0).label(label);
|
|
2113
|
+
var percent = (label) => number7().integer().min(0).max(100).optional().default(void 0).label(label);
|
|
2114
|
+
var epoch = (label) => number7().integer().min(0).optional().default(void 0).label(label);
|
|
2115
|
+
var salaryBound = (label) => number7().integer().min(0).optional().default(void 0).label(label).when("salaryCurrencies", {
|
|
2116
|
+
is: (v) => !Array.isArray(v) || v.length !== 1,
|
|
2117
|
+
then: (s) => s.test(
|
|
2118
|
+
"requires-single-currency",
|
|
2119
|
+
"Pick exactly one salary currency to filter by salary",
|
|
2120
|
+
(value) => value === void 0 || value === null
|
|
2121
|
+
)
|
|
2122
|
+
});
|
|
2123
|
+
var ListFilterBranchSchema = object18({
|
|
2055
2124
|
// ── Enum facets (closed vocabularies, chip pickers) ─────────────────────────
|
|
2056
2125
|
countries: array10().of(string15().required()).optional().default([]).label("Countries"),
|
|
2057
2126
|
roles: enumFacet(SupportedUserRoles, "Roles"),
|
|
@@ -2106,39 +2175,285 @@ var ListFilterSchema = object18({
|
|
|
2106
2175
|
// Case-insensitive keyword match against headline + aboutMe.
|
|
2107
2176
|
keywords: tokens("Profile Keywords"),
|
|
2108
2177
|
// ── Range / boolean facets ──────────────────────────────────────────────────
|
|
2109
|
-
|
|
2110
|
-
|
|
2178
|
+
// A salary bound is meaningless without a currency: `minSalary` is a raw
|
|
2179
|
+
// integer and 50000 SEK is not 50000 USD, so an un-currencied range matches
|
|
2180
|
+
// across currencies and quietly means nothing. Requiring exactly one currency
|
|
2181
|
+
// alongside a bound is what makes the comparison sound.
|
|
2182
|
+
minSalaryFloor: salaryBound("Min Salary \u2265"),
|
|
2183
|
+
minSalaryCeil: salaryBound("Min Salary \u2264"),
|
|
2111
2184
|
openToWorkOnly: boolean11().optional().default(false).label("Open to Work Only"),
|
|
2112
2185
|
remoteExperienceOnly: boolean11().optional().default(false).label("Has Remote Experience"),
|
|
2113
2186
|
hasResumeOnly: boolean11().optional().default(false).label("Has Resume"),
|
|
2114
2187
|
onboardingCompletedOnly: boolean11().optional().default(false).label("Onboarding Completed"),
|
|
2115
2188
|
emailVerifiedOnly: boolean11().optional().default(false).label("Email Verified Only"),
|
|
2116
|
-
mobileVerifiedOnly: boolean11().optional().default(false).label("Mobile Verified Only")
|
|
2117
|
-
|
|
2189
|
+
mobileVerifiedOnly: boolean11().optional().default(false).label("Mobile Verified Only"),
|
|
2190
|
+
// ── Lifecycle windows ───────────────────────────────────────────────────────
|
|
2191
|
+
// Relative, because a saved list is resolved fresh on every send: a stored
|
|
2192
|
+
// absolute epoch would freeze at authoring time and the audience would decay
|
|
2193
|
+
// to nothing. The absolute forms below exist for one-shot sends; when both are
|
|
2194
|
+
// present the relative form wins.
|
|
2195
|
+
lastSeenBeforeDays: days("Inactive For (days) \u2265"),
|
|
2196
|
+
lastSeenAfterDays: days("Active Within (days)"),
|
|
2197
|
+
createdAtWithinDays: days("Signed Up Within (days)"),
|
|
2198
|
+
createdAtOlderThanDays: days("Signed Up Before (days ago)"),
|
|
2199
|
+
completenessMin: percent("Profile Completeness \u2265"),
|
|
2200
|
+
completenessMax: percent("Profile Completeness \u2264"),
|
|
2201
|
+
// Absolute epoch-ms forms. Prefer the relative fields above for anything that
|
|
2202
|
+
// repeats.
|
|
2203
|
+
lastSeenBefore: epoch("Last Seen Before"),
|
|
2204
|
+
lastSeenAfter: epoch("Last Seen After"),
|
|
2205
|
+
createdAtFrom: epoch("Signed Up After"),
|
|
2206
|
+
createdAtBefore: epoch("Signed Up Before"),
|
|
2207
|
+
// ── Geography ───────────────────────────────────────────────────────────────
|
|
2208
|
+
// `countries` above matches location.countryCode. These narrow within it.
|
|
2209
|
+
// NOTE: stateCodes are stored bare (`KA`, not `IN-KA`) — see the pending
|
|
2210
|
+
// @todo STATE-CODE-ISO-PREFIX migration. Match the stored form, do not prefix.
|
|
2211
|
+
states: tokens("States"),
|
|
2212
|
+
stateCodes: tokens("State Codes"),
|
|
2213
|
+
cities: tokens("Cities"),
|
|
2214
|
+
// Radius search around a point, against location.geo. All three required
|
|
2215
|
+
// together; validated as a group rather than field-by-field so a half-filled
|
|
2216
|
+
// radius cannot silently match everyone.
|
|
2217
|
+
withinRadius: object18({
|
|
2218
|
+
lat: number7().min(-90).max(90).required().label("Latitude"),
|
|
2219
|
+
lng: number7().min(-180).max(180).required().label("Longitude"),
|
|
2220
|
+
km: number7().moreThan(0).max(2e4).required().label("Radius (km)")
|
|
2221
|
+
}).optional().default(void 0).label("Within Radius"),
|
|
2222
|
+
// ── Signup context (immutable market / locale) ──────────────────────────────
|
|
2223
|
+
signupCountries: tokens("Signup Countries"),
|
|
2224
|
+
signupLocales: tokens("Signup Locales"),
|
|
2225
|
+
signupSources: enumFacet(SupportedSignupContextSources, "Signup Source"),
|
|
2226
|
+
// ── Attainment ──────────────────────────────────────────────────────────────
|
|
2227
|
+
// Absent on users who predate the field. Absent means "unknown", never a
|
|
2228
|
+
// match — filtering on this necessarily excludes them.
|
|
2229
|
+
educationLevels: enumFacet(SupportedEducationLevels, "Education Level"),
|
|
2230
|
+
// ── Consent metadata ────────────────────────────────────────────────────────
|
|
2231
|
+
// `marketingConsent.subscribed` is NOT a facet: it is a hard opt-in gate
|
|
2232
|
+
// applied unconditionally by user-service and is not relaxable from here.
|
|
2233
|
+
jobAlertFrequencies: enumFacet(
|
|
2234
|
+
SupportedJobAlertFrequencies,
|
|
2235
|
+
"Job Alert Frequency"
|
|
2236
|
+
),
|
|
2237
|
+
consentSources: tokens("Consent Source"),
|
|
2238
|
+
consentUpdatedWithinDays: days("Consent Updated Within (days)"),
|
|
2239
|
+
// ── Notification preferences ────────────────────────────────────────────────
|
|
2240
|
+
// Read as opt-IN (`=== true`), matching marketingConsent and NOT the `!== false`
|
|
2241
|
+
// reading used for transactional notification routing. A campaign is bulk mail,
|
|
2242
|
+
// so a channel the user never affirmatively enabled does not count as consent.
|
|
2243
|
+
// `productUpdates` defaults to false on the user document, so targeting it
|
|
2244
|
+
// selects only people who deliberately turned it on.
|
|
2245
|
+
notificationChannelsEnabled: enumFacet(
|
|
2246
|
+
SupportedNotificationChannels,
|
|
2247
|
+
"Notification Channels Enabled"
|
|
2248
|
+
),
|
|
2249
|
+
// ── Tenure / recency ────────────────────────────────────────────────────────
|
|
2250
|
+
// Against workExperiences[].duration and educations[].duration ("YYYY-MM").
|
|
2251
|
+
currentlyEmployed: boolean11().optional().default(false).label("Currently Employed"),
|
|
2252
|
+
graduatedWithinMonths: months("Graduated Within (months)"),
|
|
2253
|
+
skillUsedWithinMonths: months("Skill Used Within (months)"),
|
|
2254
|
+
// ── Profile richness ────────────────────────────────────────────────────────
|
|
2255
|
+
hasImage: boolean11().optional().default(false).label("Has Photo"),
|
|
2256
|
+
hasHeadline: boolean11().optional().default(false).label("Has Headline"),
|
|
2257
|
+
hasAboutMe: boolean11().optional().default(false).label("Has About Me"),
|
|
2258
|
+
hasMobile: boolean11().optional().default(false).label("Has Mobile Number"),
|
|
2259
|
+
// Segment by WHICH profile section is missing (profileCompleteness.missing[]),
|
|
2260
|
+
// rather than only by the overall percentage.
|
|
2261
|
+
missingSections: tokens("Missing Profile Sections"),
|
|
2262
|
+
// ── Employer / institute taxonomy ───────────────────────────────────────────
|
|
2263
|
+
companyTypes: enumFacet(SupportedPageTypes, "Company Type"),
|
|
2264
|
+
instituteTypes: enumFacet(SupportedPageTypes, "Institute Type"),
|
|
2265
|
+
// ── Activity ────────────────────────────────────────────────────────────────
|
|
2266
|
+
// Backed by denormalised counters on the user document, because the source
|
|
2267
|
+
// data lives in other services' collections. Tri-state: `false` is a real
|
|
2268
|
+
// filter ("has NO job alert"), which is the audience an alert nudge wants, so
|
|
2269
|
+
// these are `undefined` when unset rather than defaulting to false.
|
|
2270
|
+
hasJobAlert: boolean11().optional().default(void 0).label("Has a Job Alert"),
|
|
2271
|
+
hasApplied: boolean11().optional().default(void 0).label("Has Applied"),
|
|
2272
|
+
appliedWithinDays: days("Applied Within (days)"),
|
|
2273
|
+
// ── Free-text search (name / email) ─────────────────────────────────────────
|
|
2274
|
+
search: string15().trim().optional().label("Search")
|
|
2275
|
+
}).label("Audience Filter Branch");
|
|
2276
|
+
var rejectUnknownKeys = (schema, allowed) => schema.test(
|
|
2277
|
+
"no-unknown-filter-keys",
|
|
2278
|
+
({ value, label }) => {
|
|
2279
|
+
const known = new Set(allowed());
|
|
2280
|
+
const bad = Object.keys(value ?? {}).filter(
|
|
2281
|
+
(k) => !known.has(k)
|
|
2282
|
+
);
|
|
2283
|
+
return `${label} has unsupported keys: ${bad.join(", ")}`;
|
|
2284
|
+
},
|
|
2285
|
+
(value) => {
|
|
2286
|
+
if (value == null) return true;
|
|
2287
|
+
const known = new Set(allowed());
|
|
2288
|
+
return Object.keys(value).every(
|
|
2289
|
+
(k) => known.has(k)
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
);
|
|
2293
|
+
var BRANCH_KEYS = Object.keys(ListFilterBranchSchema.fields);
|
|
2294
|
+
var ListFilterBranch = rejectUnknownKeys(
|
|
2295
|
+
ListFilterBranchSchema,
|
|
2296
|
+
() => BRANCH_KEYS
|
|
2297
|
+
);
|
|
2298
|
+
var ListFilterSchema = rejectUnknownKeys(
|
|
2299
|
+
ListFilterBranchSchema.shape({
|
|
2300
|
+
or: array10().of(ListFilterBranch).optional().default([]).label("Or Any Of")
|
|
2301
|
+
}).label("Audience Filter"),
|
|
2302
|
+
() => [...BRANCH_KEYS, "or"]
|
|
2303
|
+
);
|
|
2118
2304
|
var MailingListSchema = object18({
|
|
2119
2305
|
name: string15().required().min(1).max(120).label("Name"),
|
|
2120
2306
|
description: string15().optional().max(500).label("Description"),
|
|
2121
2307
|
filter: ListFilterSchema.default(() => ListFilterSchema.getDefault())
|
|
2122
2308
|
}).label("Mailing List");
|
|
2309
|
+
var CampaignTriggerSchema = object18({
|
|
2310
|
+
kind: string15().oneOf(SupportedCampaignTriggerKinds).required().default("manual" /* Manual */).label("Trigger"),
|
|
2311
|
+
// IANA zone, e.g. "Asia/Kolkata". Local wall-clock times are interpreted in it,
|
|
2312
|
+
// so occurrences land at the intended hour across DST rather than drifting.
|
|
2313
|
+
timezone: string15().optional().default("UTC").label("Timezone").when("kind", {
|
|
2314
|
+
is: (k) => k !== "manual" /* Manual */,
|
|
2315
|
+
then: (s) => s.required()
|
|
2316
|
+
}),
|
|
2317
|
+
// kind: once
|
|
2318
|
+
runAt: epoch("Send At").when("kind", {
|
|
2319
|
+
is: "once" /* Once */,
|
|
2320
|
+
then: (s) => s.required()
|
|
2321
|
+
}),
|
|
2322
|
+
// kind: schedule
|
|
2323
|
+
startAt: epoch("Starts").when("kind", {
|
|
2324
|
+
is: "schedule" /* Schedule */,
|
|
2325
|
+
then: (s) => s.required()
|
|
2326
|
+
}),
|
|
2327
|
+
interval: object18({
|
|
2328
|
+
every: number7().integer().min(1).max(365).required().label("Every"),
|
|
2329
|
+
unit: string15().oneOf(SupportedCampaignIntervalUnits).required().label("Unit")
|
|
2330
|
+
}).optional().default(void 0).label("Repeats").when("kind", {
|
|
2331
|
+
is: "schedule" /* Schedule */,
|
|
2332
|
+
then: (s) => s.required()
|
|
2333
|
+
}),
|
|
2334
|
+
/** Local wall-clock send time, "HH:mm" in `timezone`. */
|
|
2335
|
+
timeOfDay: string15().matches(/^([01]\d|2[0-3]):[0-5]\d$/, "Use HH:mm").optional().default("09:00").label("Time of Day"),
|
|
2336
|
+
/** For weekly intervals: ISO weekdays, 1 = Monday .. 7 = Sunday. */
|
|
2337
|
+
byWeekday: array10().of(number7().integer().min(1).max(7).required()).optional().default([]).label("On Days"),
|
|
2338
|
+
/**
|
|
2339
|
+
* For monthly intervals: 1-31, or -1 meaning "last day of the month". A month
|
|
2340
|
+
* shorter than the chosen day is clamped to its last day, so 31 in February
|
|
2341
|
+
* fires on the 28th/29th rather than being skipped.
|
|
2342
|
+
*/
|
|
2343
|
+
byMonthday: number7().integer().min(-1).max(31).notOneOf([0]).optional().default(void 0).label("Day of Month"),
|
|
2344
|
+
end: object18({
|
|
2345
|
+
kind: string15().oneOf(SupportedCampaignEndKinds).required().label("Ends"),
|
|
2346
|
+
occurrences: number7().integer().min(1).max(1e4).optional().default(void 0).label("After N Sends").when("kind", {
|
|
2347
|
+
is: "afterOccurrences" /* AfterOccurrences */,
|
|
2348
|
+
then: (s) => s.required()
|
|
2349
|
+
}),
|
|
2350
|
+
date: epoch("End Date").when("kind", {
|
|
2351
|
+
is: "onDate" /* OnDate */,
|
|
2352
|
+
then: (s) => s.required()
|
|
2353
|
+
})
|
|
2354
|
+
}).optional().default(() => ({ kind: "never" /* Never */ })).label("End Condition"),
|
|
2355
|
+
// kind: lifecycle
|
|
2356
|
+
anchorField: string15().oneOf(SupportedCampaignAnchorFields).optional().default(void 0).label("Anchor Date").when("kind", {
|
|
2357
|
+
is: "lifecycle" /* Lifecycle */,
|
|
2358
|
+
then: (s) => s.required()
|
|
2359
|
+
}),
|
|
2360
|
+
/** Days after the anchor at which this user becomes eligible. */
|
|
2361
|
+
offsetDays: number7().integer().min(0).max(3650).optional().default(void 0).label("Days After Anchor").when("kind", {
|
|
2362
|
+
is: "lifecycle" /* Lifecycle */,
|
|
2363
|
+
then: (s) => s.required()
|
|
2364
|
+
}),
|
|
2365
|
+
/**
|
|
2366
|
+
* How long a user stays eligible after crossing the offset. Needed because the
|
|
2367
|
+
* scheduler samples periodically and a zero-width window would miss anyone not
|
|
2368
|
+
* observed on the exact day. A user inside the window on several ticks is still
|
|
2369
|
+
* mailed once: the permanent idempotency key, not this value, guarantees that.
|
|
2370
|
+
*/
|
|
2371
|
+
windowDays: number7().integer().min(1).max(90).optional().default(2).label("Eligibility Window (days)")
|
|
2372
|
+
}).label("Trigger");
|
|
2373
|
+
var BindingSchema = object18({
|
|
2374
|
+
kind: string15().oneOf(SupportedBindingKinds).required().label("Source"),
|
|
2375
|
+
value: string15().required().min(1).max(2e3).label("Value")
|
|
2376
|
+
}).label("Binding");
|
|
2123
2377
|
var CampaignSchema = object18({
|
|
2124
2378
|
name: string15().required().min(1).max(200).label("Campaign Name"),
|
|
2125
|
-
subject
|
|
2126
|
-
// HTML
|
|
2127
|
-
|
|
2379
|
+
// The template renders its own subject; an override is only needed for the
|
|
2380
|
+
// inline-HTML path, hence not required when a template is chosen.
|
|
2381
|
+
subject: string15().optional().max(255).label("Subject").when("templateKey", {
|
|
2382
|
+
is: (v) => !v,
|
|
2383
|
+
then: (s) => s.required().min(1)
|
|
2384
|
+
}),
|
|
2385
|
+
/**
|
|
2386
|
+
* Inline HTML body; may contain {{unsubscribe_url}}. Optional now that a
|
|
2387
|
+
* campaign can instead name a registry template — exactly one of the two is
|
|
2388
|
+
* required. Scheduled and lifecycle campaigns must use a template: the
|
|
2389
|
+
* registry path is the one with a compliance-checked footer and a render test,
|
|
2390
|
+
* and hand-pasted HTML that repeats forever is a mistake that repeats forever.
|
|
2391
|
+
*/
|
|
2392
|
+
html: string15().optional().label("HTML Body").when("templateKey", {
|
|
2393
|
+
is: (v) => !v,
|
|
2394
|
+
then: (s) => s.required().min(1)
|
|
2395
|
+
}),
|
|
2128
2396
|
text: string15().optional().label("Plain-text Body"),
|
|
2129
|
-
listId: string15().required().label("Mailing List")
|
|
2397
|
+
listId: string15().required().label("Mailing List"),
|
|
2398
|
+
/** Key from email-service's template registry. Mutually exclusive with `html`. */
|
|
2399
|
+
templateKey: string15().optional().label("Email Template"),
|
|
2400
|
+
/**
|
|
2401
|
+
* One entry per variable the chosen template declares. Validated at save time
|
|
2402
|
+
* against the template's own `variables`, so a missing required binding is a
|
|
2403
|
+
* rejected save rather than a broken send.
|
|
2404
|
+
*/
|
|
2405
|
+
/**
|
|
2406
|
+
* A DICTIONARY keyed by template variable name, which Yup has no native type
|
|
2407
|
+
* for. It must be built with `lazy` from the keys actually present: a bare
|
|
2408
|
+
* `object()` has no known keys, so under `stripUnknown: true` (which the
|
|
2409
|
+
* consumers' validate helper passes) every binding is "unknown" and the whole
|
|
2410
|
+
* map arrives as `{}` — silently turning a fully-configured campaign into one
|
|
2411
|
+
* whose required variables all appear unbound. Shaping each entry also means a
|
|
2412
|
+
* malformed binding is rejected here rather than at send time.
|
|
2413
|
+
*/
|
|
2414
|
+
bindings: lazy2((value) => {
|
|
2415
|
+
const keys = value && typeof value === "object" ? Object.keys(value) : [];
|
|
2416
|
+
return object18(
|
|
2417
|
+
Object.fromEntries(keys.map((k) => [k, BindingSchema.required()]))
|
|
2418
|
+
).optional().default(() => ({})).label("Template Data");
|
|
2419
|
+
}),
|
|
2420
|
+
/** Registered data source supplying per-recipient content, plus its params. */
|
|
2421
|
+
dataSource: object18({
|
|
2422
|
+
id: string15().required().label("Data Source"),
|
|
2423
|
+
/**
|
|
2424
|
+
* A free-form bag whose keys are defined by the chosen source's OWN
|
|
2425
|
+
* `paramsSchema`, which this package cannot see. Same `lazy` treatment as
|
|
2426
|
+
* `bindings` above, and for the same reason: a bare `object()` has no known
|
|
2427
|
+
* keys, so under `stripUnknown: true` every param is "unknown" and the bag
|
|
2428
|
+
* arrives as `{}` — a campaign configured to mail remote Python roles in
|
|
2429
|
+
* Bangalore silently saves as an unfiltered platform-wide digest.
|
|
2430
|
+
*
|
|
2431
|
+
* Values stay unvalidated here on purpose; the marketing service validates
|
|
2432
|
+
* them against the real source schema in `checkCampaignConfig`, which is the
|
|
2433
|
+
* only place that knows what a given source accepts.
|
|
2434
|
+
*/
|
|
2435
|
+
params: lazy2((value) => {
|
|
2436
|
+
const keys = value && typeof value === "object" ? Object.keys(value) : [];
|
|
2437
|
+
return object18(
|
|
2438
|
+
Object.fromEntries(keys.map((k) => [k, mixed4().optional()]))
|
|
2439
|
+
).optional().default(() => ({})).label("Parameters");
|
|
2440
|
+
})
|
|
2441
|
+
}).optional().default(void 0).label("Data Source"),
|
|
2442
|
+
trigger: CampaignTriggerSchema.default(
|
|
2443
|
+
() => CampaignTriggerSchema.getDefault()
|
|
2444
|
+
),
|
|
2445
|
+
// ── Safety ──────────────────────────────────────────────────────────────────
|
|
2446
|
+
// Both default to the safe position: a new campaign neither runs nor sends
|
|
2447
|
+
// until someone deliberately says so, and says so twice.
|
|
2448
|
+
enabled: boolean11().optional().default(false).label("Enabled"),
|
|
2449
|
+
dryRun: boolean11().optional().default(true).label("Dry Run"),
|
|
2450
|
+
maxRecipients: number7().integer().min(1).max(CAMPAIGN_MAX_RECIPIENTS_CEILING).optional().default(void 0).label("Max Recipients"),
|
|
2451
|
+
utm: object18({
|
|
2452
|
+
campaign: string15().optional().max(120).label("utm_campaign"),
|
|
2453
|
+
content: string15().optional().max(120).label("utm_content")
|
|
2454
|
+
}).optional().default(void 0).label("Link Tagging")
|
|
2130
2455
|
}).label("Campaign");
|
|
2131
2456
|
|
|
2132
|
-
// src/marketing/marketing.constant.ts
|
|
2133
|
-
var CampaignStatus = /* @__PURE__ */ ((CampaignStatus2) => {
|
|
2134
|
-
CampaignStatus2["Draft"] = "draft";
|
|
2135
|
-
CampaignStatus2["Sending"] = "sending";
|
|
2136
|
-
CampaignStatus2["Sent"] = "sent";
|
|
2137
|
-
CampaignStatus2["Failed"] = "failed";
|
|
2138
|
-
return CampaignStatus2;
|
|
2139
|
-
})(CampaignStatus || {});
|
|
2140
|
-
var SupportedCampaignStatuses = Object.values(CampaignStatus);
|
|
2141
|
-
|
|
2142
2457
|
// src/pagination/pagination.schema.ts
|
|
2143
2458
|
import { number as number8, object as object19, string as string16 } from "yup";
|
|
2144
2459
|
var PaginationSchema = object19().shape({
|
|
@@ -2224,7 +2539,7 @@ var SavedSearchSchema = object20({
|
|
|
2224
2539
|
}).noUnknown().strict();
|
|
2225
2540
|
|
|
2226
2541
|
// src/report/report.schema.ts
|
|
2227
|
-
import { mixed as
|
|
2542
|
+
import { mixed as mixed5, object as object21, string as string18 } from "yup";
|
|
2228
2543
|
|
|
2229
2544
|
// src/report/report.constant.ts
|
|
2230
2545
|
var ResourceType = /* @__PURE__ */ ((ResourceType3) => {
|
|
@@ -2249,14 +2564,14 @@ var SupportedReportReasons = Object.values(ReportReason);
|
|
|
2249
2564
|
|
|
2250
2565
|
// src/report/report.schema.ts
|
|
2251
2566
|
var ReportSchema = object21({
|
|
2252
|
-
type:
|
|
2567
|
+
type: mixed5().oneOf(SupportedResourceTypes).required().label("Type"),
|
|
2253
2568
|
resourceId: string18().required().label("Resource ID"),
|
|
2254
|
-
reason:
|
|
2569
|
+
reason: mixed5().oneOf(SupportedReportReasons).required("Please choose a reason").label("Reason"),
|
|
2255
2570
|
comment: string18().max(1e3).optional().label("Comment")
|
|
2256
2571
|
}).label("Report Schema");
|
|
2257
2572
|
|
|
2258
2573
|
// src/feedback/feedback.schema.ts
|
|
2259
|
-
import { mixed as
|
|
2574
|
+
import { mixed as mixed6, object as object22, string as string19 } from "yup";
|
|
2260
2575
|
|
|
2261
2576
|
// src/feedback/feedback.constant.ts
|
|
2262
2577
|
var FeedbackType = /* @__PURE__ */ ((FeedbackType3) => {
|
|
@@ -2280,11 +2595,11 @@ var FEEDBACK_MESSAGE_MAX = 4e3;
|
|
|
2280
2595
|
|
|
2281
2596
|
// src/feedback/feedback.schema.ts
|
|
2282
2597
|
var FeedbackSchema = object22({
|
|
2283
|
-
type:
|
|
2598
|
+
type: mixed6().oneOf(SupportedFeedbackTypes).required().label("Type"),
|
|
2284
2599
|
message: string19().trim().required("Please enter a message").max(FEEDBACK_MESSAGE_MAX).label("Message"),
|
|
2285
2600
|
email: string19().email().optional().label("Email"),
|
|
2286
2601
|
pageUrl: string19().optional().label("Page URL"),
|
|
2287
|
-
status:
|
|
2602
|
+
status: mixed6().oneOf(SupportedFeedbackStatuses).optional().label("Status")
|
|
2288
2603
|
}).label("Feedback Schema");
|
|
2289
2604
|
|
|
2290
2605
|
// src/user/user.schema.ts
|
|
@@ -2386,6 +2701,13 @@ var UserGeneralDetailSchema = object30({
|
|
|
2386
2701
|
}).required().label("Name"),
|
|
2387
2702
|
headline: string28().trim().max(200).optional().label("Headline"),
|
|
2388
2703
|
image: string28().optional().label("Image"),
|
|
2704
|
+
// The DISPLAYED avatar (`image`) is a flattened export with any status frame
|
|
2705
|
+
// ("#OpenToWork", …) already burned into the pixels. `imageOriginal` keeps the
|
|
2706
|
+
// un-framed source the user uploaded, so the image builder can reopen it and
|
|
2707
|
+
// apply a DIFFERENT frame instead of stacking a second one on top of the first.
|
|
2708
|
+
// Optional and absent on every pre-existing user: treat a missing value as
|
|
2709
|
+
// "only the flattened image is available".
|
|
2710
|
+
imageOriginal: string28().optional().label("Original Image"),
|
|
2389
2711
|
aboutMe: string28().trim().max(3e3).optional().label("About Me"),
|
|
2390
2712
|
email: string28().required().label("Email"),
|
|
2391
2713
|
mobile: string28().nullable().optional().label("Mobile"),
|
|
@@ -2712,8 +3034,16 @@ var StudentCompletenessSchema = UserCompletenessSchema.omit([
|
|
|
2712
3034
|
export {
|
|
2713
3035
|
AnswerChoiceType,
|
|
2714
3036
|
ApplicationReceivePreference,
|
|
3037
|
+
BindingKind,
|
|
3038
|
+
BindingSchema,
|
|
3039
|
+
CAMPAIGN_MAX_RECIPIENTS_CEILING,
|
|
3040
|
+
CampaignAnchorField,
|
|
3041
|
+
CampaignEndKind,
|
|
3042
|
+
CampaignIntervalUnit,
|
|
2715
3043
|
CampaignSchema,
|
|
2716
3044
|
CampaignStatus,
|
|
3045
|
+
CampaignTriggerKind,
|
|
3046
|
+
CampaignTriggerSchema,
|
|
2717
3047
|
ChoiceQuestionSchema,
|
|
2718
3048
|
Common,
|
|
2719
3049
|
CompensationType,
|
|
@@ -2755,12 +3085,15 @@ export {
|
|
|
2755
3085
|
JobSearchUrgency,
|
|
2756
3086
|
JobStatus,
|
|
2757
3087
|
JobSubCategory,
|
|
3088
|
+
ListFilterBranch,
|
|
3089
|
+
ListFilterBranchSchema,
|
|
2758
3090
|
ListFilterSchema,
|
|
2759
3091
|
LocationSchema,
|
|
2760
3092
|
MIN_SALARY_LOWER_BOUND,
|
|
2761
3093
|
MIN_SALARY_UPPER_BOUND,
|
|
2762
3094
|
MailingListSchema,
|
|
2763
3095
|
NotificationChannel,
|
|
3096
|
+
OccurrenceStatus,
|
|
2764
3097
|
PREFILL_MIRRORED_KEYS,
|
|
2765
3098
|
PRIVATE_PROFILE_FIELDS,
|
|
2766
3099
|
PageSchema,
|
|
@@ -2790,7 +3123,12 @@ export {
|
|
|
2790
3123
|
StudyType,
|
|
2791
3124
|
SupportedAnswerChoiceTypes,
|
|
2792
3125
|
SupportedApplicationReceivePreferences,
|
|
3126
|
+
SupportedBindingKinds,
|
|
3127
|
+
SupportedCampaignAnchorFields,
|
|
3128
|
+
SupportedCampaignEndKinds,
|
|
3129
|
+
SupportedCampaignIntervalUnits,
|
|
2793
3130
|
SupportedCampaignStatuses,
|
|
3131
|
+
SupportedCampaignTriggerKinds,
|
|
2794
3132
|
SupportedCompensationTypes,
|
|
2795
3133
|
SupportedContactTypes,
|
|
2796
3134
|
SupportedEducationLevels,
|
|
@@ -2806,6 +3144,7 @@ export {
|
|
|
2806
3144
|
SupportedJobStatuses,
|
|
2807
3145
|
SupportedJobSubCategories,
|
|
2808
3146
|
SupportedNotificationChannels,
|
|
3147
|
+
SupportedOccurrenceStatuses,
|
|
2809
3148
|
SupportedPageStatuses,
|
|
2810
3149
|
SupportedPageTypes,
|
|
2811
3150
|
SupportedPostStatuses,
|
package/package.json
CHANGED
|
@@ -6,3 +6,101 @@ export enum CampaignStatus {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export const SupportedCampaignStatuses = Object.values(CampaignStatus);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* How a campaign decides when to send.
|
|
12
|
+
*
|
|
13
|
+
* The two scheduled kinds dedupe differently, and that difference is the reason
|
|
14
|
+
* they are separate kinds rather than one kind with a flag:
|
|
15
|
+
*
|
|
16
|
+
* • `Schedule` fires on a wall clock. Every occurrence is a shared moment, so
|
|
17
|
+
* its identity is the occurrence itself and a recipient may legitimately be
|
|
18
|
+
* mailed again next occurrence.
|
|
19
|
+
* • `Lifecycle` fires relative to each user's own anchor date (signup, …), so
|
|
20
|
+
* there is no shared moment. Identity is the user, permanently: a lifecycle
|
|
21
|
+
* campaign mails a given person at most once, ever.
|
|
22
|
+
*/
|
|
23
|
+
export enum CampaignTriggerKind {
|
|
24
|
+
/** Admin presses send. Also the dry-run test path. */
|
|
25
|
+
Manual = "manual",
|
|
26
|
+
/** One send, at one future instant. */
|
|
27
|
+
Once = "once",
|
|
28
|
+
/** Repeats on a wall-clock schedule until its end condition. */
|
|
29
|
+
Schedule = "schedule",
|
|
30
|
+
/** Fires per user, N days after that user's anchor date. Once per user ever. */
|
|
31
|
+
Lifecycle = "lifecycle",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const SupportedCampaignTriggerKinds = Object.values(CampaignTriggerKind);
|
|
35
|
+
|
|
36
|
+
/** Repeat unit for a `schedule` trigger. */
|
|
37
|
+
export enum CampaignIntervalUnit {
|
|
38
|
+
Day = "day",
|
|
39
|
+
Week = "week",
|
|
40
|
+
Month = "month",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const SupportedCampaignIntervalUnits = Object.values(CampaignIntervalUnit);
|
|
44
|
+
|
|
45
|
+
/** How a repeating campaign stops. `Never` runs until disabled. */
|
|
46
|
+
export enum CampaignEndKind {
|
|
47
|
+
Never = "never",
|
|
48
|
+
AfterOccurrences = "afterOccurrences",
|
|
49
|
+
OnDate = "onDate",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const SupportedCampaignEndKinds = Object.values(CampaignEndKind);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The user-document date a `lifecycle` trigger counts its offset from. A closed
|
|
56
|
+
* list because each value must be a real, indexed, immutable-enough date on the
|
|
57
|
+
* user: an anchor that moves would re-fire the campaign for someone already
|
|
58
|
+
* mailed, which the permanent idempotency key would then silently suppress.
|
|
59
|
+
*/
|
|
60
|
+
export enum CampaignAnchorField {
|
|
61
|
+
CreatedAt = "createdAt",
|
|
62
|
+
OnboardingCompletedAt = "onboardingCompletedAt",
|
|
63
|
+
MarketingConsentUpdatedAt = "marketingConsent.updatedAt",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const SupportedCampaignAnchorFields = Object.values(CampaignAnchorField);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Where a single template variable's value comes from. The admin picks one per
|
|
70
|
+
* variable the chosen template declares.
|
|
71
|
+
*
|
|
72
|
+
* Whether a campaign is *personalised* is derived from these, not stored: it is
|
|
73
|
+
* personalised iff any binding is `Recipient` or `Source`. A stored flag could
|
|
74
|
+
* disagree with the bindings it describes.
|
|
75
|
+
*/
|
|
76
|
+
export enum BindingKind {
|
|
77
|
+
/** A literal typed by the admin. Same for everyone. */
|
|
78
|
+
Static = "static",
|
|
79
|
+
/** A field of the resolved recipient, e.g. `name`. */
|
|
80
|
+
Recipient = "recipient",
|
|
81
|
+
/** A URL, UTM-tagged at send time. */
|
|
82
|
+
Url = "url",
|
|
83
|
+
/** A field of the campaign's data source output, e.g. `matchedJobs`. */
|
|
84
|
+
Source = "source",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const SupportedBindingKinds = Object.values(BindingKind);
|
|
88
|
+
|
|
89
|
+
/** Lifecycle of one materialised occurrence of a scheduled campaign. */
|
|
90
|
+
export enum OccurrenceStatus {
|
|
91
|
+
Pending = "pending",
|
|
92
|
+
Running = "running",
|
|
93
|
+
Sent = "sent",
|
|
94
|
+
Skipped = "skipped",
|
|
95
|
+
Failed = "failed",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const SupportedOccurrenceStatuses = Object.values(OccurrenceStatus);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Hard ceiling on recipients per scheduled/lifecycle send, enforced in code and
|
|
102
|
+
* not overridable by config. Distinct from the one-shot audience cap: a campaign
|
|
103
|
+
* that repeats forever gets the tighter bound, because a misconfigured filter
|
|
104
|
+
* that mails too many people does it again every occurrence.
|
|
105
|
+
*/
|
|
106
|
+
export const CAMPAIGN_MAX_RECIPIENTS_CEILING = 5000;
|