@thejob/schema 2.1.8 → 2.2.0
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/CLAUDE.md +17 -0
- package/dist/index.cjs +2174 -722
- package/dist/index.d.cts +1992 -534
- package/dist/index.d.ts +1992 -534
- package/dist/index.js +2075 -696
- package/package.json +1 -1
- package/src/billing/billing.constant.ts +179 -0
- package/src/billing/billing.schema.ts +280 -0
- package/src/common/facet.schema.ts +342 -0
- package/src/feedback/feedback-filter.schema.ts +39 -0
- package/src/geo/city-filter.schema.ts +41 -0
- package/src/geo/city.schema.ts +104 -0
- package/src/geo/country-filter.schema.ts +47 -0
- package/src/geo/country.schema.ts +148 -0
- package/src/geo/geo-sync.schema.ts +116 -0
- package/src/geo/geo.constant.ts +78 -0
- package/src/geo/state-filter.schema.ts +57 -0
- package/src/geo/state.schema.ts +101 -0
- package/src/group/group-filter.schema.ts +81 -0
- package/src/group/group.constant.ts +15 -0
- package/src/index.ts +35 -0
- package/src/job/job-filter.schema.ts +266 -0
- package/src/language/language-filter.schema.ts +40 -0
- package/src/marketing/marketing.constant.ts +105 -30
- package/src/marketing/marketing.schema.ts +118 -403
- package/src/page/page-filter.schema.ts +67 -0
- package/src/skill/skill-filter.schema.ts +61 -0
- package/src/user/general-detail.schema.ts +7 -0
- package/src/user/push-subscription.schema.ts +86 -0
- package/src/user/user-filter.schema.ts +438 -0
- package/src/user/user-interest.schema.ts +34 -2
- package/src/user/user.constant.ts +20 -8
- package/src/user/user.schema.ts +45 -0
- package/src/user/user.types.ts +35 -0
package/dist/index.js
CHANGED
|
@@ -20,6 +20,102 @@ var UserIdAndCreatedAtSchema = object().shape({
|
|
|
20
20
|
createdAt: string().required().label("Created At")
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
// src/common/facet.schema.ts
|
|
24
|
+
import { array, boolean as boolean2, number as number2, object as object2, string as string2 } from "yup";
|
|
25
|
+
var tokens = (label) => array().of(string2().trim().required()).optional().default([]).label(label);
|
|
26
|
+
var codeTokens = (label) => array().of(string2().trim().uppercase().required()).optional().default([]).label(label);
|
|
27
|
+
var enumFacet = (values, label) => array().of(string2().oneOf(values).required()).optional().default([]).label(label);
|
|
28
|
+
var days = (label) => number2().integer().min(0).max(3650).optional().default(void 0).label(label);
|
|
29
|
+
var months = (label) => number2().integer().min(0).max(1200).optional().default(void 0).label(label);
|
|
30
|
+
var percent = (label) => number2().integer().min(0).max(100).optional().default(void 0).label(label);
|
|
31
|
+
var epoch = (label) => number2().integer().min(0).optional().default(void 0).label(label).meta({ widget: "date" });
|
|
32
|
+
var currencyBound = (label, currencyField) => number2().integer().min(0).optional().default(void 0).label(label).when(currencyField, {
|
|
33
|
+
is: (v) => !Array.isArray(v) || v.length !== 1,
|
|
34
|
+
then: (s) => s.test(
|
|
35
|
+
"requires-single-currency",
|
|
36
|
+
"Pick exactly one salary currency to filter by salary",
|
|
37
|
+
(value) => value === void 0 || value === null
|
|
38
|
+
)
|
|
39
|
+
});
|
|
40
|
+
var qualified = (qualifierValues, qualifierKey, label) => array().of(
|
|
41
|
+
// `rejectUnknownKeys`, not `.noUnknown()`, for the reason spelled out on
|
|
42
|
+
// that helper: outside strict mode `.noUnknown()` silently STRIPS an
|
|
43
|
+
// unknown key rather than failing, so a misspelled qualifier would arrive
|
|
44
|
+
// as a name-only entry and quietly widen the match instead of erroring.
|
|
45
|
+
rejectUnknownKeys(
|
|
46
|
+
object2({
|
|
47
|
+
name: string2().trim().required().label("Name"),
|
|
48
|
+
// `"open"` for a free-text qualifier (a certification's issuing body).
|
|
49
|
+
// An empty array would NOT mean "anything goes" — `.oneOf([])` rejects
|
|
50
|
+
// every value — so the open case is named rather than inferred from an
|
|
51
|
+
// empty vocabulary.
|
|
52
|
+
[qualifierKey]: qualifierValues === "open" ? string2().trim().optional().default(void 0).label(label) : string2().oneOf(qualifierValues).optional().default(void 0).label(label)
|
|
53
|
+
}).label(label).required(),
|
|
54
|
+
() => ["name", qualifierKey]
|
|
55
|
+
)
|
|
56
|
+
).optional().default([]).label(label);
|
|
57
|
+
var conjunctionFieldName = (f) => typeof f === "string" ? f : f.name;
|
|
58
|
+
var conjunction = (fields, label) => {
|
|
59
|
+
const names = fields.map(conjunctionFieldName);
|
|
60
|
+
const isBoolean = (name) => fields.some(
|
|
61
|
+
(f) => typeof f !== "string" && f.name === name && f.type === "boolean"
|
|
62
|
+
);
|
|
63
|
+
return array().of(
|
|
64
|
+
rejectUnknownKeys(
|
|
65
|
+
object2(
|
|
66
|
+
Object.fromEntries(
|
|
67
|
+
fields.map((f) => {
|
|
68
|
+
const name = conjunctionFieldName(f);
|
|
69
|
+
if (typeof f !== "string" && f.type === "boolean") {
|
|
70
|
+
return [name, boolean2().optional().default(void 0).label(name)];
|
|
71
|
+
}
|
|
72
|
+
const base = string2().trim().optional().default(void 0).label(name);
|
|
73
|
+
return [
|
|
74
|
+
name,
|
|
75
|
+
typeof f !== "string" && f.oneOf ? base.oneOf([...f.oneOf, void 0]) : base
|
|
76
|
+
];
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
).test(
|
|
80
|
+
"at-least-one-field",
|
|
81
|
+
// Names the unknown key when that is the real problem. A misspelled
|
|
82
|
+
// field (`companyName`) is stripped before this test sees it, so it
|
|
83
|
+
// would otherwise report "needs at least one field set" — true, but
|
|
84
|
+
// it sends the reader looking in the wrong place.
|
|
85
|
+
({ value }) => {
|
|
86
|
+
const unknown = Object.keys(
|
|
87
|
+
value ?? {}
|
|
88
|
+
).filter((k) => !names.includes(k));
|
|
89
|
+
return unknown.length ? `${label} has unsupported keys: ${unknown.join(", ")}` : `${label} needs at least one of: ${names.join(", ")}`;
|
|
90
|
+
},
|
|
91
|
+
(value) => value == null || names.some((f) => {
|
|
92
|
+
const v = value[f];
|
|
93
|
+
if (isBoolean(f)) return typeof v === "boolean";
|
|
94
|
+
return typeof v === "string" && v.trim() !== "";
|
|
95
|
+
})
|
|
96
|
+
).label(label).required(),
|
|
97
|
+
() => names
|
|
98
|
+
)
|
|
99
|
+
).optional().default([]).label(label);
|
|
100
|
+
};
|
|
101
|
+
var rejectUnknownKeys = (schema, allowed) => schema.test(
|
|
102
|
+
"no-unknown-keys",
|
|
103
|
+
({ value, label }) => {
|
|
104
|
+
const known = new Set(allowed());
|
|
105
|
+
const bad = Object.keys(value ?? {}).filter(
|
|
106
|
+
(k) => !known.has(k)
|
|
107
|
+
);
|
|
108
|
+
return `${label} has unsupported keys: ${bad.join(", ")}`;
|
|
109
|
+
},
|
|
110
|
+
(value) => {
|
|
111
|
+
if (value == null) return true;
|
|
112
|
+
const known = new Set(allowed());
|
|
113
|
+
return Object.keys(value).every(
|
|
114
|
+
(k) => known.has(k)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
23
119
|
// src/common/common.constant.ts
|
|
24
120
|
var Common = /* @__PURE__ */ ((Common2) => {
|
|
25
121
|
Common2["Yes"] = "yes";
|
|
@@ -76,7 +172,7 @@ var ExperienceLevel = /* @__PURE__ */ ((ExperienceLevel2) => {
|
|
|
76
172
|
var SupportedExperienceLevels = Object.values(ExperienceLevel);
|
|
77
173
|
|
|
78
174
|
// src/duration/duration.schema.ts
|
|
79
|
-
import { boolean as
|
|
175
|
+
import { boolean as boolean3, object as object3, ref } from "yup";
|
|
80
176
|
|
|
81
177
|
// src/extensions/date-string.extension.ts
|
|
82
178
|
import dayjs from "dayjs";
|
|
@@ -131,7 +227,7 @@ var DurationSchema = (options) => {
|
|
|
131
227
|
const { startKey = "startDate", endKey = "endDate", format } = options || {};
|
|
132
228
|
const startLabel = options?.startLabel || startKey;
|
|
133
229
|
const endLabel = options?.endLabel || endKey;
|
|
134
|
-
return
|
|
230
|
+
return object3().shape({
|
|
135
231
|
[startKey]: dateString().format(format, `${startLabel} must be in the format ${format}`).required(`${startLabel} is required`).typeError(`${startLabel} must be a valid date`),
|
|
136
232
|
[endKey]: dateString().format(format, `${endLabel} must be in the format ${format}`).when("isActive", {
|
|
137
233
|
is: (val) => Boolean(val) === false,
|
|
@@ -143,12 +239,12 @@ var DurationSchema = (options) => {
|
|
|
143
239
|
// (value) => !value
|
|
144
240
|
// ),ccc
|
|
145
241
|
}),
|
|
146
|
-
isActive:
|
|
242
|
+
isActive: boolean3().optional().default(false).typeError("isActive must be a boolean value")
|
|
147
243
|
});
|
|
148
244
|
};
|
|
149
245
|
|
|
150
246
|
// src/group/group.schema.ts
|
|
151
|
-
import { array, boolean as
|
|
247
|
+
import { array as array2, boolean as boolean4, mixed, number as number3, object as object4, string as string3 } from "yup";
|
|
152
248
|
|
|
153
249
|
// src/group/group.constant.ts
|
|
154
250
|
var GroupVisibility = /* @__PURE__ */ ((GroupVisibility2) => {
|
|
@@ -172,80 +268,129 @@ var GroupStatus = /* @__PURE__ */ ((GroupStatus2) => {
|
|
|
172
268
|
GroupStatus2["Archived"] = "archived";
|
|
173
269
|
return GroupStatus2;
|
|
174
270
|
})(GroupStatus || {});
|
|
271
|
+
var SupportedGroupStatuses = Object.values(GroupStatus);
|
|
272
|
+
var SupportedGroupVisibilities = Object.values(GroupVisibility);
|
|
273
|
+
var SupportedGroupManagedBy = Object.values(GroupManagedBy);
|
|
274
|
+
var SupportedGroupMembershipStatuses = Object.values(
|
|
275
|
+
GroupMembershipStatus
|
|
276
|
+
);
|
|
175
277
|
|
|
176
278
|
// src/group/group.schema.ts
|
|
177
|
-
var GroupTranslationSchema =
|
|
178
|
-
language:
|
|
179
|
-
name:
|
|
180
|
-
description:
|
|
181
|
-
slug:
|
|
182
|
-
seo:
|
|
183
|
-
description:
|
|
184
|
-
keywords:
|
|
279
|
+
var GroupTranslationSchema = object4({
|
|
280
|
+
language: string3().required("Language is required"),
|
|
281
|
+
name: string3().required("Name is required"),
|
|
282
|
+
description: string3().optional(),
|
|
283
|
+
slug: string3().optional(),
|
|
284
|
+
seo: object4({
|
|
285
|
+
description: string3().optional(),
|
|
286
|
+
keywords: array2().of(string3().required()).optional()
|
|
185
287
|
}).optional()
|
|
186
288
|
});
|
|
187
|
-
var GroupSchema =
|
|
188
|
-
name:
|
|
189
|
-
description:
|
|
190
|
-
slug:
|
|
191
|
-
banner:
|
|
192
|
-
logo:
|
|
193
|
-
light:
|
|
194
|
-
dark:
|
|
289
|
+
var GroupSchema = object4().shape({
|
|
290
|
+
name: string3().required().min(3).max(100).label("Name"),
|
|
291
|
+
description: string3().required().min(10).max(1e3).label("Description"),
|
|
292
|
+
slug: string3().required().label("Slug"),
|
|
293
|
+
banner: string3().label("Banner"),
|
|
294
|
+
logo: object4().shape({
|
|
295
|
+
light: string3().required().label("Light Logo"),
|
|
296
|
+
dark: string3().optional().nullable().label("Dark Logo")
|
|
195
297
|
}).optional().nullable().default(null).label("Logo"),
|
|
196
|
-
isFeatured:
|
|
197
|
-
isDiscoverable:
|
|
198
|
-
featuredMarkets:
|
|
199
|
-
followersCount:
|
|
200
|
-
membersCount:
|
|
298
|
+
isFeatured: boolean4().label("Is Featured"),
|
|
299
|
+
isDiscoverable: boolean4().default(true).label("Is Discoverable"),
|
|
300
|
+
featuredMarkets: array2().of(string3()).optional().default([]).label("Featured Markets"),
|
|
301
|
+
followersCount: number3().min(0).label("Followers Count"),
|
|
302
|
+
membersCount: number3().min(0).label("Members Count"),
|
|
201
303
|
visibility: mixed().oneOf(Object.values(GroupVisibility)).default("public" /* Public */).label("Visibility"),
|
|
202
|
-
groupRules:
|
|
203
|
-
countries:
|
|
204
|
-
filterQuery:
|
|
205
|
-
jobs:
|
|
206
|
-
courses:
|
|
207
|
-
discussions:
|
|
304
|
+
groupRules: string3().optional().label("Group Rules"),
|
|
305
|
+
countries: array2().of(string3()).optional().label("Countries"),
|
|
306
|
+
filterQuery: object4().shape({
|
|
307
|
+
jobs: string3().label("Job"),
|
|
308
|
+
courses: string3().label("Course"),
|
|
309
|
+
discussions: string3().label("Discussion")
|
|
208
310
|
}).label("Filter Query"),
|
|
209
|
-
seo:
|
|
210
|
-
description:
|
|
211
|
-
keywords:
|
|
311
|
+
seo: object4().shape({
|
|
312
|
+
description: string3().optional().label("Description"),
|
|
313
|
+
keywords: array2().of(string3()).optional().label("Keywords")
|
|
212
314
|
}).optional().label("SEO"),
|
|
213
|
-
translation:
|
|
315
|
+
translation: array2().of(GroupTranslationSchema).optional().label("Translations"),
|
|
214
316
|
managedBy: mixed().oneOf(Object.values(GroupManagedBy)).default("community" /* Community */).label("Managed By"),
|
|
215
317
|
status: mixed().oneOf(Object.values(GroupStatus)).default("active" /* Active */).label("Status"),
|
|
216
|
-
createdAt:
|
|
217
|
-
updatedAt:
|
|
318
|
+
createdAt: number3().optional().label("Created At"),
|
|
319
|
+
updatedAt: number3().optional().label("Updated At")
|
|
218
320
|
}).label("Group Schema");
|
|
219
|
-
var GroupMembershipSchema =
|
|
220
|
-
groupId:
|
|
221
|
-
userId:
|
|
321
|
+
var GroupMembershipSchema = object4({
|
|
322
|
+
groupId: string3().required().label("Group ID"),
|
|
323
|
+
userId: string3().required().label("User ID"),
|
|
222
324
|
status: mixed().oneOf(Object.values(GroupMembershipStatus)).required().label("Status"),
|
|
223
|
-
createdAt:
|
|
224
|
-
updatedAt:
|
|
325
|
+
createdAt: number3().optional().label("Created At"),
|
|
326
|
+
updatedAt: number3().optional().label("Updated At")
|
|
225
327
|
});
|
|
226
328
|
|
|
329
|
+
// src/group/group-filter.schema.ts
|
|
330
|
+
import { boolean as boolean5, object as object5 } from "yup";
|
|
331
|
+
var GroupFilterShape = object5({
|
|
332
|
+
// ── Classification ──────────────────────────────────────────────────────────
|
|
333
|
+
// Closed vocabularies, matched exactly. Values are LOWERCASE (`active`,
|
|
334
|
+
// `public`, `system`) and that casing is data, not style: it is what is stored.
|
|
335
|
+
//
|
|
336
|
+
// All three are currently NON-SELECTIVE — every group in production is
|
|
337
|
+
// `active` / `public` / `system`. They are declared anyway because they are
|
|
338
|
+
// real vocabulary that gains meaning as the corpus grows, and a filter that
|
|
339
|
+
// silently ignores `status` would be worse than one that narrows to everything.
|
|
340
|
+
status: enumFacet(SupportedGroupStatuses, "Status"),
|
|
341
|
+
visibility: enumFacet(SupportedGroupVisibilities, "Visibility"),
|
|
342
|
+
managedBy: enumFacet(SupportedGroupManagedBy, "Managed By"),
|
|
343
|
+
// ── Flags ───────────────────────────────────────────────────────────────────
|
|
344
|
+
// No `.default(false)`: an unchecked box must mean "no constraint", not "only
|
|
345
|
+
// the non-featured ones", so absent simply omits the clause.
|
|
346
|
+
isFeatured: boolean5().optional().label("Featured Only"),
|
|
347
|
+
isDiscoverable: boolean5().optional().label("Discoverable Only"),
|
|
348
|
+
// ── Markets ─────────────────────────────────────────────────────────────────
|
|
349
|
+
/**
|
|
350
|
+
* `tokens`, NOT `codeTokens`. This is the trap in this file.
|
|
351
|
+
*
|
|
352
|
+
* Every other filter here uses `codeTokens` for country codes, because users
|
|
353
|
+
* and jobs store uppercase ISO alpha-2. Groups store them LOWERCASE (`in`,
|
|
354
|
+
* `se`, `ww` — verified against production). `codeTokens` uppercases its
|
|
355
|
+
* input, so `$in: ["IN"]` against a stored `"in"` matches nothing: no error,
|
|
356
|
+
* empty result, empty mail.
|
|
357
|
+
*
|
|
358
|
+
* If these are ever normalised to uppercase, this becomes `codeTokens` in the
|
|
359
|
+
* same change as the data migration, not before it.
|
|
360
|
+
*/
|
|
361
|
+
countries: tokens("Countries"),
|
|
362
|
+
featuredMarkets: tokens("Featured Markets"),
|
|
363
|
+
/** Group SLUGS, for naming specific groups. A display name breaks on rename. */
|
|
364
|
+
slugs: tokens("Slugs")
|
|
365
|
+
}).label("Group Filter");
|
|
366
|
+
var GROUP_FILTER_KEYS = Object.keys(GroupFilterShape.fields);
|
|
367
|
+
var GroupFilterSchema = rejectUnknownKeys(
|
|
368
|
+
GroupFilterShape,
|
|
369
|
+
() => GROUP_FILTER_KEYS
|
|
370
|
+
);
|
|
371
|
+
|
|
227
372
|
// src/job/job.schema.ts
|
|
228
|
-
import { array as
|
|
373
|
+
import { array as array9, boolean as boolean11, lazy, mixed as mixed3, number as number6, object as object18, string as string14 } from "yup";
|
|
229
374
|
|
|
230
375
|
// src/location/location.schema.ts
|
|
231
|
-
import { array as
|
|
232
|
-
var LocationSchema =
|
|
233
|
-
placeId:
|
|
234
|
-
country:
|
|
235
|
-
countryCode:
|
|
236
|
-
state:
|
|
237
|
-
stateCode:
|
|
238
|
-
city:
|
|
239
|
-
address:
|
|
240
|
-
geo:
|
|
241
|
-
type:
|
|
242
|
-
coordinates:
|
|
376
|
+
import { array as array3, number as number4, object as object6, string as string4 } from "yup";
|
|
377
|
+
var LocationSchema = object6({
|
|
378
|
+
placeId: string4().optional().nullable(),
|
|
379
|
+
country: string4().required(),
|
|
380
|
+
countryCode: string4().required(),
|
|
381
|
+
state: string4().optional().nullable(),
|
|
382
|
+
stateCode: string4().optional().nullable(),
|
|
383
|
+
city: string4().optional().nullable(),
|
|
384
|
+
address: string4().optional().nullable(),
|
|
385
|
+
geo: object6({
|
|
386
|
+
type: string4().oneOf(["Point"]).default("Point"),
|
|
387
|
+
coordinates: array3().of(number4().required()).length(2).required()
|
|
243
388
|
// [longitude, latitude]
|
|
244
389
|
}).required()
|
|
245
390
|
});
|
|
246
391
|
|
|
247
392
|
// src/page/page.schema.ts
|
|
248
|
-
import { array as
|
|
393
|
+
import { array as array6, boolean as boolean9, date, number as number5, object as object12, string as string8 } from "yup";
|
|
249
394
|
|
|
250
395
|
// src/page/page.constant.ts
|
|
251
396
|
var PageType = /* @__PURE__ */ ((PageType2) => {
|
|
@@ -283,44 +428,44 @@ var ContactTypes = /* @__PURE__ */ ((ContactTypes2) => {
|
|
|
283
428
|
var SupportedContactTypes = Object.values(ContactTypes);
|
|
284
429
|
|
|
285
430
|
// src/page/page-location.schema.ts
|
|
286
|
-
import { boolean as
|
|
287
|
-
var PageLocationSchema =
|
|
288
|
-
isHeadquarters:
|
|
289
|
-
coverImage:
|
|
290
|
-
name:
|
|
431
|
+
import { boolean as boolean6, object as object7, string as string5 } from "yup";
|
|
432
|
+
var PageLocationSchema = object7().shape({
|
|
433
|
+
isHeadquarters: boolean6().oneOf([true, false]).default(false).label("Is Headquarters"),
|
|
434
|
+
coverImage: string5().optional().label("Cover Image"),
|
|
435
|
+
name: string5().required().label("Office Name"),
|
|
291
436
|
/* TODO: Add Location Schema to enable geo spacial search*/
|
|
292
437
|
// location: LocationSchema,
|
|
293
|
-
contact:
|
|
294
|
-
email:
|
|
295
|
-
phone:
|
|
296
|
-
address:
|
|
438
|
+
contact: object7().shape({
|
|
439
|
+
email: string5().email().optional().label("Email"),
|
|
440
|
+
phone: string5().optional().label("Phone"),
|
|
441
|
+
address: string5().required().label("Address")
|
|
297
442
|
})
|
|
298
443
|
}).label("PageLocationSchema");
|
|
299
444
|
|
|
300
445
|
// src/page/institute/institute.schema.ts
|
|
301
|
-
import { object as
|
|
302
|
-
var InstituteSchema =
|
|
446
|
+
import { object as object8 } from "yup";
|
|
447
|
+
var InstituteSchema = object8().shape({}).label("InstituteSchema");
|
|
303
448
|
|
|
304
449
|
// src/page/company/company.schema.ts
|
|
305
|
-
import { array as
|
|
450
|
+
import { array as array5, boolean as boolean7, object as object10 } from "yup";
|
|
306
451
|
|
|
307
452
|
// src/page/company/perks-and-benefits.schema.ts
|
|
308
|
-
import { array as
|
|
309
|
-
import { object as
|
|
310
|
-
var PerksAndBenefitSchema =
|
|
311
|
-
name:
|
|
312
|
-
description:
|
|
313
|
-
category:
|
|
453
|
+
import { array as array4 } from "yup";
|
|
454
|
+
import { object as object9, string as string6 } from "yup";
|
|
455
|
+
var PerksAndBenefitSchema = object9().shape({
|
|
456
|
+
name: string6().trim().max(50).required().label("Name"),
|
|
457
|
+
description: string6().max(1e3).required().label("Description"),
|
|
458
|
+
category: array4().of(string6()).optional().label("Category")
|
|
314
459
|
});
|
|
315
460
|
|
|
316
461
|
// src/page/company/company.schema.ts
|
|
317
|
-
var CompanySchema =
|
|
318
|
-
equalOpportunityEmployer:
|
|
319
|
-
perksAndBenefits:
|
|
462
|
+
var CompanySchema = object10().shape({
|
|
463
|
+
equalOpportunityEmployer: boolean7().optional().label("Equal Opportunity Employer"),
|
|
464
|
+
perksAndBenefits: array5().of(PerksAndBenefitSchema).optional().label("Perks and Benefits")
|
|
320
465
|
}).noUnknown().strict().label("Company Schema");
|
|
321
466
|
|
|
322
467
|
// src/social-account/social-account.schema.ts
|
|
323
|
-
import { boolean as
|
|
468
|
+
import { boolean as boolean8, object as object11, string as string7 } from "yup";
|
|
324
469
|
|
|
325
470
|
// src/social-account/social-account.constant.ts
|
|
326
471
|
var SocialAccount = /* @__PURE__ */ ((SocialAccount2) => {
|
|
@@ -358,43 +503,43 @@ var SocialAccount = /* @__PURE__ */ ((SocialAccount2) => {
|
|
|
358
503
|
var SupportedSocialAccounts = Object.values(SocialAccount);
|
|
359
504
|
|
|
360
505
|
// src/social-account/social-account.schema.ts
|
|
361
|
-
var SocialAccountSchema =
|
|
506
|
+
var SocialAccountSchema = object11().shape({
|
|
362
507
|
// TODO: Remove isNew - it's unused. Backend (user-social-accounts.service.ts) just strips it before saving.
|
|
363
|
-
isNew:
|
|
364
|
-
type:
|
|
365
|
-
url:
|
|
508
|
+
isNew: boolean8().default(false).optional().label("Is New"),
|
|
509
|
+
type: string7().oneOf(SupportedSocialAccounts).required().label("Account Type"),
|
|
510
|
+
url: string7().required().label("URL")
|
|
366
511
|
}).noUnknown().strict().label("Social Account");
|
|
367
512
|
|
|
368
513
|
// src/page/page.schema.ts
|
|
369
|
-
var PageSchema =
|
|
370
|
-
id:
|
|
371
|
-
shortId:
|
|
372
|
-
name:
|
|
373
|
-
headline:
|
|
374
|
-
about:
|
|
375
|
-
website:
|
|
376
|
-
slug:
|
|
377
|
-
employeeCount:
|
|
378
|
-
yearFounded:
|
|
379
|
-
type:
|
|
380
|
-
locations:
|
|
514
|
+
var PageSchema = object12().shape({
|
|
515
|
+
id: string8().required().label("ID"),
|
|
516
|
+
shortId: string8().required().label("Short ID"),
|
|
517
|
+
name: string8().required().label("Name"),
|
|
518
|
+
headline: string8().max(100).optional().label("Headline"),
|
|
519
|
+
about: string8().required().min(100).max(1e4).label("About"),
|
|
520
|
+
website: string8().required().label("Website"),
|
|
521
|
+
slug: string8().required().label("Slug"),
|
|
522
|
+
employeeCount: string8().optional().label("Employee Count"),
|
|
523
|
+
yearFounded: string8().optional().label("Year Founded"),
|
|
524
|
+
type: string8().oneOf(SupportedPageTypes).required().label("Page Type"),
|
|
525
|
+
locations: array6().of(PageLocationSchema).optional().label("Locations"),
|
|
381
526
|
/*
|
|
382
527
|
* @deprecated
|
|
383
528
|
* 'locations' prop already has contact information, so this can be removed
|
|
384
529
|
*/
|
|
385
|
-
contacts:
|
|
386
|
-
|
|
387
|
-
type:
|
|
388
|
-
value:
|
|
389
|
-
label:
|
|
390
|
-
isPrimary:
|
|
391
|
-
isVerified:
|
|
530
|
+
contacts: array6().of(
|
|
531
|
+
object12().shape({
|
|
532
|
+
type: string8().oneOf(SupportedContactTypes).required().label("Type"),
|
|
533
|
+
value: string8().required().label("Value"),
|
|
534
|
+
label: string8().optional().label("Label"),
|
|
535
|
+
isPrimary: boolean9().optional().default(false).label("Is Primary"),
|
|
536
|
+
isVerified: boolean9().optional().default(false).label("Is Verified")
|
|
392
537
|
})
|
|
393
538
|
).optional().label("Contacts"),
|
|
394
539
|
verified: date().optional().label("Verified"),
|
|
395
|
-
logo:
|
|
396
|
-
dark:
|
|
397
|
-
light:
|
|
540
|
+
logo: object12().shape({
|
|
541
|
+
dark: string8().optional().nullable().label("Dark Logo"),
|
|
542
|
+
light: string8().required().label("Light Logo")
|
|
398
543
|
}).optional().nullable().default(null).label("Logo"),
|
|
399
544
|
// Taxonomy (see job-taxonomy.constant.ts): category -> industry -> subCategory,
|
|
400
545
|
// stored as slugs and translated for display. As on JobSchema, these are NOT
|
|
@@ -402,21 +547,21 @@ var PageSchema = object10().shape({
|
|
|
402
547
|
// never a reason to reject the whole page. `categories` is the top level (kept
|
|
403
548
|
// required for backward compatibility); `industries` and `subCategories` are the
|
|
404
549
|
// deeper levels the tree picker fills in when a leaf auto-selects its parents.
|
|
405
|
-
categories:
|
|
406
|
-
industries:
|
|
407
|
-
subCategories:
|
|
408
|
-
socialAccounts:
|
|
409
|
-
isOwner:
|
|
410
|
-
isFeatured:
|
|
411
|
-
featuredMarkets:
|
|
412
|
-
status:
|
|
413
|
-
followersCount:
|
|
550
|
+
categories: array6().of(string8().trim().required()).required().min(1).label("Categories"),
|
|
551
|
+
industries: array6().of(string8().trim().required()).optional().default([]).label("Industries"),
|
|
552
|
+
subCategories: array6().of(string8().trim().required()).optional().default([]).label("Sub Categories"),
|
|
553
|
+
socialAccounts: array6().of(SocialAccountSchema).optional().label("Social Accounts"),
|
|
554
|
+
isOwner: boolean9().optional().label("Is Owner"),
|
|
555
|
+
isFeatured: boolean9().optional().default(false).label("Is Featured"),
|
|
556
|
+
featuredMarkets: array6().of(string8().required()).optional().default([]).label("Featured Markets"),
|
|
557
|
+
status: string8().oneOf(SupportedPageStatuses).required().label("Status"),
|
|
558
|
+
followersCount: number5().min(0).optional().label("Followers Count")
|
|
414
559
|
}).concat(CompanySchema).concat(InstituteSchema).concat(DbDefaultSchema).noUnknown(true).strict(true).required().label("Page Schema");
|
|
415
560
|
|
|
416
561
|
// src/skill/skill.schema.ts
|
|
417
|
-
import { array as
|
|
418
|
-
var SkillSchema =
|
|
419
|
-
name:
|
|
562
|
+
import { array as array7, object as object13, string as string9 } from "yup";
|
|
563
|
+
var SkillSchema = object13({
|
|
564
|
+
name: string9().trim().required().label("Skill Name"),
|
|
420
565
|
/**
|
|
421
566
|
* The taxonomy concept this skill resolved to, assigned by thejob-taxonomy-service
|
|
422
567
|
* via `POST /normalize/batch`. Stable slug, e.g. "power-bi".
|
|
@@ -434,14 +579,14 @@ var SkillSchema = object11({
|
|
|
434
579
|
*
|
|
435
580
|
* Consumers must therefore read `conceptName ?? name`, never `conceptName` alone.
|
|
436
581
|
*/
|
|
437
|
-
conceptId:
|
|
582
|
+
conceptId: string9().optional().nullable().label("Concept ID"),
|
|
438
583
|
/** Canonical display name for `conceptId`, e.g. "Power BI". See `conceptId`. */
|
|
439
|
-
conceptName:
|
|
440
|
-
logo:
|
|
441
|
-
light:
|
|
442
|
-
dark:
|
|
584
|
+
conceptName: string9().optional().nullable().label("Concept Name"),
|
|
585
|
+
logo: object13({
|
|
586
|
+
light: string9().required().label("Light Logo"),
|
|
587
|
+
dark: string9().optional().nullable().label("Dark Logo")
|
|
443
588
|
}).optional().nullable().default(null).label("Logo"),
|
|
444
|
-
tags:
|
|
589
|
+
tags: array7().of(string9().max(50)).max(20).optional().label("Tags")
|
|
445
590
|
}).concat(DbDefaultSchema).noUnknown().strict().label("Skill");
|
|
446
591
|
|
|
447
592
|
// src/question/question.constant.ts
|
|
@@ -460,24 +605,24 @@ var AnswerChoiceType = /* @__PURE__ */ ((AnswerChoiceType2) => {
|
|
|
460
605
|
var SupportedAnswerChoiceTypes = Object.values(AnswerChoiceType);
|
|
461
606
|
|
|
462
607
|
// src/question/question.schema.ts
|
|
463
|
-
import { boolean as
|
|
464
|
-
var QuestionSchema =
|
|
465
|
-
label:
|
|
466
|
-
default:
|
|
467
|
-
title:
|
|
468
|
-
type:
|
|
469
|
-
isOptional:
|
|
470
|
-
readonly:
|
|
471
|
-
isNew:
|
|
608
|
+
import { boolean as boolean10, object as object14, string as string10 } from "yup";
|
|
609
|
+
var QuestionSchema = object14().shape({
|
|
610
|
+
label: string10().max(50).optional().label("Label"),
|
|
611
|
+
default: boolean10().optional().default(false).label("Default"),
|
|
612
|
+
title: string10().required().max(200).label("Title"),
|
|
613
|
+
type: string10().oneOf(SupportedQuestionTypes).required().label("Type"),
|
|
614
|
+
isOptional: boolean10().optional().label("Mark if the question is optional"),
|
|
615
|
+
readonly: boolean10().optional().label("Readonly"),
|
|
616
|
+
isNew: boolean10().optional().label("Is New")
|
|
472
617
|
});
|
|
473
618
|
|
|
474
619
|
// src/question/input-question.schema.ts
|
|
475
|
-
import { object as
|
|
620
|
+
import { object as object15, string as string11 } from "yup";
|
|
476
621
|
var InputQuestionSchema = QuestionSchema.concat(
|
|
477
|
-
|
|
478
|
-
type:
|
|
479
|
-
preferredAnswer:
|
|
480
|
-
answers:
|
|
622
|
+
object15().shape({
|
|
623
|
+
type: string11().oneOf(["input" /* Input */]).required().label("Question type"),
|
|
624
|
+
preferredAnswer: string11().max(100).optional().label("Preferred Answer"),
|
|
625
|
+
answers: string11().max(500).when("isOptional", {
|
|
481
626
|
is: (isOptional) => Boolean(isOptional),
|
|
482
627
|
then: (schema) => schema.optional(),
|
|
483
628
|
otherwise: (schema) => schema.required()
|
|
@@ -486,10 +631,10 @@ var InputQuestionSchema = QuestionSchema.concat(
|
|
|
486
631
|
);
|
|
487
632
|
|
|
488
633
|
// src/question/choice-question.schema.ts
|
|
489
|
-
import { array as
|
|
634
|
+
import { array as array8, mixed as mixed2, object as object16, string as string12 } from "yup";
|
|
490
635
|
var ChoicePreferredAnswerSchema = (label = "Preferred answer") => mixed2().when("answerChoiceType", {
|
|
491
636
|
is: (value) => value === "multiple" /* Multiple */,
|
|
492
|
-
then: () =>
|
|
637
|
+
then: () => array8(string12()).test(
|
|
493
638
|
"is-valid-option",
|
|
494
639
|
`${label} must be from the provided options`,
|
|
495
640
|
function(answer) {
|
|
@@ -500,7 +645,7 @@ var ChoicePreferredAnswerSchema = (label = "Preferred answer") => mixed2().when(
|
|
|
500
645
|
return true;
|
|
501
646
|
}
|
|
502
647
|
).optional().label(label),
|
|
503
|
-
otherwise: () =>
|
|
648
|
+
otherwise: () => string12().test(
|
|
504
649
|
"is-valid-option",
|
|
505
650
|
`${label} must be from the provided options`,
|
|
506
651
|
function(answer) {
|
|
@@ -511,18 +656,18 @@ var ChoicePreferredAnswerSchema = (label = "Preferred answer") => mixed2().when(
|
|
|
511
656
|
});
|
|
512
657
|
var getChoiceAnswerSchema = (answerChoiceType, isOptional) => {
|
|
513
658
|
if (answerChoiceType === "multiple" /* Multiple */) {
|
|
514
|
-
const multiAnswerSchema =
|
|
659
|
+
const multiAnswerSchema = array8(string12().max(500));
|
|
515
660
|
return isOptional ? multiAnswerSchema.optional().default([]) : multiAnswerSchema.min(1, "Select at least one option.").required("Select at least one option.").default([]);
|
|
516
661
|
}
|
|
517
|
-
const singleAnswerSchema =
|
|
662
|
+
const singleAnswerSchema = string12().max(500);
|
|
518
663
|
return isOptional ? singleAnswerSchema.optional() : singleAnswerSchema.required("Select an option.");
|
|
519
664
|
};
|
|
520
665
|
var ChoiceQuestionSchema = QuestionSchema.concat(
|
|
521
|
-
|
|
522
|
-
type:
|
|
523
|
-
optionsFrom:
|
|
524
|
-
options:
|
|
525
|
-
answerChoiceType:
|
|
666
|
+
object16().shape({
|
|
667
|
+
type: string12().oneOf(["choice" /* Choice */]).required().label("Question type"),
|
|
668
|
+
optionsFrom: string12().oneOf(["skill" /* Skill */, "education_level" /* EducationLevel */, "experience_level" /* ExperienceLevel */]).optional().label("Option field"),
|
|
669
|
+
options: array8(string12().required()).min(1).required().label("Options"),
|
|
670
|
+
answerChoiceType: string12().oneOf(SupportedAnswerChoiceTypes).required().default("single" /* Single */).label("Type"),
|
|
526
671
|
preferredAnswer: ChoicePreferredAnswerSchema("Preferred answer"),
|
|
527
672
|
answers: mixed2().when(
|
|
528
673
|
["answerChoiceType", "isOptional"],
|
|
@@ -532,12 +677,12 @@ var ChoiceQuestionSchema = QuestionSchema.concat(
|
|
|
532
677
|
);
|
|
533
678
|
|
|
534
679
|
// src/question/read-and-acknowledge.schema.ts
|
|
535
|
-
import { object as
|
|
536
|
-
var AcknowledgeAnswerSchema =
|
|
680
|
+
import { object as object17, string as string13 } from "yup";
|
|
681
|
+
var AcknowledgeAnswerSchema = string13().oneOf(["yes" /* Yes */, "no" /* No */]).required("This question must be acknowledged.");
|
|
537
682
|
var ReadAndAcknowledgeQuestionSchema = QuestionSchema.concat(
|
|
538
|
-
|
|
539
|
-
type:
|
|
540
|
-
description:
|
|
683
|
+
object17().shape({
|
|
684
|
+
type: string13().oneOf(["readAndAcknowledge" /* ReadAndAcknowledge */]).required().label("Question type"),
|
|
685
|
+
description: string13().max(1e4).required().label("Content"),
|
|
541
686
|
preferredAnswer: AcknowledgeAnswerSchema.label("Preferred Answer"),
|
|
542
687
|
answers: AcknowledgeAnswerSchema.label("Answer")
|
|
543
688
|
})
|
|
@@ -618,20 +763,20 @@ var SupportedApplicationReceivePreferences = Object.values(
|
|
|
618
763
|
);
|
|
619
764
|
|
|
620
765
|
// src/job/job.schema.ts
|
|
621
|
-
var JobSchema =
|
|
766
|
+
var JobSchema = object18({
|
|
622
767
|
/** Unique id usable by external systems to reference the job. */
|
|
623
|
-
uniqueId:
|
|
768
|
+
uniqueId: string14().optional().label("Unique ID"),
|
|
624
769
|
/** Job title shown in listings and search. */
|
|
625
|
-
headline:
|
|
770
|
+
headline: string14().required().min(5).max(150).label("Headline"),
|
|
626
771
|
/** User-friendly, unique URL identifier. */
|
|
627
|
-
slug:
|
|
772
|
+
slug: string14().required().max(250).label("Slug"),
|
|
628
773
|
/** How applications are received (inbox vs external website). */
|
|
629
774
|
applicationReceivePreference: mixed3().oneOf([
|
|
630
775
|
"inbox" /* Inbox */,
|
|
631
776
|
"external_website" /* ExternalWebsite */
|
|
632
777
|
]).required().label("Preference"),
|
|
633
778
|
/** Required when applicationReceivePreference is ExternalWebsite. */
|
|
634
|
-
externalApplyLink:
|
|
779
|
+
externalApplyLink: string14().when("applicationReceivePreference", {
|
|
635
780
|
is: (value) => value === "external_website" /* ExternalWebsite */,
|
|
636
781
|
then: (schema) => schema.url().required().label("Apply for Job link"),
|
|
637
782
|
otherwise: (schema) => schema.optional()
|
|
@@ -639,35 +784,35 @@ var JobSchema = object16({
|
|
|
639
784
|
/** Company the job belongs to (subset of PageSchema). */
|
|
640
785
|
company: PageSchema.pick(["name", "slug", "type", "logo"]).deepPartial().concat(PageSchema.pick(["name", "type"])).required().label("Company"),
|
|
641
786
|
/** @deprecated kept for backwards compatibility. */
|
|
642
|
-
designation:
|
|
643
|
-
employmentType:
|
|
644
|
-
workMode:
|
|
787
|
+
designation: string14().optional().nullable().label("Designation"),
|
|
788
|
+
employmentType: string14().required().label("Employment type"),
|
|
789
|
+
workMode: string14().required().label("Work mode"),
|
|
645
790
|
/**
|
|
646
791
|
* `conceptId`/`conceptName` carry the taxonomy-service normalization. They are
|
|
647
792
|
* picked here (not just `name`/`logo`) because SkillSchema is `.noUnknown()`:
|
|
648
793
|
* an unpicked field that is actually PRESENT fails validation, so omitting them
|
|
649
794
|
* would reject every normalized job.
|
|
650
795
|
*/
|
|
651
|
-
skills:
|
|
796
|
+
skills: array9().of(
|
|
652
797
|
SkillSchema.pick(["name", "logo", "conceptId", "conceptName"]).required().label("Skill")
|
|
653
798
|
).nullable().optional().label("Skills"),
|
|
654
|
-
bookmarks:
|
|
655
|
-
embedding:
|
|
656
|
-
vector:
|
|
657
|
-
model:
|
|
799
|
+
bookmarks: array9().of(UserIdAndCreatedAtSchema).optional().label("Bookmarks"),
|
|
800
|
+
embedding: object18({
|
|
801
|
+
vector: array9(number6().required()).optional().label("Embedding vector"),
|
|
802
|
+
model: string14().optional().label("Embedding model")
|
|
658
803
|
}).nullable().optional().label("Embedding"),
|
|
659
|
-
applicants:
|
|
660
|
-
applicantCount:
|
|
661
|
-
externalApplicationLinkClickCount:
|
|
662
|
-
description:
|
|
663
|
-
descriptionMarkdown:
|
|
664
|
-
descriptionHTML:
|
|
804
|
+
applicants: array9().of(UserIdAndCreatedAtSchema).optional().label("Applicants"),
|
|
805
|
+
applicantCount: number6().optional().label("Applicant count"),
|
|
806
|
+
externalApplicationLinkClickCount: number6().optional().label("External application link click count"),
|
|
807
|
+
description: string14().required().min(100).max(5e4).label("Description"),
|
|
808
|
+
descriptionMarkdown: string14().required().label("Description Markdown"),
|
|
809
|
+
descriptionHTML: string14().optional().label("Description HTML"),
|
|
665
810
|
/**
|
|
666
811
|
* Job locations. Required even for remote roles (compliance/legal). Defaults
|
|
667
812
|
* to an empty array; min(1) is intentionally not enforced yet.
|
|
668
813
|
*/
|
|
669
|
-
locations:
|
|
670
|
-
educationLevel:
|
|
814
|
+
locations: array9().of(LocationSchema).default([]).label("Location"),
|
|
815
|
+
educationLevel: array9(string14().oneOf(SupportedEducationLevels)).required().min(1).label("Educational level"),
|
|
671
816
|
/** Posting validity window (start/expiry). */
|
|
672
817
|
validity: DurationSchema({
|
|
673
818
|
format: SYSTEM_DATE_FORMAT,
|
|
@@ -680,21 +825,21 @@ var JobSchema = object16({
|
|
|
680
825
|
* has no `validity.endDate` and must be verified by crawling its page. Guards
|
|
681
826
|
* re-check cadence so the same job is not re-crawled on every sweep.
|
|
682
827
|
*/
|
|
683
|
-
lastExpiryCheckAt:
|
|
828
|
+
lastExpiryCheckAt: number6().optional().nullable().label("Last expiry check at"),
|
|
684
829
|
/**
|
|
685
830
|
* When this job transitioned to `status: closed` (epoch ms). Server-owned
|
|
686
831
|
* operational field: stamped by every jobs-service close path (expiry
|
|
687
832
|
* auto-close and the dated lapse sweep). Distinct from `updatedAt` so a later
|
|
688
833
|
* edit to a closed job does not move it.
|
|
689
834
|
*/
|
|
690
|
-
closedAt:
|
|
691
|
-
experienceLevel:
|
|
692
|
-
contactEmail:
|
|
693
|
-
workingHoursPerWeek:
|
|
694
|
-
tags:
|
|
835
|
+
closedAt: number6().optional().nullable().label("Closed at"),
|
|
836
|
+
experienceLevel: string14().oneOf(SupportedExperienceLevels).nullable().optional().label("Experience level"),
|
|
837
|
+
contactEmail: string14().email().nullable().optional().label("Contact email"),
|
|
838
|
+
workingHoursPerWeek: number6().nullable().optional().min(1).max(24 * 7).label("Working hours per week"),
|
|
839
|
+
tags: array9().max(50).optional().label("Tags"),
|
|
695
840
|
/** Direct/Quick-apply questionnaire. Each item is validated against the
|
|
696
841
|
* concrete question schema for its `type`, with the stored `answers` omitted. */
|
|
697
|
-
questionnaire:
|
|
842
|
+
questionnaire: array9().of(
|
|
698
843
|
lazy((question) => {
|
|
699
844
|
const schema = getSchemaByQuestion(question.type);
|
|
700
845
|
if (question.type === "input" /* Input */) {
|
|
@@ -711,12 +856,12 @@ var JobSchema = object16({
|
|
|
711
856
|
return schema;
|
|
712
857
|
})
|
|
713
858
|
).label("Questionnaire"),
|
|
714
|
-
status:
|
|
715
|
-
seoTags:
|
|
716
|
-
description:
|
|
717
|
-
keywords:
|
|
859
|
+
status: string14().oneOf(SupportedJobStatuses).default("draft" /* Draft */).required().label("Status"),
|
|
860
|
+
seoTags: object18({
|
|
861
|
+
description: string14().optional().label("Description"),
|
|
862
|
+
keywords: array9().of(string14()).optional().label("Keywords")
|
|
718
863
|
}).nullable().optional().label("SEO Tags"),
|
|
719
|
-
jdSummary:
|
|
864
|
+
jdSummary: string14().optional().label("JD Summary"),
|
|
720
865
|
/**
|
|
721
866
|
* Structured, model-extracted sections of the posting, re-organized into clean
|
|
722
867
|
* bullet points. This is our OWN unique presentation of the job (rendered as the
|
|
@@ -725,28 +870,28 @@ var JobSchema = object16({
|
|
|
725
870
|
* optional: absent on jobs not yet (re-)enriched, and any section is `[]`/omitted
|
|
726
871
|
* when the posting genuinely has none.
|
|
727
872
|
*/
|
|
728
|
-
jobHighlights:
|
|
729
|
-
responsibilities:
|
|
730
|
-
requirements:
|
|
731
|
-
benefits:
|
|
732
|
-
qualifications:
|
|
873
|
+
jobHighlights: object18({
|
|
874
|
+
responsibilities: array9().of(string14()).optional().label("Responsibilities"),
|
|
875
|
+
requirements: array9().of(string14()).optional().label("Requirements"),
|
|
876
|
+
benefits: array9().of(string14()).optional().label("Benefits"),
|
|
877
|
+
qualifications: array9().of(string14()).optional().label("Qualifications")
|
|
733
878
|
}).nullable().optional().label("Job Highlights"),
|
|
734
|
-
canCreateAlert:
|
|
735
|
-
canDirectApply:
|
|
736
|
-
hasApplied:
|
|
737
|
-
isOwner:
|
|
738
|
-
hasBookmarked:
|
|
739
|
-
hasReported:
|
|
740
|
-
ratePerHour:
|
|
741
|
-
isPromoted:
|
|
742
|
-
salaryRange:
|
|
743
|
-
currency:
|
|
744
|
-
min:
|
|
745
|
-
max:
|
|
746
|
-
compensationType:
|
|
747
|
-
isNegotiable:
|
|
879
|
+
canCreateAlert: boolean11().optional().label("Can create alert"),
|
|
880
|
+
canDirectApply: boolean11().optional().label("Can direct apply"),
|
|
881
|
+
hasApplied: boolean11().optional().label("Has applied"),
|
|
882
|
+
isOwner: boolean11().optional().label("Is owner"),
|
|
883
|
+
hasBookmarked: boolean11().optional().label("Has bookmarked"),
|
|
884
|
+
hasReported: boolean11().optional().label("Has reported"),
|
|
885
|
+
ratePerHour: number6().optional().min(0).label("Rate per hour"),
|
|
886
|
+
isPromoted: boolean11().optional().label("Is promoted"),
|
|
887
|
+
salaryRange: object18({
|
|
888
|
+
currency: string14().required().label("Currency").default("USD"),
|
|
889
|
+
min: number6().nullable().optional().min(0).label("Minimum Salary"),
|
|
890
|
+
max: number6().nullable().optional().min(0).label("Maximum Salary"),
|
|
891
|
+
compensationType: string14().oneOf(SupportedCompensationTypes).nullable().optional().label("Compensation Type"),
|
|
892
|
+
isNegotiable: boolean11().nullable().optional().default(true).label("Is Negotiable")
|
|
748
893
|
}).nullable().optional().label("Salary Range"),
|
|
749
|
-
reports:
|
|
894
|
+
reports: array9().of(UserIdAndCreatedAtSchema).optional().label("Reports"),
|
|
750
895
|
// Taxonomy fields are constants (see job-taxonomy.constant.ts), stored as slugs and
|
|
751
896
|
// translated for display. They are deliberately NOT `oneOf`-constrained here: a value
|
|
752
897
|
// outside the taxonomy must be DROPPED, never a reason to reject the posting. Yup
|
|
@@ -759,15 +904,135 @@ var JobSchema = object16({
|
|
|
759
904
|
// time (values get renamed, retired, added), so stored jobs will always outlive the
|
|
760
905
|
// enum — being lenient here is what lets the taxonomy evolve without a migration
|
|
761
906
|
// gate on every write.
|
|
762
|
-
category:
|
|
763
|
-
industry:
|
|
764
|
-
subCategories:
|
|
765
|
-
termsAccepted:
|
|
766
|
-
isFeatured:
|
|
907
|
+
category: string14().nullable().optional().label("Category"),
|
|
908
|
+
industry: string14().nullable().optional().label("Industry"),
|
|
909
|
+
subCategories: array9().of(string14().trim().required()).nullable().optional().label("Sub Categories"),
|
|
910
|
+
termsAccepted: boolean11().oneOf([true], "Accept terms before proceeding").required("Accept terms before proceeding").label("Terms accepted"),
|
|
911
|
+
isFeatured: boolean11().optional().default(false).label("Is featured"),
|
|
767
912
|
/** Market country codes to feature this job in; empty = global. */
|
|
768
|
-
featuredMarkets:
|
|
913
|
+
featuredMarkets: array9().of(string14().required()).optional().default([]).label("Featured markets")
|
|
769
914
|
}).concat(DbDefaultSchema).noUnknown().label("Job");
|
|
770
915
|
|
|
916
|
+
// src/job/job-filter.schema.ts
|
|
917
|
+
import { boolean as boolean12, number as number7, object as object19, string as string15 } from "yup";
|
|
918
|
+
|
|
919
|
+
// src/user/user.constant.ts
|
|
920
|
+
var UserRole = /* @__PURE__ */ ((UserRole2) => {
|
|
921
|
+
UserRole2["Admin"] = "admin";
|
|
922
|
+
UserRole2["Moderator"] = "moderator";
|
|
923
|
+
UserRole2["Manager"] = "manager";
|
|
924
|
+
UserRole2["Recruiter"] = "recruiter";
|
|
925
|
+
UserRole2["JobSeeker"] = "job_seeker";
|
|
926
|
+
UserRole2["Coordinator"] = "coordinator";
|
|
927
|
+
UserRole2["System"] = "system";
|
|
928
|
+
UserRole2["Normal"] = "normal";
|
|
929
|
+
return UserRole2;
|
|
930
|
+
})(UserRole || {});
|
|
931
|
+
var SupportedUserRoles = Object.values(UserRole);
|
|
932
|
+
var UserStatus = /* @__PURE__ */ ((UserStatus2) => {
|
|
933
|
+
UserStatus2["Active"] = "active";
|
|
934
|
+
UserStatus2["Blocked"] = "blocked";
|
|
935
|
+
UserStatus2["Pending"] = "pending";
|
|
936
|
+
UserStatus2["Deleted"] = "deleted";
|
|
937
|
+
UserStatus2["MarkedForDeletion"] = "marked_for_deletion";
|
|
938
|
+
return UserStatus2;
|
|
939
|
+
})(UserStatus || {});
|
|
940
|
+
var SupportedUserStatuses = Object.values(UserStatus);
|
|
941
|
+
var DefaultUserRoles = ["normal" /* Normal */];
|
|
942
|
+
var UserProfileVisibility = /* @__PURE__ */ ((UserProfileVisibility2) => {
|
|
943
|
+
UserProfileVisibility2["Public"] = "public";
|
|
944
|
+
UserProfileVisibility2["Private"] = "private";
|
|
945
|
+
UserProfileVisibility2["Hidden"] = "hidden";
|
|
946
|
+
return UserProfileVisibility2;
|
|
947
|
+
})(UserProfileVisibility || {});
|
|
948
|
+
var SupportedUserProfileVisibilities = Object.values(
|
|
949
|
+
UserProfileVisibility
|
|
950
|
+
);
|
|
951
|
+
var SignupContextSource = /* @__PURE__ */ ((SignupContextSource2) => {
|
|
952
|
+
SignupContextSource2["CfHeader"] = "cf-header";
|
|
953
|
+
SignupContextSource2["Location"] = "location";
|
|
954
|
+
SignupContextSource2["Default"] = "default";
|
|
955
|
+
return SignupContextSource2;
|
|
956
|
+
})(SignupContextSource || {});
|
|
957
|
+
var SupportedSignupContextSources = Object.values(SignupContextSource);
|
|
958
|
+
var UserDetailType = /* @__PURE__ */ ((UserDetailType2) => {
|
|
959
|
+
UserDetailType2["Overview"] = "overview";
|
|
960
|
+
UserDetailType2["AdditionalInfo"] = "additionalInfo";
|
|
961
|
+
UserDetailType2["General"] = "general";
|
|
962
|
+
UserDetailType2["SocialAccounts"] = "socialAccounts";
|
|
963
|
+
UserDetailType2["WorkExperiences"] = "workExperiences";
|
|
964
|
+
UserDetailType2["Educations"] = "educations";
|
|
965
|
+
UserDetailType2["Skills"] = "skills";
|
|
966
|
+
UserDetailType2["Languages"] = "languages";
|
|
967
|
+
UserDetailType2["Certifications"] = "certifications";
|
|
968
|
+
UserDetailType2["Interests"] = "interests";
|
|
969
|
+
UserDetailType2["Projects"] = "projects";
|
|
970
|
+
return UserDetailType2;
|
|
971
|
+
})(UserDetailType || {});
|
|
972
|
+
var ProficiencyLevel = /* @__PURE__ */ ((ProficiencyLevel2) => {
|
|
973
|
+
ProficiencyLevel2["BasicAwareness"] = "basic_awareness";
|
|
974
|
+
ProficiencyLevel2["Beginner"] = "beginner";
|
|
975
|
+
ProficiencyLevel2["Intermediate"] = "intermediate";
|
|
976
|
+
ProficiencyLevel2["Advanced"] = "advanced";
|
|
977
|
+
ProficiencyLevel2["Expert"] = "expert";
|
|
978
|
+
return ProficiencyLevel2;
|
|
979
|
+
})(ProficiencyLevel || {});
|
|
980
|
+
var SupportedProficiencyLevels = Object.values(ProficiencyLevel);
|
|
981
|
+
var JobSearchUrgency = /* @__PURE__ */ ((JobSearchUrgency2) => {
|
|
982
|
+
JobSearchUrgency2["Asap"] = "asap";
|
|
983
|
+
JobSearchUrgency2["WithinThreeMonths"] = "within_3_months";
|
|
984
|
+
JobSearchUrgency2["WithinSixMonths"] = "within_6_months";
|
|
985
|
+
JobSearchUrgency2["PassivelyBrowsing"] = "passively_browsing";
|
|
986
|
+
return JobSearchUrgency2;
|
|
987
|
+
})(JobSearchUrgency || {});
|
|
988
|
+
var SupportedJobSearchUrgencies = Object.values(JobSearchUrgency);
|
|
989
|
+
var ReferralSource = /* @__PURE__ */ ((ReferralSource2) => {
|
|
990
|
+
ReferralSource2["Recruiter"] = "recruiter";
|
|
991
|
+
ReferralSource2["LinkedIn"] = "linkedin";
|
|
992
|
+
ReferralSource2["TikTokInstagram"] = "tiktok_instagram";
|
|
993
|
+
ReferralSource2["Google"] = "google";
|
|
994
|
+
ReferralSource2["Reddit"] = "reddit";
|
|
995
|
+
ReferralSource2["ChatGptGemini"] = "chatgpt_gemini";
|
|
996
|
+
ReferralSource2["Friend"] = "friend";
|
|
997
|
+
ReferralSource2["Other"] = "other";
|
|
998
|
+
return ReferralSource2;
|
|
999
|
+
})(ReferralSource || {});
|
|
1000
|
+
var SupportedReferralSources = Object.values(ReferralSource);
|
|
1001
|
+
var JobAlertFrequency = /* @__PURE__ */ ((JobAlertFrequency2) => {
|
|
1002
|
+
JobAlertFrequency2["Off"] = "off";
|
|
1003
|
+
JobAlertFrequency2["Daily"] = "daily";
|
|
1004
|
+
JobAlertFrequency2["Weekly"] = "weekly";
|
|
1005
|
+
return JobAlertFrequency2;
|
|
1006
|
+
})(JobAlertFrequency || {});
|
|
1007
|
+
var SupportedJobAlertFrequencies = Object.values(JobAlertFrequency);
|
|
1008
|
+
var NotificationType = /* @__PURE__ */ ((NotificationType2) => {
|
|
1009
|
+
NotificationType2["GroupActivity"] = "groupActivity";
|
|
1010
|
+
NotificationType2["JobActivity"] = "jobActivity";
|
|
1011
|
+
NotificationType2["ApplicationUpdates"] = "applicationUpdates";
|
|
1012
|
+
NotificationType2["ProductUpdates"] = "productUpdates";
|
|
1013
|
+
return NotificationType2;
|
|
1014
|
+
})(NotificationType || {});
|
|
1015
|
+
var SupportedNotificationTypes = Object.values(NotificationType);
|
|
1016
|
+
var SupportedSalaryCurrencies = ["USD", "EUR", "GBP", "SEK", "INR"];
|
|
1017
|
+
var MIN_SALARY_LOWER_BOUND = 0;
|
|
1018
|
+
var MIN_SALARY_UPPER_BOUND = 1e6;
|
|
1019
|
+
var PREFILL_MIRRORED_KEYS = [
|
|
1020
|
+
"headline",
|
|
1021
|
+
"aboutMe",
|
|
1022
|
+
"mobile",
|
|
1023
|
+
"location",
|
|
1024
|
+
"experienceLevel",
|
|
1025
|
+
"socialAccounts",
|
|
1026
|
+
"workExperiences",
|
|
1027
|
+
"educations"
|
|
1028
|
+
];
|
|
1029
|
+
var PRIVATE_PROFILE_FIELDS = [
|
|
1030
|
+
"email",
|
|
1031
|
+
"emailVerified",
|
|
1032
|
+
"mobile",
|
|
1033
|
+
"additionalInfo"
|
|
1034
|
+
];
|
|
1035
|
+
|
|
771
1036
|
// src/job/job-taxonomy.constant.ts
|
|
772
1037
|
var JobCategory = /* @__PURE__ */ ((JobCategory2) => {
|
|
773
1038
|
JobCategory2["TechnologyDataAndDigital"] = "technology_data_and_digital";
|
|
@@ -1835,6 +2100,185 @@ var TAXONOMY_LABELS = {
|
|
|
1835
2100
|
"fleet_management": "Fleet Management"
|
|
1836
2101
|
};
|
|
1837
2102
|
|
|
2103
|
+
// src/job/job-filter.schema.ts
|
|
2104
|
+
var GEO_GROUP = "Geo";
|
|
2105
|
+
var JobFilterShape = object19({
|
|
2106
|
+
// ── Classification ──────────────────────────────────────────────────────────
|
|
2107
|
+
// Closed vocabularies, sent verbatim and matched exactly: a partial match
|
|
2108
|
+
// would make "remote" also match a hypothetical "remote_first".
|
|
2109
|
+
employmentType: enumFacet(SupportedEmploymentTypes, "Employment Type"),
|
|
2110
|
+
workMode: enumFacet(SupportedWorkModes, "Work Mode"),
|
|
2111
|
+
experienceLevel: enumFacet(SupportedExperienceLevels, "Experience Level"),
|
|
2112
|
+
educationLevel: enumFacet(SupportedEducationLevels, "Education Level"),
|
|
2113
|
+
status: enumFacet(SupportedJobStatuses, "Status"),
|
|
2114
|
+
// ── Taxonomy ────────────────────────────────────────────────────────────────
|
|
2115
|
+
// NOT `enumFacet`, deliberately. Taxonomy is stored as the ENGLISH LABEL
|
|
2116
|
+
// ("Technology, Data & Digital"), not the enum constant, so a caller sends the
|
|
2117
|
+
// label and constraining these to `SupportedJobCategories` would reject every
|
|
2118
|
+
// value that actually matches.
|
|
2119
|
+
//
|
|
2120
|
+
// `meta.options` publishes the vocabulary anyway, for forms only. The schema
|
|
2121
|
+
// still accepts any string — it must, per the note above — but an admin
|
|
2122
|
+
// picking one of eight categories should not be typing it: the stored label
|
|
2123
|
+
// is "Technology, Data & Digital", and a hand-typed "Technology" matches
|
|
2124
|
+
// nothing while looking correctly configured.
|
|
2125
|
+
category: tokens("Category").meta({ options: SupportedJobCategories }),
|
|
2126
|
+
industry: tokens("Industry").meta({ options: SupportedJobIndustries }),
|
|
2127
|
+
subCategories: tokens("Sub-category").meta({
|
|
2128
|
+
options: SupportedJobSubCategories
|
|
2129
|
+
}),
|
|
2130
|
+
tags: tokens("Tags"),
|
|
2131
|
+
// ── Identity ────────────────────────────────────────────────────────────────
|
|
2132
|
+
/**
|
|
2133
|
+
* Specific jobs, by `shortId`. The one facet that names jobs outright rather
|
|
2134
|
+
* than narrowing by an attribute of them.
|
|
2135
|
+
*
|
|
2136
|
+
* It exists for a HAND-PICKED set: a group whose jobs an admin chose
|
|
2137
|
+
* individually stores exactly this list as `group.jobIds`, so the field and
|
|
2138
|
+
* the facet share a name and the stored data needs no translation.
|
|
2139
|
+
*
|
|
2140
|
+
* Case is LOAD-BEARING and must not be normalised: a shortId is mixed case
|
|
2141
|
+
* (`tI8wnSIgRG5O`), so the uppercase treatment country and state codes get
|
|
2142
|
+
* would turn every lookup into a match on nothing.
|
|
2143
|
+
*/
|
|
2144
|
+
jobIds: tokens("Jobs"),
|
|
2145
|
+
// ── Skills and company ──────────────────────────────────────────────────────
|
|
2146
|
+
//
|
|
2147
|
+
// `widget` is a RENDERING hint for whoever builds a form from this schema: it
|
|
2148
|
+
// names the picker that produces values this facet can actually match, and
|
|
2149
|
+
// nothing validates differently because of it. A form generated without one
|
|
2150
|
+
// falls back to a free text box, which is what every admin filter was doing —
|
|
2151
|
+
// so "Angular" was typed where the stored skill is "angular" and the filter
|
|
2152
|
+
// narrowed to nothing while looking correctly configured.
|
|
2153
|
+
skills: tokens("Skills").meta({ widget: "skill" }),
|
|
2154
|
+
/** Company page SLUGS: a display name breaks the day a company renames itself. */
|
|
2155
|
+
companies: tokens("Companies").meta({ widget: "company" }),
|
|
2156
|
+
/**
|
|
2157
|
+
* Company display names, for callers that only have one. Prefer `companies`.
|
|
2158
|
+
*
|
|
2159
|
+
* `hidden`: not a second control in a generated form. An admin picking a
|
|
2160
|
+
* company gets the page, and `companies` (its slug) is the stable way to name
|
|
2161
|
+
* it — a display name stops matching the day a company renames itself. This
|
|
2162
|
+
* stays in the vocabulary for API callers who genuinely only hold a name.
|
|
2163
|
+
*/
|
|
2164
|
+
companyNames: tokens("Company Names").meta({ widget: "hidden" }),
|
|
2165
|
+
// ── Location ────────────────────────────────────────────────────────────────
|
|
2166
|
+
//
|
|
2167
|
+
// Four facets, THREE controls. They are independent OR-lists: jobs-service
|
|
2168
|
+
// matches each path separately, so `countryCodes` alone asks "anywhere in
|
|
2169
|
+
// India" and `cities` alone asks "in Pune, wherever that is". Collapsing them
|
|
2170
|
+
// into one object facet would lose exactly that, and lose the cross-entry
|
|
2171
|
+
// reading ("has an India office OR a London office") with it.
|
|
2172
|
+
//
|
|
2173
|
+
// What they must NOT be is free text. A country here is `IN`, not "India",
|
|
2174
|
+
// and a state code is a BARE `KA`, not `IN-KA` — type either long form and
|
|
2175
|
+
// the filter matches nothing while looking correctly configured.
|
|
2176
|
+
//
|
|
2177
|
+
// So each names the GEO reference autocomplete (common-service `/geo/*`, a
|
|
2178
|
+
// closed vocabulary) and the field of the picked row it stores. `geoLevel`
|
|
2179
|
+
// says which endpoint to search and `geoField` which key lands in the facet.
|
|
2180
|
+
//
|
|
2181
|
+
// THREE controls, not four: a picked state carries its name and its code, so
|
|
2182
|
+
// the old separate "State Codes" box asked twice for one fact. See `states`.
|
|
2183
|
+
//
|
|
2184
|
+
// Geoapify's `/locations` is NOT the vocabulary here. It geocodes free-text
|
|
2185
|
+
// addresses — the right tool for "where is this job", the wrong one for a
|
|
2186
|
+
// filter, since it is a paid call per keystroke and its results are open.
|
|
2187
|
+
/** ISO 3166-1 alpha-2, uppercase. */
|
|
2188
|
+
countryCodes: codeTokens("Countries").meta({
|
|
2189
|
+
group: GEO_GROUP,
|
|
2190
|
+
widget: "geo",
|
|
2191
|
+
geoLevel: "country",
|
|
2192
|
+
geoField: "countryCode"
|
|
2193
|
+
}),
|
|
2194
|
+
/** Stored BARE (`KA`, not `IN-KA`) — see @todo STATE-CODE-ISO-PREFIX. */
|
|
2195
|
+
stateCodes: codeTokens("States").meta({
|
|
2196
|
+
group: GEO_GROUP,
|
|
2197
|
+
widget: "geo",
|
|
2198
|
+
geoLevel: "state",
|
|
2199
|
+
geoField: "stateCode"
|
|
2200
|
+
}),
|
|
2201
|
+
/**
|
|
2202
|
+
* State NAMES, kept as a facet but WITHOUT a control of its own.
|
|
2203
|
+
*
|
|
2204
|
+
* There is no second box to fill: a picked state carries its name and its
|
|
2205
|
+
* code together, so asking an admin for both was asking twice for one fact.
|
|
2206
|
+
* The picker writes `stateCodes`, which is where the data is going.
|
|
2207
|
+
*
|
|
2208
|
+
* The facet stays because jobs still hold state names — 15,483 carry a name
|
|
2209
|
+
* with NO code — and a saved campaign may already constrain on it. Removing
|
|
2210
|
+
* it would silently drop that constraint from those campaigns. It becomes
|
|
2211
|
+
* unreachable from the UI and is settled by the data migration.
|
|
2212
|
+
*
|
|
2213
|
+
* @todo STATE-CODE-ISO-PREFIX — retire once every job carries a stateCode.
|
|
2214
|
+
*/
|
|
2215
|
+
states: tokens("States").meta({ group: GEO_GROUP, widget: "hidden" }),
|
|
2216
|
+
/**
|
|
2217
|
+
* City NAMES, and the one facet here that does not match on a code.
|
|
2218
|
+
*
|
|
2219
|
+
* A name is a display label, not a matching key: it is not unique (99 Indian
|
|
2220
|
+
* city names repeat in the reference data) and not stable (Bengaluru was
|
|
2221
|
+
* Bangalore). Every other facet here matches a code for exactly that reason.
|
|
2222
|
+
*
|
|
2223
|
+
* Cities cannot yet. There is no city code to match ON either side: the
|
|
2224
|
+
* reference dataset carries none, and no job stores one — `LocationSchema`
|
|
2225
|
+
* has country/state codes and a city NAME. A `cityCodes` facet would match
|
|
2226
|
+
* zero of 82,875 jobs on day one.
|
|
2227
|
+
*
|
|
2228
|
+
* So this stays on the name, and the picker does the work a code would: the
|
|
2229
|
+
* admin selects a real city from the reference data, so the stored string is
|
|
2230
|
+
* guaranteed to be a spelling that exists rather than a typo that silently
|
|
2231
|
+
* matches nothing.
|
|
2232
|
+
*
|
|
2233
|
+
* @todo CITY-CODE-UNLOCODE — the fix is UN/LOCODE (or IATA) as the city key,
|
|
2234
|
+
* and it is a data programme, not a facet change: ingest and reconcile the
|
|
2235
|
+
* codes against the 152,970 reference rows, add `cityCode` to
|
|
2236
|
+
* `LocationSchema`, backfill the ~81,000 jobs that carry only a name, and fix
|
|
2237
|
+
* the write paths that emit it. Only then can this match on a code.
|
|
2238
|
+
*/
|
|
2239
|
+
cities: tokens("Cities").meta({
|
|
2240
|
+
group: GEO_GROUP,
|
|
2241
|
+
widget: "geo",
|
|
2242
|
+
geoLevel: "city",
|
|
2243
|
+
geoField: "name"
|
|
2244
|
+
}),
|
|
2245
|
+
// ── Flags ───────────────────────────────────────────────────────────────────
|
|
2246
|
+
// No `.default(false)`: an unchecked box must mean "no constraint", not "only
|
|
2247
|
+
// the non-featured ones", so absent simply omits the clause.
|
|
2248
|
+
isFeatured: boolean12().optional().label("Featured Only"),
|
|
2249
|
+
isPromoted: boolean12().optional().label("Promoted Only"),
|
|
2250
|
+
featuredMarkets: tokens("Featured Markets"),
|
|
2251
|
+
// ── Ranges ──────────────────────────────────────────────────────────────────
|
|
2252
|
+
/**
|
|
2253
|
+
* Matched as an OVERLAP against the job's advertised band, not as a bound on
|
|
2254
|
+
* one end: `{min}` keeps jobs whose `salaryRange.max` reaches it.
|
|
2255
|
+
*/
|
|
2256
|
+
// NOT `currencyBound`: that guards the USER filter, where several currencies
|
|
2257
|
+
// can be selected and a bound is meaningless across them. Here the currency is
|
|
2258
|
+
// a single sibling field, so the bound is plain and the currency simply
|
|
2259
|
+
// narrows which jobs are compared.
|
|
2260
|
+
salary: object19({
|
|
2261
|
+
min: number7().integer().min(0).optional().default(void 0).label("Salary \u2265"),
|
|
2262
|
+
max: number7().integer().min(0).optional().default(void 0).label("Salary \u2264"),
|
|
2263
|
+
currency: string15().oneOf(SupportedSalaryCurrencies).optional().label("Currency")
|
|
2264
|
+
}).optional().default(void 0).label("Salary"),
|
|
2265
|
+
/** Posted within this many days. Relative, so a saved filter does not rot. */
|
|
2266
|
+
postedWithinDays: days("Posted Within (days)"),
|
|
2267
|
+
/**
|
|
2268
|
+
* Absolute posting window, for callers that want bounds rather than the
|
|
2269
|
+
* relative `postedWithinDays`. EPOCH MS, matching how the field is stored — a
|
|
2270
|
+
* coerced date string would not compare.
|
|
2271
|
+
*/
|
|
2272
|
+
createdAt: object19({
|
|
2273
|
+
min: epoch("Posted After"),
|
|
2274
|
+
max: epoch("Posted Before")
|
|
2275
|
+
}).optional().default(void 0).label("Posted Between")
|
|
2276
|
+
}).label("Job Filter");
|
|
2277
|
+
var JobFilterSchema = rejectUnknownKeys(
|
|
2278
|
+
JobFilterShape,
|
|
2279
|
+
() => Object.keys(JobFilterShape.fields)
|
|
2280
|
+
);
|
|
2281
|
+
|
|
1838
2282
|
// src/job/job-taxonomy.util.ts
|
|
1839
2283
|
var INDUSTRY_OF_SUB = new Map(
|
|
1840
2284
|
JOB_TAXONOMY.flatMap(
|
|
@@ -1879,7 +2323,7 @@ function deriveIndustry(category, subCategories) {
|
|
|
1879
2323
|
}
|
|
1880
2324
|
|
|
1881
2325
|
// src/post/post.schema.ts
|
|
1882
|
-
import { array as
|
|
2326
|
+
import { array as array11, boolean as boolean13, number as number8, object as object20, string as string16 } from "yup";
|
|
1883
2327
|
|
|
1884
2328
|
// src/post/post.constant.ts
|
|
1885
2329
|
var PostStatus = /* @__PURE__ */ ((PostStatus2) => {
|
|
@@ -1891,177 +2335,540 @@ var PostStatus = /* @__PURE__ */ ((PostStatus2) => {
|
|
|
1891
2335
|
var SupportedPostStatuses = Object.values(PostStatus);
|
|
1892
2336
|
|
|
1893
2337
|
// src/post/post.schema.ts
|
|
1894
|
-
var PostSchema =
|
|
2338
|
+
var PostSchema = object20({
|
|
1895
2339
|
/** Headline shown in listings and at the top of the post. */
|
|
1896
|
-
title:
|
|
2340
|
+
title: string16().required().min(5).max(200).label("Title"),
|
|
1897
2341
|
/** User-friendly, unique URL identifier. */
|
|
1898
|
-
slug:
|
|
2342
|
+
slug: string16().required().max(250).label("Slug"),
|
|
1899
2343
|
/** Short summary used in listings and meta description fallback. */
|
|
1900
|
-
excerpt:
|
|
2344
|
+
excerpt: string16().max(500).optional().label("Excerpt"),
|
|
1901
2345
|
/** Rendered HTML source of truth for the post body (sanitized on write/render).
|
|
1902
2346
|
* Supports rich formatting markdown can't express (image size/alignment, etc). */
|
|
1903
|
-
bodyHTML:
|
|
2347
|
+
bodyHTML: string16().required().min(1).label("Body (HTML)"),
|
|
1904
2348
|
/** Optional Markdown mirror of the body (portable text / search fallback).
|
|
1905
2349
|
* Kept for back-compat and plain-text derivation; bodyHTML is authoritative. */
|
|
1906
|
-
bodyMarkdown:
|
|
2350
|
+
bodyMarkdown: string16().optional().label("Body (Markdown)"),
|
|
1907
2351
|
/** Cover/hero image URL. Not `.url()`-validated: the value is either an
|
|
1908
2352
|
* uploaded file URL served by this platform (which may be a bare host like
|
|
1909
2353
|
* `http://localhost:3082/...` in dev, that yup's `.url()` wrongly rejects) or
|
|
1910
2354
|
* an admin-pasted link. Kept as a plain string; empty ⇒ null. */
|
|
1911
|
-
coverImage:
|
|
2355
|
+
coverImage: string16().nullable().optional().label("Cover image"),
|
|
1912
2356
|
/** Author (user id). */
|
|
1913
|
-
authorId:
|
|
2357
|
+
authorId: string16().required().label("Author ID"),
|
|
1914
2358
|
/** Free-form tags for filtering and related-posts. */
|
|
1915
|
-
tags:
|
|
2359
|
+
tags: array11().of(string16().trim().required()).default([]).label("Tags"),
|
|
1916
2360
|
/** Estimated reading time in minutes (server-derived from body length). */
|
|
1917
|
-
readingTimeMinutes:
|
|
2361
|
+
readingTimeMinutes: number8().min(0).optional().label("Reading time (minutes)"),
|
|
1918
2362
|
/** SEO overrides. */
|
|
1919
|
-
seoTags:
|
|
1920
|
-
description:
|
|
1921
|
-
keywords:
|
|
2363
|
+
seoTags: object20({
|
|
2364
|
+
description: string16().optional().label("Description"),
|
|
2365
|
+
keywords: array11().of(string16().required()).optional().label("Keywords")
|
|
1922
2366
|
}).nullable().optional().label("SEO Tags"),
|
|
1923
2367
|
/** When the post was first published (epoch ms). Server-owned. */
|
|
1924
|
-
publishedAt:
|
|
2368
|
+
publishedAt: number8().nullable().optional().label("Published at"),
|
|
1925
2369
|
/** Whether the post is featured in the blog listing. */
|
|
1926
|
-
isFeatured:
|
|
2370
|
+
isFeatured: boolean13().default(false).optional().label("Is featured"),
|
|
1927
2371
|
/** Publication state. */
|
|
1928
|
-
status:
|
|
2372
|
+
status: string16().oneOf(SupportedPostStatuses).default("draft" /* Draft */).required().label("Status")
|
|
1929
2373
|
}).concat(DbDefaultSchema).noUnknown().label("Post");
|
|
1930
2374
|
|
|
1931
|
-
// src/
|
|
1932
|
-
|
|
2375
|
+
// src/geo/geo.constant.ts
|
|
2376
|
+
var GeoKind = /* @__PURE__ */ ((GeoKind2) => {
|
|
2377
|
+
GeoKind2["Country"] = "country";
|
|
2378
|
+
GeoKind2["State"] = "state";
|
|
2379
|
+
GeoKind2["City"] = "city";
|
|
2380
|
+
return GeoKind2;
|
|
2381
|
+
})(GeoKind || {});
|
|
2382
|
+
var SupportedGeoKinds = Object.values(GeoKind);
|
|
2383
|
+
var GeoSyncStatus = /* @__PURE__ */ ((GeoSyncStatus3) => {
|
|
2384
|
+
GeoSyncStatus3["Pending"] = "pending";
|
|
2385
|
+
GeoSyncStatus3["Running"] = "running";
|
|
2386
|
+
GeoSyncStatus3["Succeeded"] = "succeeded";
|
|
2387
|
+
GeoSyncStatus3["Failed"] = "failed";
|
|
2388
|
+
GeoSyncStatus3["Skipped"] = "skipped";
|
|
2389
|
+
return GeoSyncStatus3;
|
|
2390
|
+
})(GeoSyncStatus || {});
|
|
2391
|
+
var SupportedGeoSyncStatuses = Object.values(GeoSyncStatus);
|
|
2392
|
+
var TerminalGeoSyncStatuses = [
|
|
2393
|
+
"succeeded" /* Succeeded */,
|
|
2394
|
+
"failed" /* Failed */,
|
|
2395
|
+
"skipped" /* Skipped */
|
|
2396
|
+
];
|
|
2397
|
+
var GeoSyncPhase = /* @__PURE__ */ ((GeoSyncPhase3) => {
|
|
2398
|
+
GeoSyncPhase3["Checking"] = "checking";
|
|
2399
|
+
GeoSyncPhase3["Countries"] = "countries";
|
|
2400
|
+
GeoSyncPhase3["States"] = "states";
|
|
2401
|
+
GeoSyncPhase3["Cities"] = "cities";
|
|
2402
|
+
GeoSyncPhase3["Indexing"] = "indexing";
|
|
2403
|
+
GeoSyncPhase3["Done"] = "done";
|
|
2404
|
+
return GeoSyncPhase3;
|
|
2405
|
+
})(GeoSyncPhase || {});
|
|
2406
|
+
var SupportedGeoSyncPhases = Object.values(GeoSyncPhase);
|
|
2407
|
+
var GeoSyncTrigger = /* @__PURE__ */ ((GeoSyncTrigger3) => {
|
|
2408
|
+
GeoSyncTrigger3["Manual"] = "manual";
|
|
2409
|
+
GeoSyncTrigger3["Boot"] = "boot";
|
|
2410
|
+
return GeoSyncTrigger3;
|
|
2411
|
+
})(GeoSyncTrigger || {});
|
|
2412
|
+
var SupportedGeoSyncTriggers = Object.values(GeoSyncTrigger);
|
|
2413
|
+
var GEO_COLLECTIONS = {
|
|
2414
|
+
countries: "geo_countries",
|
|
2415
|
+
states: "geo_states",
|
|
2416
|
+
cities: "geo_cities",
|
|
2417
|
+
syncRuns: "geo_sync_runs"
|
|
2418
|
+
};
|
|
2419
|
+
var GEO_SEARCH_MAX = 64;
|
|
1933
2420
|
|
|
1934
|
-
// src/
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2421
|
+
// src/geo/country.schema.ts
|
|
2422
|
+
import { array as array12, number as number9, object as object21, string as string17 } from "yup";
|
|
2423
|
+
var CountrySchema = object21({
|
|
2424
|
+
/** The upstream dr5hn id. Stable, and the key every upsert matches on. */
|
|
2425
|
+
id: number9().integer().required().label("ID"),
|
|
2426
|
+
name: string17().trim().required().label("Name"),
|
|
2427
|
+
/**
|
|
2428
|
+
* ISO 3166-1 alpha-2, UPPERCASE (`IN`, `SE`).
|
|
2429
|
+
*
|
|
2430
|
+
* Uppercased on ingest, because every filter facet in this package
|
|
2431
|
+
* uppercase-normalizes codes (`codeTokens`) and stored job/user documents are
|
|
2432
|
+
* already clean uppercase alpha-2. A lowercase value here would compare
|
|
2433
|
+
* unequal to all of them and silently match nothing.
|
|
2434
|
+
*/
|
|
2435
|
+
countryCode: string17().trim().uppercase().length(2).required().label("Country Code"),
|
|
2436
|
+
iso3: string17().trim().uppercase().length(3).optional().nullable().label("ISO3"),
|
|
2437
|
+
numericCode: string17().trim().optional().nullable().label("Numeric Code"),
|
|
2438
|
+
phonecode: string17().trim().optional().nullable().label("Phone Code"),
|
|
2439
|
+
capital: string17().trim().optional().nullable().label("Capital"),
|
|
2440
|
+
currency: string17().trim().optional().nullable().label("Currency"),
|
|
2441
|
+
currencyName: string17().trim().optional().nullable().label("Currency Name"),
|
|
2442
|
+
currencySymbol: string17().trim().optional().nullable().label("Currency Symbol"),
|
|
2443
|
+
tld: string17().trim().optional().nullable().label("TLD"),
|
|
2444
|
+
/** The name in its own script, e.g. "भारत". Stored, but NOT searchable; see `searchByLocale`. */
|
|
2445
|
+
native: string17().trim().optional().nullable().label("Native Name"),
|
|
2446
|
+
population: number9().optional().nullable().label("Population"),
|
|
2447
|
+
gdp: number9().optional().nullable().label("GDP"),
|
|
2448
|
+
region: string17().trim().optional().nullable().label("Region"),
|
|
2449
|
+
regionId: number9().integer().optional().nullable().label("Region ID"),
|
|
2450
|
+
subregion: string17().trim().optional().nullable().label("Subregion"),
|
|
2451
|
+
subregionId: number9().integer().optional().nullable().label("Subregion ID"),
|
|
2452
|
+
nationality: string17().trim().optional().nullable().label("Nationality"),
|
|
2453
|
+
areaSqKm: number9().optional().nullable().label("Area (sq km)"),
|
|
2454
|
+
postalCodeFormat: string17().trim().optional().nullable().label("Postal Code Format"),
|
|
2455
|
+
postalCodeRegex: string17().optional().nullable().label("Postal Code Regex"),
|
|
2456
|
+
timezones: array12().of(
|
|
2457
|
+
object21({
|
|
2458
|
+
zoneName: string17().optional().nullable().label("Zone Name"),
|
|
2459
|
+
gmtOffset: number9().optional().nullable().label("GMT Offset"),
|
|
2460
|
+
gmtOffsetName: string17().optional().nullable().label("GMT Offset Name"),
|
|
2461
|
+
abbreviation: string17().optional().nullable().label("Abbreviation"),
|
|
2462
|
+
tzName: string17().optional().nullable().label("TZ Name")
|
|
2463
|
+
})
|
|
2464
|
+
).optional().nullable().label("Timezones"),
|
|
2465
|
+
/**
|
|
2466
|
+
* Localized names keyed by locale (`{ sv: "Indien", fr: "Inde", … }`), ~19 of
|
|
2467
|
+
* them. Deliberately UNSHAPED: the key set is upstream's to change, and
|
|
2468
|
+
* declaring it here would turn a new locale into a validation failure. The
|
|
2469
|
+
* VALUES are split per-locale into `searchByLocale`.
|
|
2470
|
+
*/
|
|
2471
|
+
translations: object21().optional().nullable().label("Translations"),
|
|
2472
|
+
/**
|
|
2473
|
+
* Coordinates as NUMBERS.
|
|
2474
|
+
*
|
|
2475
|
+
* Upstream ships these as strings (`"33.00000000"`). They are coerced on
|
|
2476
|
+
* ingest, because a range query against a string compares lexicographically:
|
|
2477
|
+
* `"9" > "10"` is true, so a bounding-box filter would be quietly wrong rather
|
|
2478
|
+
* than failing loudly.
|
|
2479
|
+
*/
|
|
2480
|
+
latitude: number9().optional().nullable().label("Latitude"),
|
|
2481
|
+
longitude: number9().optional().nullable().label("Longitude"),
|
|
2482
|
+
emoji: string17().optional().nullable().label("Emoji"),
|
|
2483
|
+
emojiU: string17().optional().nullable().label("Emoji Unicode"),
|
|
2484
|
+
/**
|
|
2485
|
+
* The name field searched for the DEFAULT (English) locale: the canonical
|
|
2486
|
+
* `name`, and nothing else.
|
|
2487
|
+
*
|
|
2488
|
+
* Search is scoped to ONE locale, so the searchable names are split across
|
|
2489
|
+
* this field and `searchByLocale` rather than pooled into one array. A pooled
|
|
2490
|
+
* field would make every locale's search implicitly a search of all 19
|
|
2491
|
+
* languages: a French UI searching "Inde" would also match German "Indien".
|
|
2492
|
+
*/
|
|
2493
|
+
searchDefault: array12().of(string17().required()).optional().default([]).label("Search Default"),
|
|
2494
|
+
/**
|
|
2495
|
+
* Per-locale name fields, keyed by lowercased BCP-47 tag (`de`, `pt-br`).
|
|
2496
|
+
* Each carries ONLY that locale's translation, which is what makes
|
|
2497
|
+
* `?locale=de` German and not "German plus everything that looks similar".
|
|
2498
|
+
*
|
|
2499
|
+
* Unshaped for the same reason as `translations`: the key set is upstream's.
|
|
2500
|
+
*
|
|
2501
|
+
* `native` is deliberately NOT indexed here. It cannot be attributed to a
|
|
2502
|
+
* language with this data — 84% of city native names equal SEVERAL locales'
|
|
2503
|
+
* translations (Latin spellings coinciding across de/it/nl/hr/pl) while
|
|
2504
|
+
* distinctive non-Latin names like `भारत` match none — so filing `कर्नाटक`
|
|
2505
|
+
* under German would be worse than not indexing it at all.
|
|
2506
|
+
*
|
|
2507
|
+
* The 19 upstream locales are br, ko, pt-BR, pt, nl, hr, fa, de, es, fr, ja,
|
|
2508
|
+
* it, zh-CN, tr, ru, uk, pl, hi, ar. **`sv` is absent**, so a Swedish search
|
|
2509
|
+
* matches nothing. That is the accepted cost of strict scoping.
|
|
2510
|
+
*/
|
|
2511
|
+
searchByLocale: object21().optional().nullable().label("Search By Locale"),
|
|
2512
|
+
/** Epoch ms of the run that last wrote this row. Drives the stale-row prune. */
|
|
2513
|
+
syncedAt: number9().optional().nullable().label("Synced At")
|
|
2514
|
+
}).label("Country");
|
|
2515
|
+
|
|
2516
|
+
// src/geo/state.schema.ts
|
|
2517
|
+
import { array as array13, number as number10, object as object22, string as string18 } from "yup";
|
|
2518
|
+
var StateSchema = object22({
|
|
2519
|
+
/** The upstream dr5hn id. Stable, and the key every upsert matches on. */
|
|
2520
|
+
id: number10().integer().required().label("ID"),
|
|
2521
|
+
name: string18().trim().required().label("Name"),
|
|
2522
|
+
countryId: number10().integer().optional().nullable().label("Country ID"),
|
|
2523
|
+
/** ISO 3166-1 alpha-2 of the parent, UPPERCASE. See `CountrySchema.countryCode`. */
|
|
2524
|
+
countryCode: string18().trim().uppercase().length(2).required().label("Country Code"),
|
|
2525
|
+
countryName: string18().trim().optional().nullable().label("Country Name"),
|
|
2526
|
+
/**
|
|
2527
|
+
* The BARE subdivision code, e.g. `KA` (upstream `iso2`).
|
|
2528
|
+
*
|
|
2529
|
+
* `@todo STATE-CODE-ISO-PREFIX` — this is **not globally unique**: `BDS`, `CA`,
|
|
2530
|
+
* `NL` and `SP` all recur across several countries, so it is only meaningful
|
|
2531
|
+
* PAIRED with `countryCode`. That is why the cities route is nested under both
|
|
2532
|
+
* codes rather than under a state code alone, which would silently serve
|
|
2533
|
+
* another country's cities. `stateCodeIso` below is the migration target;
|
|
2534
|
+
* both are stored so the migration has its destination present before any
|
|
2535
|
+
* consumer moves.
|
|
2536
|
+
*/
|
|
2537
|
+
stateCode: string18().trim().uppercase().optional().nullable().label("State Code"),
|
|
2538
|
+
/** Fully-qualified ISO 3166-2, e.g. `IN-KA` (upstream `iso3166_2`). */
|
|
2539
|
+
stateCodeIso: string18().trim().uppercase().optional().nullable().label("ISO 3166-2 Code"),
|
|
2540
|
+
fipsCode: string18().trim().optional().nullable().label("FIPS Code"),
|
|
2541
|
+
/** Upstream's own word for the subdivision: "province", "state", "district", … */
|
|
2542
|
+
type: string18().trim().optional().nullable().label("Type"),
|
|
2543
|
+
level: number10().integer().optional().nullable().label("Level"),
|
|
2544
|
+
parentId: number10().integer().optional().nullable().label("Parent ID"),
|
|
2545
|
+
/** The name in its own script. Stored, but NOT searchable; see `searchByLocale`. */
|
|
2546
|
+
native: string18().trim().optional().nullable().label("Native Name"),
|
|
2547
|
+
/** Numbers, not the strings upstream ships. See `CountrySchema.latitude`. */
|
|
2548
|
+
latitude: number10().optional().nullable().label("Latitude"),
|
|
2549
|
+
longitude: number10().optional().nullable().label("Longitude"),
|
|
2550
|
+
timezone: string18().trim().optional().nullable().label("Timezone"),
|
|
2551
|
+
/** Localized names keyed by locale. Unshaped; see `CountrySchema.translations`. */
|
|
2552
|
+
translations: object22().optional().nullable().label("Translations"),
|
|
2553
|
+
/**
|
|
2554
|
+
* The name field searched for the DEFAULT (English) locale: the canonical
|
|
2555
|
+
* `name`, and nothing else.
|
|
2556
|
+
*
|
|
2557
|
+
* Search is scoped to ONE locale, so the searchable names are split across
|
|
2558
|
+
* this field and `searchByLocale` rather than pooled into one array. A pooled
|
|
2559
|
+
* field would make every locale's search implicitly a search of all 19
|
|
2560
|
+
* languages: a French UI searching "Inde" would also match German "Indien".
|
|
2561
|
+
*/
|
|
2562
|
+
searchDefault: array13().of(string18().required()).optional().default([]).label("Search Default"),
|
|
2563
|
+
/**
|
|
2564
|
+
* Per-locale name fields, keyed by lowercased BCP-47 tag (`de`, `pt-br`).
|
|
2565
|
+
* Each carries ONLY that locale's translation, which is what makes
|
|
2566
|
+
* `?locale=de` German and not "German plus everything that looks similar".
|
|
2567
|
+
*
|
|
2568
|
+
* Unshaped for the same reason as `translations`: the key set is upstream's.
|
|
2569
|
+
*
|
|
2570
|
+
* `native` is deliberately NOT indexed here. It cannot be attributed to a
|
|
2571
|
+
* language with this data — 84% of city native names equal SEVERAL locales'
|
|
2572
|
+
* translations (Latin spellings coinciding across de/it/nl/hr/pl) while
|
|
2573
|
+
* distinctive non-Latin names like `भारत` match none — so filing `कर्नाटक`
|
|
2574
|
+
* under German would be worse than not indexing it at all.
|
|
2575
|
+
*
|
|
2576
|
+
* The 19 upstream locales are br, ko, pt-BR, pt, nl, hr, fa, de, es, fr, ja,
|
|
2577
|
+
* it, zh-CN, tr, ru, uk, pl, hi, ar. **`sv` is absent**, so a Swedish search
|
|
2578
|
+
* matches nothing. That is the accepted cost of strict scoping.
|
|
2579
|
+
*/
|
|
2580
|
+
searchByLocale: object22().optional().nullable().label("Search By Locale"),
|
|
2581
|
+
/** Epoch ms of the run that last wrote this row. Drives the stale-row prune. */
|
|
2582
|
+
syncedAt: number10().optional().nullable().label("Synced At")
|
|
2583
|
+
}).label("State");
|
|
2584
|
+
|
|
2585
|
+
// src/geo/city.schema.ts
|
|
2586
|
+
import { array as array14, number as number11, object as object23, string as string19 } from "yup";
|
|
2587
|
+
var CitySchema = object23({
|
|
2588
|
+
/** The upstream dr5hn id. Stable, and the key every upsert matches on. */
|
|
2589
|
+
id: number11().integer().required().label("ID"),
|
|
2590
|
+
name: string19().trim().required().label("Name"),
|
|
2591
|
+
stateId: number11().integer().optional().nullable().label("State ID"),
|
|
2592
|
+
/** The BARE subdivision code. Only meaningful paired with `countryCode`; see `StateSchema`. */
|
|
2593
|
+
stateCode: string19().trim().uppercase().optional().nullable().label("State Code"),
|
|
2594
|
+
/**
|
|
2595
|
+
* Fully-qualified ISO 3166-2, e.g. `IN-KA`.
|
|
2596
|
+
*
|
|
2597
|
+
* DERIVED on ingest as `${countryCode}-${stateCode}`, because the upstream city
|
|
2598
|
+
* record carries only the bare `state_code` — unlike the state record, which
|
|
2599
|
+
* ships `iso3166_2` directly. Stored anyway so the `@todo STATE-CODE-ISO-PREFIX`
|
|
2600
|
+
* migration can re-point the cities route without a 153k-row re-ingest.
|
|
2601
|
+
* Null when either half is missing.
|
|
2602
|
+
*/
|
|
2603
|
+
stateCodeIso: string19().trim().uppercase().optional().nullable().label("ISO 3166-2 Code"),
|
|
2604
|
+
stateName: string19().trim().optional().nullable().label("State Name"),
|
|
2605
|
+
countryId: number11().integer().optional().nullable().label("Country ID"),
|
|
2606
|
+
/** ISO 3166-1 alpha-2 of the parent, UPPERCASE. See `CountrySchema.countryCode`. */
|
|
2607
|
+
countryCode: string19().trim().uppercase().length(2).required().label("Country Code"),
|
|
2608
|
+
countryName: string19().trim().optional().nullable().label("Country Name"),
|
|
2609
|
+
/** Numbers, not the strings upstream ships. See `CountrySchema.latitude`. */
|
|
2610
|
+
latitude: number11().optional().nullable().label("Latitude"),
|
|
2611
|
+
longitude: number11().optional().nullable().label("Longitude"),
|
|
2612
|
+
/** The name in its own script. Stored, but NOT searchable; see `searchByLocale`. */
|
|
2613
|
+
native: string19().trim().optional().nullable().label("Native Name"),
|
|
2614
|
+
type: string19().trim().optional().nullable().label("Type"),
|
|
2615
|
+
level: number11().integer().optional().nullable().label("Level"),
|
|
2616
|
+
parentId: number11().integer().optional().nullable().label("Parent ID"),
|
|
2617
|
+
/**
|
|
2618
|
+
* Null on most rows. Worth keeping (it is a good relevance signal when
|
|
2619
|
+
* present) but never worth SORTING by alone, since a null-heavy sort buries
|
|
2620
|
+
* every city upstream has no figure for.
|
|
2621
|
+
*/
|
|
2622
|
+
population: number11().optional().nullable().label("Population"),
|
|
2623
|
+
timezone: string19().trim().optional().nullable().label("Timezone"),
|
|
2624
|
+
/** Localized names keyed by locale. Unshaped; see `CountrySchema.translations`. */
|
|
2625
|
+
translations: object23().optional().nullable().label("Translations"),
|
|
2626
|
+
/**
|
|
2627
|
+
* The name field searched for the DEFAULT (English) locale: the canonical
|
|
2628
|
+
* `name`, and nothing else.
|
|
2629
|
+
*
|
|
2630
|
+
* Search is scoped to ONE locale, so the searchable names are split across
|
|
2631
|
+
* this field and `searchByLocale` rather than pooled into one array. A pooled
|
|
2632
|
+
* field would make every locale's search implicitly a search of all 19
|
|
2633
|
+
* languages: a French UI searching "Inde" would also match German "Indien".
|
|
2634
|
+
*/
|
|
2635
|
+
searchDefault: array14().of(string19().required()).optional().default([]).label("Search Default"),
|
|
2636
|
+
/**
|
|
2637
|
+
* Per-locale name fields, keyed by lowercased BCP-47 tag (`de`, `pt-br`).
|
|
2638
|
+
* Each carries ONLY that locale's translation, which is what makes
|
|
2639
|
+
* `?locale=de` German and not "German plus everything that looks similar".
|
|
2640
|
+
*
|
|
2641
|
+
* Unshaped for the same reason as `translations`: the key set is upstream's.
|
|
2642
|
+
*
|
|
2643
|
+
* `native` is deliberately NOT indexed here. It cannot be attributed to a
|
|
2644
|
+
* language with this data — 84% of city native names equal SEVERAL locales'
|
|
2645
|
+
* translations (Latin spellings coinciding across de/it/nl/hr/pl) while
|
|
2646
|
+
* distinctive non-Latin names like `भारत` match none — so filing `कर्नाटक`
|
|
2647
|
+
* under German would be worse than not indexing it at all.
|
|
2648
|
+
*
|
|
2649
|
+
* The 19 upstream locales are br, ko, pt-BR, pt, nl, hr, fa, de, es, fr, ja,
|
|
2650
|
+
* it, zh-CN, tr, ru, uk, pl, hi, ar. **`sv` is absent**, so a Swedish search
|
|
2651
|
+
* matches nothing. That is the accepted cost of strict scoping.
|
|
2652
|
+
*/
|
|
2653
|
+
searchByLocale: object23().optional().nullable().label("Search By Locale"),
|
|
2654
|
+
/** Epoch ms of the run that last wrote this row. Drives the stale-row prune. */
|
|
2655
|
+
syncedAt: number11().optional().nullable().label("Synced At")
|
|
2656
|
+
}).label("City");
|
|
2657
|
+
|
|
2658
|
+
// src/geo/country-filter.schema.ts
|
|
2659
|
+
import { object as object24 } from "yup";
|
|
2660
|
+
var CountryFilterShape = object24({
|
|
2661
|
+
/** ISO 3166-1 alpha-2 codes, e.g. `IN`. Uppercased, matching how they are stored. */
|
|
2662
|
+
countryCodes: codeTokens("Country Codes"),
|
|
2663
|
+
/** ISO 3166-1 alpha-3 codes, e.g. `IND`. */
|
|
2664
|
+
iso3s: codeTokens("ISO3 Codes"),
|
|
2665
|
+
/**
|
|
2666
|
+
* `tokens`, NOT `codeTokens`: a region is a display string ("Asia", "Europe"),
|
|
2667
|
+
* and uppercasing it would match nothing.
|
|
2668
|
+
*/
|
|
2669
|
+
regions: tokens("Regions"),
|
|
2670
|
+
subregions: tokens("Subregions"),
|
|
2671
|
+
/** Display names, matched exactly. Free-text matching is `?search=`. */
|
|
2672
|
+
names: tokens("Names")
|
|
2673
|
+
}).label("Country Filter");
|
|
2674
|
+
var CountryFilterSchema = rejectUnknownKeys(
|
|
2675
|
+
CountryFilterShape,
|
|
2676
|
+
() => Object.keys(CountryFilterShape.fields)
|
|
2677
|
+
);
|
|
2678
|
+
|
|
2679
|
+
// src/geo/state-filter.schema.ts
|
|
2680
|
+
import { object as object25 } from "yup";
|
|
2681
|
+
var StateFilterShape = object25({
|
|
2682
|
+
/** ISO 3166-1 alpha-2 of the parent country, e.g. `IN`. */
|
|
2683
|
+
countryCodes: codeTokens("Country Codes"),
|
|
2684
|
+
/**
|
|
2685
|
+
* BARE subdivision codes, e.g. `KA`.
|
|
2686
|
+
*
|
|
2687
|
+
* `@todo STATE-CODE-ISO-PREFIX` — not globally unique, so this facet ALONE is
|
|
2688
|
+
* an ambiguous question: `stateCodes[0]=BDS` matches Badakhshan in Afghanistan
|
|
2689
|
+
* and every other `BDS` elsewhere. Pair it with `countryCodes` to mean one
|
|
2690
|
+
* place. See `StateSchema.stateCode`.
|
|
2691
|
+
*/
|
|
2692
|
+
stateCodes: codeTokens("State Codes"),
|
|
2693
|
+
/** Display names, matched exactly. Free-text matching is `?search=`. */
|
|
2694
|
+
names: tokens("Names"),
|
|
2695
|
+
/**
|
|
2696
|
+
* Upstream's own subdivision word: "province", "state", "district", …
|
|
2697
|
+
*
|
|
2698
|
+
* `tokens`, NOT `codeTokens`: these are lowercase display strings and
|
|
2699
|
+
* uppercasing them would match nothing. An OPEN vocabulary rather than an
|
|
2700
|
+
* `enumFacet`, because upstream ships 30-odd values and adding one should not
|
|
2701
|
+
* require a schema release here.
|
|
2702
|
+
*/
|
|
2703
|
+
types: tokens("Types")
|
|
2704
|
+
}).label("State Filter");
|
|
2705
|
+
var StateFilterSchema = rejectUnknownKeys(
|
|
2706
|
+
StateFilterShape,
|
|
2707
|
+
() => Object.keys(StateFilterShape.fields)
|
|
2708
|
+
);
|
|
2709
|
+
|
|
2710
|
+
// src/geo/city-filter.schema.ts
|
|
2711
|
+
import { object as object26 } from "yup";
|
|
2712
|
+
var CityFilterShape = object26({
|
|
2713
|
+
/** ISO 3166-1 alpha-2 of the parent country, e.g. `IN`. */
|
|
2714
|
+
countryCodes: codeTokens("Country Codes"),
|
|
2715
|
+
/**
|
|
2716
|
+
* BARE subdivision codes, e.g. `KA`. Ambiguous without `countryCodes`; see
|
|
2717
|
+
* `StateFilterSchema.stateCodes` and `@todo STATE-CODE-ISO-PREFIX`.
|
|
2718
|
+
*/
|
|
2719
|
+
stateCodes: codeTokens("State Codes"),
|
|
2720
|
+
/** Display names, matched exactly. Free-text matching is `?search=`. */
|
|
2721
|
+
names: tokens("Names")
|
|
2722
|
+
}).label("City Filter");
|
|
2723
|
+
var CityFilterSchema = rejectUnknownKeys(
|
|
2724
|
+
CityFilterShape,
|
|
2725
|
+
() => Object.keys(CityFilterShape.fields)
|
|
2726
|
+
);
|
|
2727
|
+
|
|
2728
|
+
// src/geo/geo-sync.schema.ts
|
|
2729
|
+
import { boolean as boolean14, mixed as mixed4, number as number12, object as object27, string as string20 } from "yup";
|
|
2730
|
+
var GeoSyncCountsSchema = object27({
|
|
2731
|
+
read: number12().integer().min(0).default(0).label("Read"),
|
|
2732
|
+
upserted: number12().integer().min(0).default(0).label("Upserted"),
|
|
2733
|
+
modified: number12().integer().min(0).default(0).label("Modified"),
|
|
2734
|
+
matched: number12().integer().min(0).default(0).label("Matched"),
|
|
2735
|
+
/** Rows removed because upstream dropped them. See `prunedSkipped`. */
|
|
2736
|
+
pruned: number12().integer().min(0).default(0).label("Pruned"),
|
|
2737
|
+
/**
|
|
2738
|
+
* True when the stale-row prune was REFUSED because it would have removed more
|
|
2739
|
+
* than its safety threshold of the collection. A partial fetch that looked
|
|
2740
|
+
* like a success must not be able to empty the table, so the run reports the
|
|
2741
|
+
* refusal rather than acting on it.
|
|
2742
|
+
*/
|
|
2743
|
+
prunedSkipped: boolean14().default(false).label("Prune Skipped")
|
|
2744
|
+
}).label("Sync Counts");
|
|
2745
|
+
var GeoSyncRunSchema = object27({
|
|
2746
|
+
jobId: string20().required().label("Job ID"),
|
|
2747
|
+
status: mixed4().oneOf(SupportedGeoSyncStatuses).required().label("Status"),
|
|
2748
|
+
trigger: mixed4().oneOf(SupportedGeoSyncTriggers).optional().label("Trigger"),
|
|
2749
|
+
/** Whether the caller asked to run even though the release tag was unchanged. */
|
|
2750
|
+
force: boolean14().default(false).label("Force"),
|
|
2751
|
+
/**
|
|
2752
|
+
* The upstream release tag this run fetched, e.g. `v3.2-export.7`.
|
|
2753
|
+
*
|
|
2754
|
+
* Null only when the tag could not be read AND `force` was set — a run that
|
|
2755
|
+
* cannot tell what version it is fetching is otherwise a failure, not a
|
|
2756
|
+
* refresh, so it is not allowed to proceed unattended.
|
|
2757
|
+
*/
|
|
2758
|
+
releaseTag: string20().optional().nullable().label("Release Tag"),
|
|
2759
|
+
/** The tag of the last successful run, i.e. what this one is replacing. */
|
|
2760
|
+
previousReleaseTag: string20().optional().nullable().label("Previous Release Tag"),
|
|
2761
|
+
startedAt: number12().required().label("Started At"),
|
|
2762
|
+
finishedAt: number12().optional().nullable().label("Finished At"),
|
|
2763
|
+
durationMs: number12().optional().nullable().label("Duration (ms)"),
|
|
2764
|
+
phase: mixed4().oneOf(SupportedGeoSyncPhases).optional().nullable().label("Phase"),
|
|
2765
|
+
/**
|
|
2766
|
+
* Live progress for the dashboard bar.
|
|
2767
|
+
*
|
|
2768
|
+
* `total` is an ESTIMATE — the previous successful run's count for the same
|
|
2769
|
+
* collection, or a hardcoded fallback on the very first run, since the true
|
|
2770
|
+
* total is not known until the stream ends. The UI should render it as one.
|
|
2771
|
+
*/
|
|
2772
|
+
progress: object27({
|
|
2773
|
+
collection: string20().optional().nullable().label("Collection"),
|
|
2774
|
+
processed: number12().integer().min(0).default(0).label("Processed"),
|
|
2775
|
+
total: number12().integer().min(0).optional().nullable().label("Total")
|
|
2776
|
+
}).optional().nullable().label("Progress"),
|
|
2777
|
+
counts: object27({
|
|
2778
|
+
countries: GeoSyncCountsSchema.optional().nullable().label("Countries"),
|
|
2779
|
+
states: GeoSyncCountsSchema.optional().nullable().label("States"),
|
|
2780
|
+
cities: GeoSyncCountsSchema.optional().nullable().label("Cities")
|
|
2781
|
+
}).optional().nullable().label("Counts"),
|
|
2782
|
+
/** Why a `skipped` run was skipped, in words the dashboard can show verbatim. */
|
|
2783
|
+
reason: string20().optional().nullable().label("Reason"),
|
|
2784
|
+
/** The failure message on a `failed` run. Surfaced verbatim to the admin. */
|
|
2785
|
+
error: string20().optional().nullable().label("Error")
|
|
2786
|
+
}).label("Geo Sync Run");
|
|
2787
|
+
|
|
2788
|
+
// src/language/language-filter.schema.ts
|
|
2789
|
+
import { object as object28 } from "yup";
|
|
2790
|
+
var LanguageFilterShape = object28({
|
|
2791
|
+
/** Language names, matched exactly. Free-text matching is `?search=`. */
|
|
2792
|
+
names: tokens("Names")
|
|
2793
|
+
}).label("Language Filter");
|
|
2794
|
+
var LanguageFilterSchema = rejectUnknownKeys(
|
|
2795
|
+
LanguageFilterShape,
|
|
2796
|
+
() => Object.keys(LanguageFilterShape.fields)
|
|
2797
|
+
);
|
|
2798
|
+
|
|
2799
|
+
// src/page/page-filter.schema.ts
|
|
2800
|
+
import { boolean as boolean15, object as object29 } from "yup";
|
|
2801
|
+
var PageFilterShape = object29({
|
|
2802
|
+
/**
|
|
2803
|
+
* `type`, NOT `pageType`.
|
|
2804
|
+
*
|
|
2805
|
+
* The enum constant is named `PageType`, which makes `pageType` the obvious
|
|
2806
|
+
* guess, and it is wrong: the stored key is `type` (verified — `pageType`
|
|
2807
|
+
* exists on 0 of 1,449 documents, while `type` splits 1,198 company / 251
|
|
2808
|
+
* institute). The name of a constant is not the name of a field.
|
|
2809
|
+
*/
|
|
2810
|
+
type: enumFacet(SupportedPageTypes, "Page Type"),
|
|
2811
|
+
/**
|
|
2812
|
+
* Publication state. Worth knowing when filtering: 69 documents carry NO
|
|
2813
|
+
* `status` at all and some carry `null`, so `status: ['published']` is a real
|
|
2814
|
+
* narrowing (1,379 of 1,449) and absent means "no constraint", not "published".
|
|
2815
|
+
*/
|
|
2816
|
+
status: enumFacet(SupportedPageStatuses, "Status"),
|
|
2817
|
+
// ── Flags ───────────────────────────────────────────────────────────────────
|
|
2818
|
+
// No `.default(false)`: absent means no constraint. Note `isFeatured` is
|
|
2819
|
+
// present on only 55 documents, so combining it with `status: ['published']`
|
|
2820
|
+
// leaves 11 pages — a legitimate filter that returns almost nothing.
|
|
2821
|
+
isFeatured: boolean15().optional().label("Featured Only"),
|
|
2822
|
+
/** Lowercase (`in`, `se`, `ww`), like groups. See `GroupFilterSchema`. */
|
|
2823
|
+
featuredMarkets: tokens("Featured Markets"),
|
|
2824
|
+
/** Page SLUGS. A company is matched by slug: a display name breaks on rename. */
|
|
2825
|
+
slugs: tokens("Slugs")
|
|
2826
|
+
}).label("Page Filter");
|
|
2827
|
+
var PAGE_FILTER_KEYS = Object.keys(PageFilterShape.fields);
|
|
2828
|
+
var PageFilterSchema = rejectUnknownKeys(
|
|
2829
|
+
PageFilterShape,
|
|
2830
|
+
() => PAGE_FILTER_KEYS
|
|
1965
2831
|
);
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
SignupContextSource2["Default"] = "default";
|
|
1970
|
-
return SignupContextSource2;
|
|
1971
|
-
})(SignupContextSource || {});
|
|
1972
|
-
var SupportedSignupContextSources = Object.values(SignupContextSource);
|
|
1973
|
-
var UserDetailType = /* @__PURE__ */ ((UserDetailType2) => {
|
|
1974
|
-
UserDetailType2["Overview"] = "overview";
|
|
1975
|
-
UserDetailType2["AdditionalInfo"] = "additionalInfo";
|
|
1976
|
-
UserDetailType2["General"] = "general";
|
|
1977
|
-
UserDetailType2["SocialAccounts"] = "socialAccounts";
|
|
1978
|
-
UserDetailType2["WorkExperiences"] = "workExperiences";
|
|
1979
|
-
UserDetailType2["Educations"] = "educations";
|
|
1980
|
-
UserDetailType2["Skills"] = "skills";
|
|
1981
|
-
UserDetailType2["Languages"] = "languages";
|
|
1982
|
-
UserDetailType2["Certifications"] = "certifications";
|
|
1983
|
-
UserDetailType2["Interests"] = "interests";
|
|
1984
|
-
UserDetailType2["Projects"] = "projects";
|
|
1985
|
-
return UserDetailType2;
|
|
1986
|
-
})(UserDetailType || {});
|
|
1987
|
-
var ProficiencyLevel = /* @__PURE__ */ ((ProficiencyLevel2) => {
|
|
1988
|
-
ProficiencyLevel2["BasicAwareness"] = "basic_awareness";
|
|
1989
|
-
ProficiencyLevel2["Beginner"] = "beginner";
|
|
1990
|
-
ProficiencyLevel2["Intermediate"] = "intermediate";
|
|
1991
|
-
ProficiencyLevel2["Advanced"] = "advanced";
|
|
1992
|
-
ProficiencyLevel2["Expert"] = "expert";
|
|
1993
|
-
return ProficiencyLevel2;
|
|
1994
|
-
})(ProficiencyLevel || {});
|
|
1995
|
-
var SupportedProficiencyLevels = Object.values(ProficiencyLevel);
|
|
1996
|
-
var JobSearchUrgency = /* @__PURE__ */ ((JobSearchUrgency2) => {
|
|
1997
|
-
JobSearchUrgency2["Asap"] = "asap";
|
|
1998
|
-
JobSearchUrgency2["WithinThreeMonths"] = "within_3_months";
|
|
1999
|
-
JobSearchUrgency2["WithinSixMonths"] = "within_6_months";
|
|
2000
|
-
JobSearchUrgency2["PassivelyBrowsing"] = "passively_browsing";
|
|
2001
|
-
return JobSearchUrgency2;
|
|
2002
|
-
})(JobSearchUrgency || {});
|
|
2003
|
-
var SupportedJobSearchUrgencies = Object.values(JobSearchUrgency);
|
|
2004
|
-
var ReferralSource = /* @__PURE__ */ ((ReferralSource2) => {
|
|
2005
|
-
ReferralSource2["Recruiter"] = "recruiter";
|
|
2006
|
-
ReferralSource2["LinkedIn"] = "linkedin";
|
|
2007
|
-
ReferralSource2["TikTokInstagram"] = "tiktok_instagram";
|
|
2008
|
-
ReferralSource2["Google"] = "google";
|
|
2009
|
-
ReferralSource2["Reddit"] = "reddit";
|
|
2010
|
-
ReferralSource2["ChatGptGemini"] = "chatgpt_gemini";
|
|
2011
|
-
ReferralSource2["Friend"] = "friend";
|
|
2012
|
-
ReferralSource2["Other"] = "other";
|
|
2013
|
-
return ReferralSource2;
|
|
2014
|
-
})(ReferralSource || {});
|
|
2015
|
-
var SupportedReferralSources = Object.values(ReferralSource);
|
|
2016
|
-
var JobAlertFrequency = /* @__PURE__ */ ((JobAlertFrequency2) => {
|
|
2017
|
-
JobAlertFrequency2["Off"] = "off";
|
|
2018
|
-
JobAlertFrequency2["Daily"] = "daily";
|
|
2019
|
-
JobAlertFrequency2["Weekly"] = "weekly";
|
|
2020
|
-
return JobAlertFrequency2;
|
|
2021
|
-
})(JobAlertFrequency || {});
|
|
2022
|
-
var SupportedJobAlertFrequencies = Object.values(JobAlertFrequency);
|
|
2023
|
-
var NotificationChannel = /* @__PURE__ */ ((NotificationChannel2) => {
|
|
2024
|
-
NotificationChannel2["GroupActivity"] = "groupActivity";
|
|
2025
|
-
NotificationChannel2["JobActivity"] = "jobActivity";
|
|
2026
|
-
NotificationChannel2["ApplicationUpdates"] = "applicationUpdates";
|
|
2027
|
-
NotificationChannel2["ProductUpdates"] = "productUpdates";
|
|
2028
|
-
return NotificationChannel2;
|
|
2029
|
-
})(NotificationChannel || {});
|
|
2030
|
-
var SupportedNotificationChannels = Object.values(NotificationChannel);
|
|
2031
|
-
var SupportedSalaryCurrencies = ["USD", "EUR", "GBP", "SEK", "INR"];
|
|
2032
|
-
var MIN_SALARY_LOWER_BOUND = 0;
|
|
2033
|
-
var MIN_SALARY_UPPER_BOUND = 1e6;
|
|
2034
|
-
var PREFILL_MIRRORED_KEYS = [
|
|
2035
|
-
"headline",
|
|
2036
|
-
"aboutMe",
|
|
2037
|
-
"mobile",
|
|
2038
|
-
"location",
|
|
2039
|
-
"experienceLevel",
|
|
2040
|
-
"socialAccounts",
|
|
2041
|
-
"workExperiences",
|
|
2042
|
-
"educations"
|
|
2043
|
-
];
|
|
2044
|
-
var PRIVATE_PROFILE_FIELDS = [
|
|
2045
|
-
"email",
|
|
2046
|
-
"emailVerified",
|
|
2047
|
-
"mobile",
|
|
2048
|
-
"additionalInfo"
|
|
2049
|
-
];
|
|
2832
|
+
|
|
2833
|
+
// src/marketing/marketing.schema.ts
|
|
2834
|
+
import { array as array16, boolean as boolean17, lazy as lazy2, mixed as mixed5, number as number14, object as object31, string as string22 } from "yup";
|
|
2050
2835
|
|
|
2051
2836
|
// src/marketing/marketing.constant.ts
|
|
2052
2837
|
var CampaignStatus = /* @__PURE__ */ ((CampaignStatus2) => {
|
|
2053
2838
|
CampaignStatus2["Draft"] = "draft";
|
|
2054
|
-
CampaignStatus2["
|
|
2055
|
-
CampaignStatus2["
|
|
2839
|
+
CampaignStatus2["Running"] = "running";
|
|
2840
|
+
CampaignStatus2["Paused"] = "paused";
|
|
2841
|
+
CampaignStatus2["Stopped"] = "stopped";
|
|
2842
|
+
CampaignStatus2["Completed"] = "completed";
|
|
2056
2843
|
CampaignStatus2["Failed"] = "failed";
|
|
2057
2844
|
return CampaignStatus2;
|
|
2058
2845
|
})(CampaignStatus || {});
|
|
2059
2846
|
var SupportedCampaignStatuses = Object.values(CampaignStatus);
|
|
2847
|
+
var TerminalCampaignStatuses = [
|
|
2848
|
+
"stopped" /* Stopped */,
|
|
2849
|
+
"completed" /* Completed */,
|
|
2850
|
+
"failed" /* Failed */
|
|
2851
|
+
];
|
|
2852
|
+
var CampaignStatusTransitions = {
|
|
2853
|
+
["draft" /* Draft */]: ["running" /* Running */],
|
|
2854
|
+
["running" /* Running */]: [
|
|
2855
|
+
"paused" /* Paused */,
|
|
2856
|
+
"stopped" /* Stopped */,
|
|
2857
|
+
"completed" /* Completed */,
|
|
2858
|
+
"failed" /* Failed */
|
|
2859
|
+
],
|
|
2860
|
+
["paused" /* Paused */]: ["running" /* Running */, "stopped" /* Stopped */],
|
|
2861
|
+
["stopped" /* Stopped */]: [],
|
|
2862
|
+
["completed" /* Completed */]: [],
|
|
2863
|
+
["failed" /* Failed */]: []
|
|
2864
|
+
};
|
|
2865
|
+
function canTransitionCampaign(from, to) {
|
|
2866
|
+
return CampaignStatusTransitions[from]?.includes(to) ?? false;
|
|
2867
|
+
}
|
|
2060
2868
|
var CampaignTriggerKind = /* @__PURE__ */ ((CampaignTriggerKind2) => {
|
|
2061
2869
|
CampaignTriggerKind2["Manual"] = "manual";
|
|
2062
2870
|
CampaignTriggerKind2["Once"] = "once";
|
|
2063
2871
|
CampaignTriggerKind2["Schedule"] = "schedule";
|
|
2064
|
-
CampaignTriggerKind2["Lifecycle"] = "lifecycle";
|
|
2065
2872
|
return CampaignTriggerKind2;
|
|
2066
2873
|
})(CampaignTriggerKind || {});
|
|
2067
2874
|
var SupportedCampaignTriggerKinds = Object.values(CampaignTriggerKind);
|
|
@@ -2079,21 +2886,16 @@ var CampaignEndKind = /* @__PURE__ */ ((CampaignEndKind2) => {
|
|
|
2079
2886
|
return CampaignEndKind2;
|
|
2080
2887
|
})(CampaignEndKind || {});
|
|
2081
2888
|
var SupportedCampaignEndKinds = Object.values(CampaignEndKind);
|
|
2082
|
-
var
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
var
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
BindingKind2["Url"] = "url";
|
|
2093
|
-
BindingKind2["Source"] = "source";
|
|
2094
|
-
return BindingKind2;
|
|
2095
|
-
})(BindingKind || {});
|
|
2096
|
-
var SupportedBindingKinds = Object.values(BindingKind);
|
|
2889
|
+
var TemplateVariableBindingKind = /* @__PURE__ */ ((TemplateVariableBindingKind2) => {
|
|
2890
|
+
TemplateVariableBindingKind2["Static"] = "static";
|
|
2891
|
+
TemplateVariableBindingKind2["Recipient"] = "recipient";
|
|
2892
|
+
TemplateVariableBindingKind2["Url"] = "url";
|
|
2893
|
+
TemplateVariableBindingKind2["Source"] = "source";
|
|
2894
|
+
return TemplateVariableBindingKind2;
|
|
2895
|
+
})(TemplateVariableBindingKind || {});
|
|
2896
|
+
var SupportedTemplateVariableBindingKinds = Object.values(
|
|
2897
|
+
TemplateVariableBindingKind
|
|
2898
|
+
);
|
|
2097
2899
|
var OccurrenceStatus = /* @__PURE__ */ ((OccurrenceStatus2) => {
|
|
2098
2900
|
OccurrenceStatus2["Pending"] = "pending";
|
|
2099
2901
|
OccurrenceStatus2["Running"] = "running";
|
|
@@ -2105,24 +2907,50 @@ var OccurrenceStatus = /* @__PURE__ */ ((OccurrenceStatus2) => {
|
|
|
2105
2907
|
var SupportedOccurrenceStatuses = Object.values(OccurrenceStatus);
|
|
2106
2908
|
var CAMPAIGN_MAX_RECIPIENTS_CEILING = 5e3;
|
|
2107
2909
|
|
|
2108
|
-
// src/
|
|
2109
|
-
|
|
2110
|
-
var
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2910
|
+
// src/user/user-filter.schema.ts
|
|
2911
|
+
import { array as array15, boolean as boolean16, number as number13, object as object30, string as string21 } from "yup";
|
|
2912
|
+
var UserFilterBranchShape = object30({
|
|
2913
|
+
/**
|
|
2914
|
+
* Exact email addresses. An IDENTITY lookup, not a search.
|
|
2915
|
+
*
|
|
2916
|
+
* The admin `search` box already matches email, but as a case-insensitive
|
|
2917
|
+
* unanchored regex over email + first + last, which no index can seek: it
|
|
2918
|
+
* scans every key. Measured against 103,680 users, finding one person by
|
|
2919
|
+
* their full address examined all 103,680 documents and took 552ms, where the
|
|
2920
|
+
* same lookup as an exact match on the indexed `email` field is 0ms and
|
|
2921
|
+
* examines 1.
|
|
2922
|
+
*
|
|
2923
|
+
* So this is for when the caller ALREADY KNOWS the address (previewing what
|
|
2924
|
+
* one named recipient will receive, reconciling a bounce) and `search` is for
|
|
2925
|
+
* when a human is typing a fragment.
|
|
2926
|
+
*
|
|
2927
|
+
* Values are LOWER-CASED before matching, not matched case-insensitively: all
|
|
2928
|
+
* 103,680 stored addresses are already lower case (verified), so normalising
|
|
2929
|
+
* the input keeps the match an indexed equality instead of a regex that would
|
|
2930
|
+
* put the scan straight back.
|
|
2931
|
+
*/
|
|
2932
|
+
emails: array15().of(string21().required()).optional().default([]).label("Email Addresses"),
|
|
2933
|
+
/**
|
|
2934
|
+
* Better Auth account ids. An IDENTITY lookup like `emails`, and for the same
|
|
2935
|
+
* reason: the caller already knows the exact value.
|
|
2936
|
+
*
|
|
2937
|
+
* BACKEND-ONLY, despite living beside the human-facing facets. The consumer
|
|
2938
|
+
* is hono-auth, which holds an account id and needs the profile's `shortId`
|
|
2939
|
+
* to sign into the JWT it is minting. It cannot use `GET /me` (it is the one
|
|
2940
|
+
* issuing the token) and cannot use `GET /users/:shortId` (the shortId is
|
|
2941
|
+
* precisely what it lacks), so the list route with a facet is the lookup.
|
|
2942
|
+
*
|
|
2943
|
+
* Matched as an indexed equality on `authAccountId`, which is already indexed
|
|
2944
|
+
* as the mirror's link key. No case normalisation, unlike `emails`: these are
|
|
2945
|
+
* opaque generated ids, stored exactly as Better Auth issued them, so folding
|
|
2946
|
+
* case could only ever turn a hit into a miss.
|
|
2947
|
+
*
|
|
2948
|
+
* The admin filter panel builds its controls explicitly rather than from this
|
|
2949
|
+
* shape, so declaring it here does not put an account-id box in the UI.
|
|
2950
|
+
*/
|
|
2951
|
+
authAccountIds: array15().of(string21().required()).optional().default([]).label("Auth Account IDs"),
|
|
2124
2952
|
// ── Enum facets (closed vocabularies, chip pickers) ─────────────────────────
|
|
2125
|
-
countries:
|
|
2953
|
+
countries: array15().of(string21().required()).optional().default([]).label("Countries"),
|
|
2126
2954
|
roles: enumFacet(SupportedUserRoles, "Roles"),
|
|
2127
2955
|
// The user's own current seniority (UserSchema.experienceLevel).
|
|
2128
2956
|
experienceLevels: enumFacet(SupportedExperienceLevels, "Experience Levels"),
|
|
@@ -2138,40 +2966,112 @@ var ListFilterBranchSchema = object18({
|
|
|
2138
2966
|
SupportedUserProfileVisibilities,
|
|
2139
2967
|
"Profile Visibility"
|
|
2140
2968
|
),
|
|
2141
|
-
// Proficiency held on any skill entry (skills[].proficiencyLevel).
|
|
2142
|
-
skillProficiencyLevels: enumFacet(
|
|
2143
|
-
SupportedProficiencyLevels,
|
|
2144
|
-
"Skill Proficiency"
|
|
2145
|
-
),
|
|
2146
|
-
// Proficiency held on any language entry (languages[].proficiencyLevel).
|
|
2147
|
-
languageProficiencyLevels: enumFacet(
|
|
2148
|
-
SupportedProficiencyLevels,
|
|
2149
|
-
"Language Proficiency"
|
|
2150
|
-
),
|
|
2151
2969
|
// Mode of any education entry (educations[].studyType).
|
|
2970
|
+
//
|
|
2971
|
+
// Left as a STANDALONE enum, unlike skill/language proficiency above: it is a
|
|
2972
|
+
// property of a person's education in general ("studied full time"), and the
|
|
2973
|
+
// measured divergence against pairing it with `courses` was 1 user out of
|
|
2974
|
+
// 3,465. Pairing is for qualifiers that are MEANINGLESS alone; this one is
|
|
2975
|
+
// not, so it stays cheap to use.
|
|
2152
2976
|
studyTypes: enumFacet(SupportedStudyTypes, "Study Type"),
|
|
2153
2977
|
// Platforms the user linked a social account for (socialAccounts[].type).
|
|
2154
2978
|
socialAccountTypes: enumFacet(SupportedSocialAccounts, "Social Accounts"),
|
|
2155
2979
|
// Currency of the user's salary floor (minSalaryCurrency).
|
|
2156
2980
|
salaryCurrencies: enumFacet(SupportedSalaryCurrencies, "Salary Currency"),
|
|
2157
2981
|
// ── Free-text token facets (nested profile arrays) ──────────────────────────
|
|
2158
|
-
|
|
2159
|
-
|
|
2982
|
+
// Skill and language each carry their OWN proficiency, because a level is
|
|
2983
|
+
// only meaningful attached to the thing it qualifies. See `qualified`:
|
|
2984
|
+
// as two independent facets, "python + expert" returned 301 users where only
|
|
2985
|
+
// 103 were actually expert at Python.
|
|
2986
|
+
//
|
|
2987
|
+
// `[{ name: 'python', proficiencyLevel: 'expert' }]` — the level is optional,
|
|
2988
|
+
// so naming a skill without one still filters on the skill alone.
|
|
2989
|
+
skills: qualified(SupportedProficiencyLevels, "proficiencyLevel", "Skills"),
|
|
2990
|
+
languages: qualified(SupportedProficiencyLevels, "proficiencyLevel", "Languages"),
|
|
2160
2991
|
interests: tokens("Interests"),
|
|
2161
|
-
|
|
2992
|
+
// LOOSE forms, kept deliberately. Each of these is a real question on its own
|
|
2993
|
+
// ("worked at Google", "was an engineer somewhere"), and asking for two of
|
|
2994
|
+
// them usually does mean "has each of these somewhere in their history".
|
|
2995
|
+
// They AND across the whole array, so different entries may satisfy each.
|
|
2996
|
+
//
|
|
2997
|
+
// `companyName` / `instituteName`, NOT the `company` widget the job filter
|
|
2998
|
+
// uses. Both are PICKED from real pages — a profile stores the whole
|
|
2999
|
+
// `PageSchema.pick(["name","slug","type","logo"])` object, not free text — but
|
|
3000
|
+
// the facet matches `workExperiences.company.name`, so the value to store is
|
|
3001
|
+
// the page's NAME. Storing its slug would be a picker that matches nothing.
|
|
3002
|
+
companies: tokens("Companies").meta({ widget: "companyName" }),
|
|
2162
3003
|
designations: tokens("Job Titles / Designations"),
|
|
2163
|
-
institutes: tokens("Institutes"),
|
|
3004
|
+
institutes: tokens("Institutes").meta({ widget: "instituteName" }),
|
|
2164
3005
|
courses: tokens("Courses"),
|
|
2165
3006
|
fieldsOfStudy: tokens("Fields of Study"),
|
|
2166
|
-
|
|
2167
|
-
|
|
3007
|
+
// PRECISE forms: all parts must describe ONE entry.
|
|
3008
|
+
//
|
|
3009
|
+
// The narrower question the loose facets above cannot express. "Engineer AT
|
|
3010
|
+
// Google" as `companies=Google & designations=engineer` also matches someone
|
|
3011
|
+
// who was an engineer at Acme and a designer at Google, because Mongo
|
|
3012
|
+
// satisfies each path from a different array entry. Measured on production:
|
|
3013
|
+
// VIT + B.Tech returned 71 users where 64 had one education entry that was
|
|
3014
|
+
// both, and Infosys + Test Engineer returned 7 where 6 actually matched.
|
|
3015
|
+
//
|
|
3016
|
+
// Additive rather than a replacement: both readings are legitimate, so the
|
|
3017
|
+
// vocabulary carries both and the caller says which it means. 1,573 users
|
|
3018
|
+
// have two or more work entries and 1,761 have two or more educations, so the
|
|
3019
|
+
// distinction is real and grows as profiles fill in.
|
|
3020
|
+
//
|
|
3021
|
+
// The fields mirror what the profile form actually collects for one entry
|
|
3022
|
+
// (`WorkExperienceSchema` / `EducationSchema`), minus the parts that cannot
|
|
3023
|
+
// narrow an audience: `description` is prose, and a per-entry `location`
|
|
3024
|
+
// duplicates the top-level geography facets against a much emptier path.
|
|
3025
|
+
//
|
|
3026
|
+
// `isCurrent` reads `duration.isActive`, which is what the profile form calls
|
|
3027
|
+
// "I currently work here". It is the field that makes this facet worth having
|
|
3028
|
+
// per entry: "engineer at Google" and "engineer at Google NOW" are different
|
|
3029
|
+
// audiences, and only the second is reachable without it. Live counts:
|
|
3030
|
+
// 2,190 users have a current job against 4,527 with a past one, 1,456 have a
|
|
3031
|
+
// remote entry, 4,088 are still studying.
|
|
3032
|
+
workHistory: conjunction(
|
|
3033
|
+
[
|
|
3034
|
+
"company",
|
|
3035
|
+
"designation",
|
|
3036
|
+
{ name: "isCurrent", type: "boolean" },
|
|
3037
|
+
{ name: "isRemote", type: "boolean" }
|
|
3038
|
+
],
|
|
3039
|
+
"Work History"
|
|
3040
|
+
),
|
|
3041
|
+
educationHistory: conjunction(
|
|
3042
|
+
[
|
|
3043
|
+
"institute",
|
|
3044
|
+
"course",
|
|
3045
|
+
"fieldOfStudy",
|
|
3046
|
+
{ name: "studyType", type: "string", oneOf: SupportedStudyTypes },
|
|
3047
|
+
{ name: "isCurrent", type: "boolean" },
|
|
3048
|
+
{ name: "isDistanceLearning", type: "boolean" }
|
|
3049
|
+
],
|
|
3050
|
+
"Education History"
|
|
3051
|
+
),
|
|
3052
|
+
// The issuing body belongs to the certificate, not to the person: "AWS SA"
|
|
3053
|
+
// plus "Microsoft" as two facets matches someone holding an AWS cert AND any
|
|
3054
|
+
// unrelated Microsoft one. 38% of users with a certification hold two or more,
|
|
3055
|
+
// so the entries are there to diverge.
|
|
3056
|
+
//
|
|
3057
|
+
// `authority` is free text rather than a closed vocabulary, hence `"open"`.
|
|
3058
|
+
certifications: qualified("open", "authority", "Certifications"),
|
|
2168
3059
|
projects: tokens("Projects"),
|
|
2169
3060
|
jobRoles: tokens("Target Roles"),
|
|
2170
3061
|
jobLocations: tokens("Preferred Job Locations"),
|
|
2171
3062
|
// Companies a recruiter is hiring for (recruiterProfile.hiringFor[].page.name).
|
|
2172
|
-
|
|
3063
|
+
// A picked page (`recruiterProfile.hiringFor.page`), matched on `.name`.
|
|
3064
|
+
//
|
|
3065
|
+
// `pageName`, which searches BOTH page types: `RecruiterPageLinkSchema` picks
|
|
3066
|
+
// `PageSchema` without constraining `type`, so a recruiter may be hiring for
|
|
3067
|
+
// an institute as legitimately as for a company. Production carries exactly
|
|
3068
|
+
// that on the coordinator side — a `coordinatesAt` entry holding a company —
|
|
3069
|
+
// so scoping either of these to one type would hide real data.
|
|
3070
|
+
hiringForCompanies: tokens("Hiring For").meta({ widget: "pageName" }),
|
|
2173
3071
|
// Institutes a coordinator works at (coordinatorProfile.coordinatesAt[].page.name).
|
|
2174
|
-
|
|
3072
|
+
// Same: a picked page (`coordinatorProfile.coordinatesAt.page`), on `.name`,
|
|
3073
|
+
// and likewise unconstrained in `type`.
|
|
3074
|
+
coordinatesAtInstitutes: tokens("Coordinates At").meta({ widget: "pageName" }),
|
|
2175
3075
|
// Case-insensitive keyword match against headline + aboutMe.
|
|
2176
3076
|
keywords: tokens("Profile Keywords"),
|
|
2177
3077
|
// ── Range / boolean facets ──────────────────────────────────────────────────
|
|
@@ -2179,14 +3079,16 @@ var ListFilterBranchSchema = object18({
|
|
|
2179
3079
|
// integer and 50000 SEK is not 50000 USD, so an un-currencied range matches
|
|
2180
3080
|
// across currencies and quietly means nothing. Requiring exactly one currency
|
|
2181
3081
|
// alongside a bound is what makes the comparison sound.
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
3082
|
+
// The sibling currency facet is named explicitly: this bound is inert without
|
|
3083
|
+
// it, so the dependency is stated rather than assumed.
|
|
3084
|
+
minSalaryFloor: currencyBound("Min Salary \u2265", "salaryCurrencies"),
|
|
3085
|
+
minSalaryCeil: currencyBound("Min Salary \u2264", "salaryCurrencies"),
|
|
3086
|
+
openToWorkOnly: boolean16().optional().default(false).label("Open to Work Only"),
|
|
3087
|
+
remoteExperienceOnly: boolean16().optional().default(false).label("Has Remote Experience"),
|
|
3088
|
+
hasResumeOnly: boolean16().optional().default(false).label("Has Resume"),
|
|
3089
|
+
onboardingCompletedOnly: boolean16().optional().default(false).label("Onboarding Completed"),
|
|
3090
|
+
emailVerifiedOnly: boolean16().optional().default(false).label("Email Verified Only"),
|
|
3091
|
+
mobileVerifiedOnly: boolean16().optional().default(false).label("Mobile Verified Only"),
|
|
2190
3092
|
// ── Lifecycle windows ───────────────────────────────────────────────────────
|
|
2191
3093
|
// Relative, because a saved list is resolved fresh on every send: a stored
|
|
2192
3094
|
// absolute epoch would freeze at authoring time and the audience would decay
|
|
@@ -2214,10 +3116,10 @@ var ListFilterBranchSchema = object18({
|
|
|
2214
3116
|
// Radius search around a point, against location.geo. All three required
|
|
2215
3117
|
// together; validated as a group rather than field-by-field so a half-filled
|
|
2216
3118
|
// radius cannot silently match everyone.
|
|
2217
|
-
withinRadius:
|
|
2218
|
-
lat:
|
|
2219
|
-
lng:
|
|
2220
|
-
km:
|
|
3119
|
+
withinRadius: object30({
|
|
3120
|
+
lat: number13().min(-90).max(90).required().label("Latitude"),
|
|
3121
|
+
lng: number13().min(-180).max(180).required().label("Longitude"),
|
|
3122
|
+
km: number13().moreThan(0).max(2e4).required().label("Radius (km)")
|
|
2221
3123
|
}).optional().default(void 0).label("Within Radius"),
|
|
2222
3124
|
// ── Signup context (immutable market / locale) ──────────────────────────────
|
|
2223
3125
|
signupCountries: tokens("Signup Countries"),
|
|
@@ -2228,14 +3130,27 @@ var ListFilterBranchSchema = object18({
|
|
|
2228
3130
|
// match — filtering on this necessarily excludes them.
|
|
2229
3131
|
educationLevels: enumFacet(SupportedEducationLevels, "Education Level"),
|
|
2230
3132
|
// ── Consent metadata ────────────────────────────────────────────────────────
|
|
2231
|
-
// `
|
|
2232
|
-
// applied unconditionally by user-service
|
|
3133
|
+
// `marketingSubscribed` IS a facet, and deliberately so. It used to be a hard
|
|
3134
|
+
// opt-in gate applied unconditionally by user-service; that gate was removed
|
|
3135
|
+
// when user-service became a pure data source, so consent is now an ordinary
|
|
3136
|
+
// filter that marketing-service defaults to `true` in `resolveAudience`.
|
|
3137
|
+
//
|
|
3138
|
+
// NO `.default()` HERE, unlike every other boolean facet. The send spreads its
|
|
3139
|
+
// default first and lets a stored value win, so this must distinguish "the
|
|
3140
|
+
// list said nothing" (absent -> default applies) from "the list said false"
|
|
3141
|
+
// (override -> mails users who never opted in). A `.default(false)` would
|
|
3142
|
+
// materialise the second case on every list that never chose it.
|
|
3143
|
+
//
|
|
3144
|
+
// @todo CONSENT-FILTER-ENFORCEMENT — nothing enforces consent anywhere now.
|
|
3145
|
+
marketingSubscribed: boolean16().optional().label("Marketing Subscribed"),
|
|
2233
3146
|
jobAlertFrequencies: enumFacet(
|
|
2234
3147
|
SupportedJobAlertFrequencies,
|
|
2235
3148
|
"Job Alert Frequency"
|
|
2236
3149
|
),
|
|
2237
3150
|
consentSources: tokens("Consent Source"),
|
|
2238
3151
|
consentUpdatedWithinDays: days("Consent Updated Within (days)"),
|
|
3152
|
+
/** The absolute bound `consentUpdatedWithinDays` resolves into (epoch ms). */
|
|
3153
|
+
consentUpdatedAfter: epoch("Consent Updated After"),
|
|
2239
3154
|
// ── Notification preferences ────────────────────────────────────────────────
|
|
2240
3155
|
// Read as opt-IN (`=== true`), matching marketingConsent and NOT the `!== false`
|
|
2241
3156
|
// reading used for transactional notification routing. A campaign is bulk mail,
|
|
@@ -2243,19 +3158,31 @@ var ListFilterBranchSchema = object18({
|
|
|
2243
3158
|
// `productUpdates` defaults to false on the user document, so targeting it
|
|
2244
3159
|
// selects only people who deliberately turned it on.
|
|
2245
3160
|
notificationChannelsEnabled: enumFacet(
|
|
2246
|
-
|
|
3161
|
+
SupportedNotificationTypes,
|
|
2247
3162
|
"Notification Channels Enabled"
|
|
2248
3163
|
),
|
|
2249
3164
|
// ── Tenure / recency ────────────────────────────────────────────────────────
|
|
2250
3165
|
// Against workExperiences[].duration and educations[].duration ("YYYY-MM").
|
|
2251
|
-
currentlyEmployed:
|
|
3166
|
+
currentlyEmployed: boolean16().optional().default(false).label("Currently Employed"),
|
|
2252
3167
|
graduatedWithinMonths: months("Graduated Within (months)"),
|
|
2253
3168
|
skillUsedWithinMonths: months("Skill Used Within (months)"),
|
|
3169
|
+
// The ABSOLUTE bounds the three relative windows above resolve into
|
|
3170
|
+
// (`graduatedWithinMonths` -> `graduatedAfter`, and so on). Both forms are
|
|
3171
|
+
// accepted: a caller may send either, and user-service derives the bound when
|
|
3172
|
+
// only the window is given.
|
|
3173
|
+
//
|
|
3174
|
+
// A UI should offer the WINDOW, not these. A saved list is re-resolved on
|
|
3175
|
+
// every send, so a stored absolute date rots — "graduated after 2024-03" keeps
|
|
3176
|
+
// meaning that forever, while "within 18 months" keeps meaning what the admin
|
|
3177
|
+
// intended. They are declared because the vocabulary must be complete, not
|
|
3178
|
+
// because a list should store one.
|
|
3179
|
+
graduatedAfter: string21().trim().optional().label("Graduated After"),
|
|
3180
|
+
skillUsedAfter: string21().trim().optional().label("Skill Used After"),
|
|
2254
3181
|
// ── Profile richness ────────────────────────────────────────────────────────
|
|
2255
|
-
hasImage:
|
|
2256
|
-
hasHeadline:
|
|
2257
|
-
hasAboutMe:
|
|
2258
|
-
hasMobile:
|
|
3182
|
+
hasImage: boolean16().optional().default(false).label("Has Photo"),
|
|
3183
|
+
hasHeadline: boolean16().optional().default(false).label("Has Headline"),
|
|
3184
|
+
hasAboutMe: boolean16().optional().default(false).label("Has About Me"),
|
|
3185
|
+
hasMobile: boolean16().optional().default(false).label("Has Mobile Number"),
|
|
2259
3186
|
// Segment by WHICH profile section is missing (profileCompleteness.missing[]),
|
|
2260
3187
|
// rather than only by the overall percentage.
|
|
2261
3188
|
missingSections: tokens("Missing Profile Sections"),
|
|
@@ -2267,50 +3194,39 @@ var ListFilterBranchSchema = object18({
|
|
|
2267
3194
|
// data lives in other services' collections. Tri-state: `false` is a real
|
|
2268
3195
|
// filter ("has NO job alert"), which is the audience an alert nudge wants, so
|
|
2269
3196
|
// these are `undefined` when unset rather than defaulting to false.
|
|
2270
|
-
hasJobAlert:
|
|
2271
|
-
hasApplied:
|
|
2272
|
-
appliedWithinDays: days("Applied Within (days)")
|
|
2273
|
-
//
|
|
2274
|
-
|
|
3197
|
+
hasJobAlert: boolean16().optional().default(void 0).label("Has a Job Alert"),
|
|
3198
|
+
hasApplied: boolean16().optional().default(void 0).label("Has Applied"),
|
|
3199
|
+
appliedWithinDays: days("Applied Within (days)")
|
|
3200
|
+
// `search` is NOT here. It is free text over name/email, not a facet: it names
|
|
3201
|
+
// no field on a user document, user-service treats it as a RESERVED PARAM
|
|
3202
|
+
// beside the filter rather than a filter key, and it is meaningless inside an
|
|
3203
|
+
// `or` branch (a union of "matches this text" with a facet clause). It travels
|
|
3204
|
+
// as a sibling of the filter — see `PreviewFilterSchema` in marketing-service —
|
|
3205
|
+
// so a saved audience cannot carry free text it would silently re-run later.
|
|
2275
3206
|
}).label("Audience Filter Branch");
|
|
2276
|
-
var
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
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"),
|
|
3207
|
+
var BRANCH_KEYS = Object.keys(UserFilterBranchShape.fields);
|
|
3208
|
+
var UserFilterBranch = rejectUnknownKeys(UserFilterBranchShape, () => BRANCH_KEYS);
|
|
3209
|
+
var UserFilterSchema = rejectUnknownKeys(
|
|
3210
|
+
UserFilterBranchShape.shape({
|
|
3211
|
+
or: array15().of(UserFilterBranch).optional().default([]).label("Or Any Of")
|
|
3212
|
+
}).label("User Filter"),
|
|
2302
3213
|
() => [...BRANCH_KEYS, "or"]
|
|
2303
3214
|
);
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
3215
|
+
|
|
3216
|
+
// src/marketing/marketing.schema.ts
|
|
3217
|
+
var MailingListFilterSchema = UserFilterSchema;
|
|
3218
|
+
var MailingListSchema = object31({
|
|
3219
|
+
name: string22().required().min(1).max(120).label("Name"),
|
|
3220
|
+
description: string22().optional().max(500).label("Description"),
|
|
3221
|
+
filter: MailingListFilterSchema.default(
|
|
3222
|
+
() => MailingListFilterSchema.getDefault()
|
|
3223
|
+
)
|
|
2308
3224
|
}).label("Mailing List");
|
|
2309
|
-
var CampaignTriggerSchema =
|
|
2310
|
-
kind:
|
|
3225
|
+
var CampaignTriggerSchema = object31({
|
|
3226
|
+
kind: string22().oneOf(SupportedCampaignTriggerKinds).required().default("manual" /* Manual */).label("Trigger"),
|
|
2311
3227
|
// IANA zone, e.g. "Asia/Kolkata". Local wall-clock times are interpreted in it,
|
|
2312
3228
|
// so occurrences land at the intended hour across DST rather than drifting.
|
|
2313
|
-
timezone:
|
|
3229
|
+
timezone: string22().optional().default("UTC").label("Timezone").when("kind", {
|
|
2314
3230
|
is: (k) => k !== "manual" /* Manual */,
|
|
2315
3231
|
then: (s) => s.required()
|
|
2316
3232
|
}),
|
|
@@ -2324,26 +3240,26 @@ var CampaignTriggerSchema = object18({
|
|
|
2324
3240
|
is: "schedule" /* Schedule */,
|
|
2325
3241
|
then: (s) => s.required()
|
|
2326
3242
|
}),
|
|
2327
|
-
interval:
|
|
2328
|
-
every:
|
|
2329
|
-
unit:
|
|
3243
|
+
interval: object31({
|
|
3244
|
+
every: number14().integer().min(1).max(365).required().label("Every"),
|
|
3245
|
+
unit: string22().oneOf(SupportedCampaignIntervalUnits).required().label("Unit")
|
|
2330
3246
|
}).optional().default(void 0).label("Repeats").when("kind", {
|
|
2331
3247
|
is: "schedule" /* Schedule */,
|
|
2332
3248
|
then: (s) => s.required()
|
|
2333
3249
|
}),
|
|
2334
3250
|
/** Local wall-clock send time, "HH:mm" in `timezone`. */
|
|
2335
|
-
timeOfDay:
|
|
3251
|
+
timeOfDay: string22().matches(/^([01]\d|2[0-3]):[0-5]\d$/, "Use HH:mm").optional().default("09:00").label("Time of Day"),
|
|
2336
3252
|
/** For weekly intervals: ISO weekdays, 1 = Monday .. 7 = Sunday. */
|
|
2337
|
-
byWeekday:
|
|
3253
|
+
byWeekday: array16().of(number14().integer().min(1).max(7).required()).optional().default([]).label("On Days"),
|
|
2338
3254
|
/**
|
|
2339
3255
|
* For monthly intervals: 1-31, or -1 meaning "last day of the month". A month
|
|
2340
3256
|
* shorter than the chosen day is clamped to its last day, so 31 in February
|
|
2341
3257
|
* fires on the 28th/29th rather than being skipped.
|
|
2342
3258
|
*/
|
|
2343
|
-
byMonthday:
|
|
2344
|
-
end:
|
|
2345
|
-
kind:
|
|
2346
|
-
occurrences:
|
|
3259
|
+
byMonthday: number14().integer().min(-1).max(31).notOneOf([0]).optional().default(void 0).label("Day of Month"),
|
|
3260
|
+
end: object31({
|
|
3261
|
+
kind: string22().oneOf(SupportedCampaignEndKinds).required().label("Ends"),
|
|
3262
|
+
occurrences: number14().integer().min(1).max(1e4).optional().default(void 0).label("After N Sends").when("kind", {
|
|
2347
3263
|
is: "afterOccurrences" /* AfterOccurrences */,
|
|
2348
3264
|
then: (s) => s.required()
|
|
2349
3265
|
}),
|
|
@@ -2351,52 +3267,80 @@ var CampaignTriggerSchema = object18({
|
|
|
2351
3267
|
is: "onDate" /* OnDate */,
|
|
2352
3268
|
then: (s) => s.required()
|
|
2353
3269
|
})
|
|
2354
|
-
}).optional().default(() => ({ kind: "never" /* Never */ })).label("End Condition")
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
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
|
-
}),
|
|
3270
|
+
}).optional().default(() => ({ kind: "never" /* Never */ })).label("End Condition")
|
|
3271
|
+
}).label("Trigger");
|
|
3272
|
+
var CampaignDataSourceSchema = object31({
|
|
3273
|
+
id: string22().required().label("Data Source"),
|
|
2365
3274
|
/**
|
|
2366
|
-
*
|
|
2367
|
-
*
|
|
2368
|
-
*
|
|
2369
|
-
*
|
|
3275
|
+
* Derive this source's filter per recipient from their profile, instead of
|
|
3276
|
+
* using the admin-entered `params` for everyone.
|
|
3277
|
+
*
|
|
3278
|
+
* A second AXIS on a datasource, not a datasource of its own: a
|
|
3279
|
+
* personalisation filter is an ordinary filter for that source, the only
|
|
3280
|
+
* difference being who wrote it. Jobs previously shipped the two modes as
|
|
3281
|
+
* two registry entries (`recent-jobs` / `matched-jobs`), which let them drift
|
|
3282
|
+
* apart down to their output names.
|
|
3283
|
+
*
|
|
3284
|
+
* Optional, and absent reads as false, so every existing campaign stays
|
|
3285
|
+
* un-personalised. Whether a source can honour it at all is declared by the
|
|
3286
|
+
* source itself (`supportsPersonalisation`), which this package cannot see.
|
|
2370
3287
|
*/
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
3288
|
+
personalised: boolean17().optional().label("Personalised"),
|
|
3289
|
+
/**
|
|
3290
|
+
* A free-form bag whose keys are defined by the chosen source's OWN
|
|
3291
|
+
* `paramsSchema`, which this package cannot see. Same `lazy` treatment as
|
|
3292
|
+
* `bindings` above, and for the same reason: a bare `object()` has no known
|
|
3293
|
+
* keys, so under `stripUnknown: true` every param is "unknown" and the bag
|
|
3294
|
+
* arrives as `{}` — a campaign configured to mail remote Python roles in
|
|
3295
|
+
* Bangalore silently saves as an unfiltered platform-wide digest.
|
|
3296
|
+
*
|
|
3297
|
+
* Values stay unvalidated here on purpose; the marketing service validates
|
|
3298
|
+
* them against the real source schema in `checkCampaignConfig`, which is the
|
|
3299
|
+
* only place that knows what a given source accepts.
|
|
3300
|
+
*/
|
|
3301
|
+
params: lazy2((value) => {
|
|
3302
|
+
const keys = value && typeof value === "object" ? Object.keys(value) : [];
|
|
3303
|
+
return object31(
|
|
3304
|
+
Object.fromEntries(keys.map((k) => [k, mixed5().optional()]))
|
|
3305
|
+
).optional().default(() => ({})).label("Parameters");
|
|
3306
|
+
})
|
|
3307
|
+
}).label("Data Source");
|
|
3308
|
+
var TemplateVariableBindingSchema = object31({
|
|
3309
|
+
kind: string22().oneOf(SupportedTemplateVariableBindingKinds).required().label("Source"),
|
|
3310
|
+
value: string22().required().min(1).max(2e3).label("Value")
|
|
3311
|
+
}).label("Template Variable Binding");
|
|
3312
|
+
var CampaignSchema = object31({
|
|
3313
|
+
name: string22().required().min(1).max(200).label("Campaign Name"),
|
|
2379
3314
|
// The template renders its own subject; an override is only needed for the
|
|
2380
3315
|
// inline-HTML path, hence not required when a template is chosen.
|
|
2381
|
-
subject:
|
|
3316
|
+
subject: string22().optional().max(255).label("Subject").when("templateKey", {
|
|
2382
3317
|
is: (v) => !v,
|
|
2383
3318
|
then: (s) => s.required().min(1)
|
|
2384
3319
|
}),
|
|
2385
3320
|
/**
|
|
2386
3321
|
* Inline HTML body; may contain {{unsubscribe_url}}. Optional now that a
|
|
2387
3322
|
* campaign can instead name a registry template — exactly one of the two is
|
|
2388
|
-
* required. Scheduled
|
|
2389
|
-
*
|
|
3323
|
+
* required. Scheduled campaigns must use a template: the registry path is the
|
|
3324
|
+
* one with a compliance-checked footer and a render test,
|
|
2390
3325
|
* and hand-pasted HTML that repeats forever is a mistake that repeats forever.
|
|
2391
3326
|
*/
|
|
2392
|
-
html:
|
|
3327
|
+
html: string22().optional().label("HTML Body").when("templateKey", {
|
|
2393
3328
|
is: (v) => !v,
|
|
2394
3329
|
then: (s) => s.required().min(1)
|
|
2395
3330
|
}),
|
|
2396
|
-
text:
|
|
2397
|
-
listId:
|
|
3331
|
+
text: string22().optional().label("Plain-text Body"),
|
|
3332
|
+
listId: string22().required().label("Mailing List"),
|
|
3333
|
+
/**
|
|
3334
|
+
* Where the campaign is in its life. See `CampaignStatus` for the transitions.
|
|
3335
|
+
*
|
|
3336
|
+
* Declared here so the enum is ENFORCED on write. It was previously written
|
|
3337
|
+
* straight onto the document by the service with no schema behind it, and a
|
|
3338
|
+
* value the enum never contained (`scheduled`) reached production rows and
|
|
3339
|
+
* rendered in the admin list as though it were real.
|
|
3340
|
+
*/
|
|
3341
|
+
status: string22().oneOf(SupportedCampaignStatuses).optional().default("draft" /* Draft */).label("Status"),
|
|
2398
3342
|
/** Key from email-service's template registry. Mutually exclusive with `html`. */
|
|
2399
|
-
templateKey:
|
|
3343
|
+
templateKey: string22().optional().label("Email Template"),
|
|
2400
3344
|
/**
|
|
2401
3345
|
* One entry per variable the chosen template declares. Validated at save time
|
|
2402
3346
|
* against the template's own `variables`, so a missing required binding is a
|
|
@@ -2413,56 +3357,50 @@ var CampaignSchema = object18({
|
|
|
2413
3357
|
*/
|
|
2414
3358
|
bindings: lazy2((value) => {
|
|
2415
3359
|
const keys = value && typeof value === "object" ? Object.keys(value) : [];
|
|
2416
|
-
return
|
|
2417
|
-
Object.fromEntries(keys.map((k) => [k,
|
|
3360
|
+
return object31(
|
|
3361
|
+
Object.fromEntries(keys.map((k) => [k, TemplateVariableBindingSchema.required()]))
|
|
2418
3362
|
).optional().default(() => ({})).label("Template Data");
|
|
2419
3363
|
}),
|
|
2420
|
-
/**
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
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"),
|
|
3364
|
+
/**
|
|
3365
|
+
* The data sources this campaign draws content from.
|
|
3366
|
+
*
|
|
3367
|
+
* SEVERAL, because one mail may combine them. Each source's outputs are
|
|
3368
|
+
* NAMESPACED by its id in the rendered payload (`jobs.jobCount`,
|
|
3369
|
+
* `groups.groupCount`), so two sources providing the same name cannot
|
|
3370
|
+
* overwrite each other and a template says which one it means.
|
|
3371
|
+
*/
|
|
3372
|
+
dataSources: array16().of(CampaignDataSourceSchema).optional().default(void 0).label("Data Sources"),
|
|
2442
3373
|
trigger: CampaignTriggerSchema.default(
|
|
2443
3374
|
() => CampaignTriggerSchema.getDefault()
|
|
2444
3375
|
),
|
|
2445
3376
|
// ── Safety ──────────────────────────────────────────────────────────────────
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
3377
|
+
/**
|
|
3378
|
+
* Still TWO deliberate acts before mail leaves, as before: a campaign must be
|
|
3379
|
+
* activated (`status: running`) AND taken out of dry run.
|
|
3380
|
+
*
|
|
3381
|
+
* The first half used to be a separate `enabled` boolean living alongside the
|
|
3382
|
+
* status, which meant two fields encoding one idea and able to disagree — an
|
|
3383
|
+
* `enabled: false` campaign whose status said it was sending was
|
|
3384
|
+
* representable, and the admin list showed exactly that. Activation is a
|
|
3385
|
+
* state transition, so it belongs in the state.
|
|
3386
|
+
*/
|
|
3387
|
+
dryRun: boolean17().optional().default(true).label("Dry Run"),
|
|
3388
|
+
maxRecipients: number14().integer().min(1).max(CAMPAIGN_MAX_RECIPIENTS_CEILING).optional().default(void 0).label("Max Recipients"),
|
|
3389
|
+
utm: object31({
|
|
3390
|
+
campaign: string22().optional().max(120).label("utm_campaign"),
|
|
3391
|
+
content: string22().optional().max(120).label("utm_content")
|
|
2454
3392
|
}).optional().default(void 0).label("Link Tagging")
|
|
2455
3393
|
}).label("Campaign");
|
|
2456
3394
|
|
|
2457
3395
|
// src/pagination/pagination.schema.ts
|
|
2458
|
-
import { number as
|
|
2459
|
-
var PaginationSchema =
|
|
2460
|
-
page:
|
|
2461
|
-
limit:
|
|
2462
|
-
sortBy:
|
|
2463
|
-
nextPage:
|
|
2464
|
-
prevPage:
|
|
2465
|
-
sortDirection:
|
|
3396
|
+
import { number as number15, object as object32, string as string23 } from "yup";
|
|
3397
|
+
var PaginationSchema = object32().shape({
|
|
3398
|
+
page: number15().required().min(1).label("Page").default(1),
|
|
3399
|
+
limit: number15().required().min(1).max(100).label("Limit").default(10),
|
|
3400
|
+
sortBy: string23().notOneOf([""]).optional().label("SortBy"),
|
|
3401
|
+
nextPage: string23().optional().label("Next Page Token"),
|
|
3402
|
+
prevPage: string23().optional().label("Previous Page Token"),
|
|
3403
|
+
sortDirection: string23().oneOf(["asc", "desc"]).optional().label("Sort Direction")
|
|
2466
3404
|
});
|
|
2467
3405
|
|
|
2468
3406
|
// src/pagination/pagination.constant.ts
|
|
@@ -2478,7 +3416,7 @@ var DefaultPaginationOptions = {
|
|
|
2478
3416
|
};
|
|
2479
3417
|
|
|
2480
3418
|
// src/saved-search/saved-search.schema.ts
|
|
2481
|
-
import { number as
|
|
3419
|
+
import { number as number16, object as object33, string as string24 } from "yup";
|
|
2482
3420
|
|
|
2483
3421
|
// src/saved-search/saved-search.constant.ts
|
|
2484
3422
|
var SavedSearchStatus = /* @__PURE__ */ ((SavedSearchStatus2) => {
|
|
@@ -2489,21 +3427,21 @@ var SavedSearchStatus = /* @__PURE__ */ ((SavedSearchStatus2) => {
|
|
|
2489
3427
|
var SupportedSavedSearchStatuses = Object.values(SavedSearchStatus);
|
|
2490
3428
|
|
|
2491
3429
|
// src/saved-search/saved-search.schema.ts
|
|
2492
|
-
var SavedSearchSchema =
|
|
3430
|
+
var SavedSearchSchema = object33({
|
|
2493
3431
|
/** Public identifier. Server-assigned. */
|
|
2494
|
-
shortId:
|
|
3432
|
+
shortId: string24().trim().label("Short Id"),
|
|
2495
3433
|
/**
|
|
2496
3434
|
* Owner's Better Auth account id. Server-assigned from the verified token,
|
|
2497
3435
|
* never accepted from a request body.
|
|
2498
3436
|
*/
|
|
2499
|
-
ownerUserId:
|
|
3437
|
+
ownerUserId: string24().trim().label("Owner"),
|
|
2500
3438
|
/** What the user calls this search, e.g. "Remote React roles". */
|
|
2501
|
-
label:
|
|
3439
|
+
label: string24().trim().max(120).required().label("Label"),
|
|
2502
3440
|
/**
|
|
2503
3441
|
* The free-text query. Optional because a search can be filters-only ("all
|
|
2504
3442
|
* remote jobs in Sweden"), which is a legitimate alert to want.
|
|
2505
3443
|
*/
|
|
2506
|
-
search:
|
|
3444
|
+
search: string24().trim().max(500).optional().label("Search Text"),
|
|
2507
3445
|
/**
|
|
2508
3446
|
* Field filters, in the same `{ field: values }` shape the search endpoint
|
|
2509
3447
|
* parses from `filter[field]=a,b`. Held as a free-form object rather than a
|
|
@@ -2511,7 +3449,7 @@ var SavedSearchSchema = object20({
|
|
|
2511
3449
|
* change independently; validating them here would put this package in the
|
|
2512
3450
|
* business of tracking that vocabulary.
|
|
2513
3451
|
*/
|
|
2514
|
-
filter:
|
|
3452
|
+
filter: object33().optional().default({}).label("Filters"),
|
|
2515
3453
|
/**
|
|
2516
3454
|
* How often to mail matches. `off` keeps the search saved but silent, which is
|
|
2517
3455
|
* the difference between "I don't want mail" and "delete my search".
|
|
@@ -2520,26 +3458,26 @@ var SavedSearchSchema = object20({
|
|
|
2520
3458
|
* than defining a second one: two vocabularies for the same concept is how
|
|
2521
3459
|
* they drift.
|
|
2522
3460
|
*/
|
|
2523
|
-
frequency:
|
|
2524
|
-
status:
|
|
3461
|
+
frequency: string24().oneOf(SupportedJobAlertFrequencies).default("off" /* Off */).label("Alert Frequency"),
|
|
3462
|
+
status: string24().oneOf(SupportedSavedSearchStatuses).default("active" /* Active */).label("Status"),
|
|
2525
3463
|
/**
|
|
2526
3464
|
* Epoch ms of the last alert sent for this search. The matching sweep reads it
|
|
2527
3465
|
* to decide whether the cadence is due, and writes it only AFTER a successful
|
|
2528
3466
|
* publish, so a failed run retries next tick rather than silently skipping a
|
|
2529
3467
|
* period.
|
|
2530
3468
|
*/
|
|
2531
|
-
lastNotifiedAt:
|
|
3469
|
+
lastNotifiedAt: number16().optional().label("Last Notified At"),
|
|
2532
3470
|
/**
|
|
2533
3471
|
* Epoch ms high-water mark: only jobs created after this are "new" for the
|
|
2534
3472
|
* next alert. Kept separate from `lastNotifiedAt` because a run that finds no
|
|
2535
3473
|
* matches should still advance what counts as new, without implying mail was
|
|
2536
3474
|
* sent.
|
|
2537
3475
|
*/
|
|
2538
|
-
lastMatchedAt:
|
|
3476
|
+
lastMatchedAt: number16().optional().label("Last Matched At")
|
|
2539
3477
|
}).noUnknown().strict();
|
|
2540
3478
|
|
|
2541
3479
|
// src/report/report.schema.ts
|
|
2542
|
-
import { mixed as
|
|
3480
|
+
import { mixed as mixed6, object as object34, string as string25 } from "yup";
|
|
2543
3481
|
|
|
2544
3482
|
// src/report/report.constant.ts
|
|
2545
3483
|
var ResourceType = /* @__PURE__ */ ((ResourceType3) => {
|
|
@@ -2563,15 +3501,15 @@ var ReportReason = /* @__PURE__ */ ((ReportReason3) => {
|
|
|
2563
3501
|
var SupportedReportReasons = Object.values(ReportReason);
|
|
2564
3502
|
|
|
2565
3503
|
// src/report/report.schema.ts
|
|
2566
|
-
var ReportSchema =
|
|
2567
|
-
type:
|
|
2568
|
-
resourceId:
|
|
2569
|
-
reason:
|
|
2570
|
-
comment:
|
|
3504
|
+
var ReportSchema = object34({
|
|
3505
|
+
type: mixed6().oneOf(SupportedResourceTypes).required().label("Type"),
|
|
3506
|
+
resourceId: string25().required().label("Resource ID"),
|
|
3507
|
+
reason: mixed6().oneOf(SupportedReportReasons).required("Please choose a reason").label("Reason"),
|
|
3508
|
+
comment: string25().max(1e3).optional().label("Comment")
|
|
2571
3509
|
}).label("Report Schema");
|
|
2572
3510
|
|
|
2573
|
-
// src/feedback/feedback.schema.ts
|
|
2574
|
-
import {
|
|
3511
|
+
// src/feedback/feedback-filter.schema.ts
|
|
3512
|
+
import { object as object35 } from "yup";
|
|
2575
3513
|
|
|
2576
3514
|
// src/feedback/feedback.constant.ts
|
|
2577
3515
|
var FeedbackType = /* @__PURE__ */ ((FeedbackType3) => {
|
|
@@ -2593,64 +3531,317 @@ var FeedbackStatus = /* @__PURE__ */ ((FeedbackStatus3) => {
|
|
|
2593
3531
|
var SupportedFeedbackStatuses = Object.values(FeedbackStatus);
|
|
2594
3532
|
var FEEDBACK_MESSAGE_MAX = 4e3;
|
|
2595
3533
|
|
|
3534
|
+
// src/feedback/feedback-filter.schema.ts
|
|
3535
|
+
var FeedbackFilterShape = object35({
|
|
3536
|
+
type: enumFacet(SupportedFeedbackTypes, "Type"),
|
|
3537
|
+
status: enumFacet(SupportedFeedbackStatuses, "Status")
|
|
3538
|
+
}).label("Feedback Filter");
|
|
3539
|
+
var FeedbackFilterSchema = rejectUnknownKeys(
|
|
3540
|
+
FeedbackFilterShape,
|
|
3541
|
+
() => Object.keys(FeedbackFilterShape.fields)
|
|
3542
|
+
);
|
|
3543
|
+
|
|
2596
3544
|
// src/feedback/feedback.schema.ts
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
3545
|
+
import { mixed as mixed7, object as object36, string as string26 } from "yup";
|
|
3546
|
+
var FeedbackSchema = object36({
|
|
3547
|
+
type: mixed7().oneOf(SupportedFeedbackTypes).required().label("Type"),
|
|
3548
|
+
message: string26().trim().required("Please enter a message").max(FEEDBACK_MESSAGE_MAX).label("Message"),
|
|
3549
|
+
email: string26().email().optional().label("Email"),
|
|
3550
|
+
pageUrl: string26().optional().label("Page URL"),
|
|
3551
|
+
status: mixed7().oneOf(SupportedFeedbackStatuses).optional().label("Status")
|
|
2603
3552
|
}).label("Feedback Schema");
|
|
2604
3553
|
|
|
3554
|
+
// src/skill/skill-filter.schema.ts
|
|
3555
|
+
import { object as object37 } from "yup";
|
|
3556
|
+
var SkillFilterShape = object37({
|
|
3557
|
+
/** Display names, matched exactly. Free-text matching is `?search=`. */
|
|
3558
|
+
names: tokens("Names"),
|
|
3559
|
+
/**
|
|
3560
|
+
* Taxonomy concept ids, e.g. "power-bi".
|
|
3561
|
+
*
|
|
3562
|
+
* `tokens`, NOT `codeTokens`: a conceptId is a lowercase slug, and
|
|
3563
|
+
* uppercasing it would match nothing.
|
|
3564
|
+
*/
|
|
3565
|
+
conceptIds: tokens("Concept IDs"),
|
|
3566
|
+
tags: tokens("Tags")
|
|
3567
|
+
}).label("Skill Filter");
|
|
3568
|
+
var SkillFilterSchema = rejectUnknownKeys(
|
|
3569
|
+
SkillFilterShape,
|
|
3570
|
+
() => Object.keys(SkillFilterShape.fields)
|
|
3571
|
+
);
|
|
3572
|
+
|
|
3573
|
+
// src/billing/billing.schema.ts
|
|
3574
|
+
import { array as array17, boolean as boolean18, number as number17, object as object38, string as string27 } from "yup";
|
|
3575
|
+
|
|
3576
|
+
// src/billing/billing.constant.ts
|
|
3577
|
+
var PlanKind = /* @__PURE__ */ ((PlanKind2) => {
|
|
3578
|
+
PlanKind2["Subscription"] = "subscription";
|
|
3579
|
+
PlanKind2["OneOff"] = "one-off";
|
|
3580
|
+
return PlanKind2;
|
|
3581
|
+
})(PlanKind || {});
|
|
3582
|
+
var SupportedPlanKinds = Object.values(PlanKind);
|
|
3583
|
+
var PlanStatus = /* @__PURE__ */ ((PlanStatus2) => {
|
|
3584
|
+
PlanStatus2["Draft"] = "draft";
|
|
3585
|
+
PlanStatus2["Active"] = "active";
|
|
3586
|
+
PlanStatus2["Archived"] = "archived";
|
|
3587
|
+
return PlanStatus2;
|
|
3588
|
+
})(PlanStatus || {});
|
|
3589
|
+
var SupportedPlanStatuses = Object.values(PlanStatus);
|
|
3590
|
+
var GrantType = /* @__PURE__ */ ((GrantType2) => {
|
|
3591
|
+
GrantType2["Boolean"] = "boolean";
|
|
3592
|
+
GrantType2["Quota"] = "quota";
|
|
3593
|
+
GrantType2["Credit"] = "credit";
|
|
3594
|
+
return GrantType2;
|
|
3595
|
+
})(GrantType || {});
|
|
3596
|
+
var SupportedGrantTypes = Object.values(GrantType);
|
|
3597
|
+
var QuotaPeriod = /* @__PURE__ */ ((QuotaPeriod2) => {
|
|
3598
|
+
QuotaPeriod2["Month"] = "month";
|
|
3599
|
+
QuotaPeriod2["Year"] = "year";
|
|
3600
|
+
QuotaPeriod2["Lifetime"] = "lifetime";
|
|
3601
|
+
return QuotaPeriod2;
|
|
3602
|
+
})(QuotaPeriod || {});
|
|
3603
|
+
var SupportedQuotaPeriods = Object.values(QuotaPeriod);
|
|
3604
|
+
var BillingPeriod = /* @__PURE__ */ ((BillingPeriod2) => {
|
|
3605
|
+
BillingPeriod2["Month"] = "month";
|
|
3606
|
+
BillingPeriod2["Year"] = "year";
|
|
3607
|
+
return BillingPeriod2;
|
|
3608
|
+
})(BillingPeriod || {});
|
|
3609
|
+
var SupportedBillingPeriods = Object.values(BillingPeriod);
|
|
3610
|
+
var SubscriptionStatus = /* @__PURE__ */ ((SubscriptionStatus2) => {
|
|
3611
|
+
SubscriptionStatus2["Active"] = "active";
|
|
3612
|
+
SubscriptionStatus2["Trialing"] = "trialing";
|
|
3613
|
+
SubscriptionStatus2["PastDue"] = "past_due";
|
|
3614
|
+
SubscriptionStatus2["Paused"] = "paused";
|
|
3615
|
+
SubscriptionStatus2["Canceled"] = "canceled";
|
|
3616
|
+
return SubscriptionStatus2;
|
|
3617
|
+
})(SubscriptionStatus || {});
|
|
3618
|
+
var SupportedSubscriptionStatuses = Object.values(SubscriptionStatus);
|
|
3619
|
+
var EntitlingSubscriptionStatuses = [
|
|
3620
|
+
"active" /* Active */,
|
|
3621
|
+
"trialing" /* Trialing */,
|
|
3622
|
+
"past_due" /* PastDue */
|
|
3623
|
+
];
|
|
3624
|
+
var TransactionStatus = /* @__PURE__ */ ((TransactionStatus2) => {
|
|
3625
|
+
TransactionStatus2["Draft"] = "draft";
|
|
3626
|
+
TransactionStatus2["Ready"] = "ready";
|
|
3627
|
+
TransactionStatus2["Billed"] = "billed";
|
|
3628
|
+
TransactionStatus2["Paid"] = "paid";
|
|
3629
|
+
TransactionStatus2["Completed"] = "completed";
|
|
3630
|
+
TransactionStatus2["Canceled"] = "canceled";
|
|
3631
|
+
TransactionStatus2["PastDue"] = "past_due";
|
|
3632
|
+
return TransactionStatus2;
|
|
3633
|
+
})(TransactionStatus || {});
|
|
3634
|
+
var SupportedTransactionStatuses = Object.values(TransactionStatus);
|
|
3635
|
+
var PaddleEnvironment = /* @__PURE__ */ ((PaddleEnvironment2) => {
|
|
3636
|
+
PaddleEnvironment2["Sandbox"] = "sandbox";
|
|
3637
|
+
PaddleEnvironment2["Production"] = "production";
|
|
3638
|
+
return PaddleEnvironment2;
|
|
3639
|
+
})(PaddleEnvironment || {});
|
|
3640
|
+
var SupportedPaddleEnvironments = Object.values(PaddleEnvironment);
|
|
3641
|
+
var FeatureKey = /* @__PURE__ */ ((FeatureKey2) => {
|
|
3642
|
+
FeatureKey2["JobPost"] = "job.post";
|
|
3643
|
+
FeatureKey2["JobAlerts"] = "job.alerts";
|
|
3644
|
+
FeatureKey2["PageCreate"] = "page.create";
|
|
3645
|
+
FeatureKey2["GroupCreate"] = "group.create";
|
|
3646
|
+
FeatureKey2["CandidateFinder"] = "candidate.finder";
|
|
3647
|
+
FeatureKey2["SearchAdvanced"] = "search.advanced";
|
|
3648
|
+
FeatureKey2["GetDiscovered"] = "get-discovered";
|
|
3649
|
+
return FeatureKey2;
|
|
3650
|
+
})(FeatureKey || {});
|
|
3651
|
+
var SupportedFeatureKeys = Object.values(FeatureKey);
|
|
3652
|
+
var EntitlementDenialReason = /* @__PURE__ */ ((EntitlementDenialReason2) => {
|
|
3653
|
+
EntitlementDenialReason2["NotEntitled"] = "not-entitled";
|
|
3654
|
+
EntitlementDenialReason2["QuotaExhausted"] = "quota-exhausted";
|
|
3655
|
+
EntitlementDenialReason2["NoCredits"] = "no-credits";
|
|
3656
|
+
return EntitlementDenialReason2;
|
|
3657
|
+
})(EntitlementDenialReason || {});
|
|
3658
|
+
var SupportedEntitlementDenialReasons = Object.values(
|
|
3659
|
+
EntitlementDenialReason
|
|
3660
|
+
);
|
|
3661
|
+
var GRANT_MAX_LIMIT = 1e5;
|
|
3662
|
+
var CONSUME_MAX_UNITS = 1e3;
|
|
3663
|
+
|
|
3664
|
+
// src/billing/billing.schema.ts
|
|
3665
|
+
var MoneySchema = object38({
|
|
3666
|
+
amountMinor: number17().integer().min(0).required().label("Amount (minor units)"),
|
|
3667
|
+
/**
|
|
3668
|
+
* ISO-4217, as returned by Paddle. Deliberately NOT constrained to
|
|
3669
|
+
* `SupportedSalaryCurrencies`: that list is the job-salary vocabulary, and
|
|
3670
|
+
* Paddle bills in currencies it does not contain. Constraining it here would
|
|
3671
|
+
* reject real payments.
|
|
3672
|
+
*/
|
|
3673
|
+
currencyCode: string27().trim().uppercase().required().label("Currency")
|
|
3674
|
+
}).label("Money");
|
|
3675
|
+
var PlanGrantSchema = object38({
|
|
3676
|
+
/**
|
|
3677
|
+
* The gated capability, e.g. `job.post`. A free string on purpose: see the
|
|
3678
|
+
* note on `FeatureKey` in `billing.constant.ts`. Adding a feature to the
|
|
3679
|
+
* paywall must not require releasing this package.
|
|
3680
|
+
*/
|
|
3681
|
+
feature: string27().trim().required().min(1).max(64).label("Feature"),
|
|
3682
|
+
type: string27().oneOf(SupportedGrantTypes).required().default("boolean" /* Boolean */).label("Grant Type"),
|
|
3683
|
+
/** Uses per period. Required for `quota`, meaningless otherwise. */
|
|
3684
|
+
limit: number17().integer().min(1).max(GRANT_MAX_LIMIT).optional().label("Limit").when("type", {
|
|
3685
|
+
is: "quota" /* Quota */,
|
|
3686
|
+
then: (s) => s.required("A quota grant needs a limit")
|
|
3687
|
+
}),
|
|
3688
|
+
period: string27().oneOf(SupportedQuotaPeriods).optional().default("month" /* Month */).label("Period").when("type", {
|
|
3689
|
+
is: "quota" /* Quota */,
|
|
3690
|
+
then: (s) => s.required("A quota grant needs a period")
|
|
3691
|
+
}),
|
|
3692
|
+
/** Non-expiring uses granted per purchase. Required for `credit`. */
|
|
3693
|
+
amount: number17().integer().min(1).max(GRANT_MAX_LIMIT).optional().label("Amount").when("type", {
|
|
3694
|
+
is: "credit" /* Credit */,
|
|
3695
|
+
then: (s) => s.required("A credit grant needs an amount")
|
|
3696
|
+
})
|
|
3697
|
+
}).label("Plan Grant");
|
|
3698
|
+
var PaddlePriceIdsSchema = object38({
|
|
3699
|
+
sandbox: array17().of(string27().required()).optional().default([]).label("Sandbox Price IDs"),
|
|
3700
|
+
live: array17().of(string27().required()).optional().default([]).label("Live Price IDs")
|
|
3701
|
+
}).label("Paddle Price IDs");
|
|
3702
|
+
var PlanSchema = object38({
|
|
3703
|
+
/** Stable slug, e.g. `employer-pro`. The identifier everything else references. */
|
|
3704
|
+
key: string27().trim().lowercase().required().min(2).max(64).label("Key"),
|
|
3705
|
+
name: string27().required().min(1).max(120).label("Name"),
|
|
3706
|
+
description: string27().optional().max(500).label("Description"),
|
|
3707
|
+
kind: string27().oneOf(SupportedPlanKinds).required().default("subscription" /* Subscription */).label("Kind"),
|
|
3708
|
+
status: string27().oneOf(SupportedPlanStatuses).required().default("draft" /* Draft */).label("Status"),
|
|
3709
|
+
paddlePriceIds: PaddlePriceIdsSchema.default(
|
|
3710
|
+
() => PaddlePriceIdsSchema.getDefault()
|
|
3711
|
+
),
|
|
3712
|
+
grants: array17().of(PlanGrantSchema.required()).required().min(1, "A plan must grant at least one feature").label("Grants"),
|
|
3713
|
+
/** Display price for the pricing page. Billing is always Paddle's own figure. */
|
|
3714
|
+
price: MoneySchema.optional().default(void 0).label("Price"),
|
|
3715
|
+
/**
|
|
3716
|
+
* How often this plan bills, for the pricing page's monthly/yearly toggle and
|
|
3717
|
+
* for rendering `price` with the right suffix.
|
|
3718
|
+
*
|
|
3719
|
+
* Display only: Paddle owns the real billing interval, and this never drives a
|
|
3720
|
+
* charge. It is optional because one-off plans have no interval, and because
|
|
3721
|
+
* plans seeded before this field existed must keep validating. Treat absent on
|
|
3722
|
+
* a subscription as "monthly" at the use site rather than defaulting it here,
|
|
3723
|
+
* so an unset value stays visible as unset.
|
|
3724
|
+
*/
|
|
3725
|
+
billingPeriod: string27().oneOf(SupportedBillingPeriods).optional().label("Billing Period"),
|
|
3726
|
+
/** Ordering on the pricing page, ascending. */
|
|
3727
|
+
sortOrder: number17().integer().optional().default(0).label("Sort Order"),
|
|
3728
|
+
createdAt: number17().optional().label("Created At"),
|
|
3729
|
+
updatedAt: number17().optional().label("Updated At")
|
|
3730
|
+
}).label("Plan");
|
|
3731
|
+
var BillingSubscriptionSchema = object38({
|
|
3732
|
+
userId: string27().required().label("User ID"),
|
|
3733
|
+
paddleSubscriptionId: string27().required().label("Paddle Subscription ID"),
|
|
3734
|
+
paddleCustomerId: string27().required().label("Paddle Customer ID"),
|
|
3735
|
+
planKey: string27().required().label("Plan Key"),
|
|
3736
|
+
priceIds: array17().of(string27().required()).required().label("Price IDs"),
|
|
3737
|
+
status: string27().oneOf(SupportedSubscriptionStatuses).required().label("Status"),
|
|
3738
|
+
/** Start of the period the quota is anchored to. */
|
|
3739
|
+
currentPeriodStart: number17().optional().label("Current Period Start"),
|
|
3740
|
+
currentPeriodEnd: number17().optional().label("Current Period End"),
|
|
3741
|
+
/** Set when a cancellation is scheduled; access continues until the period ends. */
|
|
3742
|
+
canceledAt: number17().optional().label("Canceled At"),
|
|
3743
|
+
/** `occurred_at` of the newest Paddle event applied. Guards out-of-order writes. */
|
|
3744
|
+
lastEventAt: number17().required().default(0).label("Last Event At"),
|
|
3745
|
+
createdAt: number17().optional().label("Created At"),
|
|
3746
|
+
updatedAt: number17().optional().label("Updated At")
|
|
3747
|
+
}).label("Billing Subscription");
|
|
3748
|
+
var BillingTransactionSchema = object38({
|
|
3749
|
+
userId: string27().required().label("User ID"),
|
|
3750
|
+
paddleTransactionId: string27().required().label("Paddle Transaction ID"),
|
|
3751
|
+
paddleCustomerId: string27().optional().label("Paddle Customer ID"),
|
|
3752
|
+
paddleSubscriptionId: string27().optional().label("Paddle Subscription ID"),
|
|
3753
|
+
planKey: string27().optional().label("Plan Key"),
|
|
3754
|
+
priceIds: array17().of(string27().required()).required().label("Price IDs"),
|
|
3755
|
+
status: string27().oneOf(SupportedTransactionStatuses).required().label("Status"),
|
|
3756
|
+
total: MoneySchema.optional().default(void 0).label("Total"),
|
|
3757
|
+
billedAt: number17().optional().label("Billed At"),
|
|
3758
|
+
lastEventAt: number17().required().default(0).label("Last Event At"),
|
|
3759
|
+
createdAt: number17().optional().label("Created At"),
|
|
3760
|
+
updatedAt: number17().optional().label("Updated At")
|
|
3761
|
+
}).label("Billing Transaction");
|
|
3762
|
+
var FeatureEntitlementSchema = object38({
|
|
3763
|
+
feature: string27().required().label("Feature"),
|
|
3764
|
+
type: string27().oneOf(SupportedGrantTypes).required().label("Grant Type"),
|
|
3765
|
+
/** Whether the feature may be used AT ALL right now. */
|
|
3766
|
+
allowed: boolean18().required().default(false).label("Allowed"),
|
|
3767
|
+
/** Quota ceiling for the period. Absent for boolean grants. */
|
|
3768
|
+
limit: number17().integer().optional().label("Limit"),
|
|
3769
|
+
used: number17().integer().optional().label("Used"),
|
|
3770
|
+
/** Quota remaining plus any credit balance. Absent for boolean grants. */
|
|
3771
|
+
remaining: number17().integer().optional().label("Remaining"),
|
|
3772
|
+
/** Non-expiring balance from one-off purchases. */
|
|
3773
|
+
credits: number17().integer().optional().label("Credits"),
|
|
3774
|
+
/** When the quota next refreshes. */
|
|
3775
|
+
resetsAt: number17().optional().label("Resets At")
|
|
3776
|
+
}).label("Feature Entitlement");
|
|
3777
|
+
var EntitlementDenialSchema = object38({
|
|
3778
|
+
feature: string27().required().label("Feature"),
|
|
3779
|
+
reason: string27().oneOf(SupportedEntitlementDenialReasons).required().label("Reason"),
|
|
3780
|
+
limit: number17().integer().optional().label("Limit"),
|
|
3781
|
+
used: number17().integer().optional().label("Used"),
|
|
3782
|
+
resetsAt: number17().optional().label("Resets At"),
|
|
3783
|
+
upgradeUrl: string27().optional().label("Upgrade URL")
|
|
3784
|
+
}).label("Entitlement Denial");
|
|
3785
|
+
var EntitlementConsumeSchema = object38({
|
|
3786
|
+
userId: string27().required().label("User ID"),
|
|
3787
|
+
feature: string27().trim().required().min(1).max(64).label("Feature"),
|
|
3788
|
+
units: number17().integer().min(1).max(CONSUME_MAX_UNITS).required().default(1).label("Units"),
|
|
3789
|
+
/**
|
|
3790
|
+
* Caller-supplied de-duplication key. Makes a spend exactly-once across the
|
|
3791
|
+
* retries that at-least-once delivery and ordinary network failures produce.
|
|
3792
|
+
*/
|
|
3793
|
+
idempotencyKey: string27().trim().required().min(8).max(128).label("Idempotency Key")
|
|
3794
|
+
}).label("Entitlement Consume");
|
|
3795
|
+
|
|
2605
3796
|
// src/user/user.schema.ts
|
|
2606
|
-
import { array as
|
|
3797
|
+
import { array as array21, boolean as boolean22, number as number20, object as object51, string as string40 } from "yup";
|
|
2607
3798
|
|
|
2608
3799
|
// src/user/work-experience.schema.ts
|
|
2609
|
-
import { boolean as
|
|
2610
|
-
var WorkExperienceSchema =
|
|
3800
|
+
import { boolean as boolean19, object as object39, string as string28 } from "yup";
|
|
3801
|
+
var WorkExperienceSchema = object39().shape({
|
|
2611
3802
|
company: PageSchema.pick(["name", "slug", "type", "logo"]).deepPartial().concat(PageSchema.pick(["name", "type"])).required().label("Company"),
|
|
2612
|
-
designation:
|
|
3803
|
+
designation: string28().trim().required().label("Designation"),
|
|
2613
3804
|
duration: DurationSchema({
|
|
2614
3805
|
format: "YYYY-MM",
|
|
2615
3806
|
startLabel: "Start Date",
|
|
2616
3807
|
endLabel: "End Date"
|
|
2617
3808
|
}).required().label("Duration"),
|
|
2618
|
-
description:
|
|
3809
|
+
description: string28().trim().max(3e3).optional().label("Description"),
|
|
2619
3810
|
location: LocationSchema.required().label("Location"),
|
|
2620
|
-
isRemote:
|
|
3811
|
+
isRemote: boolean19().oneOf([true, false]).default(false).label("Is Remote")
|
|
2621
3812
|
}).noUnknown().strict().label("Work Experience");
|
|
2622
3813
|
|
|
2623
3814
|
// src/user/education.schema.ts
|
|
2624
|
-
import { boolean as
|
|
2625
|
-
var EducationSchema =
|
|
3815
|
+
import { boolean as boolean20, object as object40, string as string29 } from "yup";
|
|
3816
|
+
var EducationSchema = object40({
|
|
2626
3817
|
institute: PageSchema.pick(["name", "slug", "type", "logo"]).deepPartial().concat(PageSchema.pick(["name", "type"])).required().label("Institute"),
|
|
2627
|
-
course:
|
|
2628
|
-
fieldOfStudy:
|
|
3818
|
+
course: string29().trim().required().label("Course"),
|
|
3819
|
+
fieldOfStudy: string29().trim().required().label("Field of Study"),
|
|
2629
3820
|
duration: DurationSchema({
|
|
2630
3821
|
format: "YYYY-MM",
|
|
2631
3822
|
startLabel: "Start Date",
|
|
2632
3823
|
endLabel: "End Date"
|
|
2633
3824
|
}).required().label("Duration"),
|
|
2634
|
-
description:
|
|
2635
|
-
isDistanceLearning:
|
|
3825
|
+
description: string29().trim().max(3e3).optional().label("Description"),
|
|
3826
|
+
isDistanceLearning: boolean20().default(false).label("Is Distance Learning"),
|
|
2636
3827
|
location: LocationSchema.required().label("Location"),
|
|
2637
|
-
studyType:
|
|
3828
|
+
studyType: string29().oneOf(SupportedStudyTypes).trim().max(50).required().label("Study Type")
|
|
2638
3829
|
}).noUnknown().strict().label("Education");
|
|
2639
3830
|
|
|
2640
3831
|
// src/user/user-skill.schema.ts
|
|
2641
|
-
import { object as
|
|
2642
|
-
var UserSkillSchema =
|
|
2643
|
-
name:
|
|
2644
|
-
proficiencyLevel:
|
|
3832
|
+
import { object as object41, string as string30 } from "yup";
|
|
3833
|
+
var UserSkillSchema = object41({
|
|
3834
|
+
name: string30().required().label("Name"),
|
|
3835
|
+
proficiencyLevel: string30().oneOf(SupportedProficiencyLevels).required().label("Proficiency Level"),
|
|
2645
3836
|
lastUsed: dateString().format("YYYY-MM").nullable().optional().label("Last Used")
|
|
2646
3837
|
}).noUnknown().strict().label("Skill");
|
|
2647
3838
|
|
|
2648
3839
|
// src/user/project.schema.ts
|
|
2649
|
-
import { object as
|
|
2650
|
-
var UserProjectSchema =
|
|
2651
|
-
name:
|
|
2652
|
-
url:
|
|
2653
|
-
description:
|
|
3840
|
+
import { object as object42, string as string31 } from "yup";
|
|
3841
|
+
var UserProjectSchema = object42({
|
|
3842
|
+
name: string31().trim().max(50).required().label("Name"),
|
|
3843
|
+
url: string31().optional().label("URL"),
|
|
3844
|
+
description: string31().trim().min(100).max(3e3).required().label("Description"),
|
|
2654
3845
|
duration: DurationSchema({
|
|
2655
3846
|
format: "YYYY-MM",
|
|
2656
3847
|
startLabel: "Start Date",
|
|
@@ -2659,54 +3850,68 @@ var UserProjectSchema = object26({
|
|
|
2659
3850
|
}).noUnknown().strict().label("Project");
|
|
2660
3851
|
|
|
2661
3852
|
// src/user/user-certification.schema.ts
|
|
2662
|
-
import { object as
|
|
2663
|
-
var UserCertificationSchema =
|
|
2664
|
-
name:
|
|
3853
|
+
import { object as object43, string as string32 } from "yup";
|
|
3854
|
+
var UserCertificationSchema = object43({
|
|
3855
|
+
name: string32().trim().max(100).required().label("Name"),
|
|
2665
3856
|
// TODO: Add validation for authority
|
|
2666
|
-
authority:
|
|
2667
|
-
licenseNumber:
|
|
3857
|
+
authority: string32().trim().max(100).required().label("Authority"),
|
|
3858
|
+
licenseNumber: string32().trim().max(50).optional().label("License Number"),
|
|
2668
3859
|
duration: DurationSchema({
|
|
2669
3860
|
format: "YYYY-MM",
|
|
2670
3861
|
startLabel: "Start Date",
|
|
2671
3862
|
endLabel: "End Date"
|
|
2672
3863
|
}).optional().nullable().label("Duration"),
|
|
2673
|
-
url:
|
|
3864
|
+
url: string32().optional().label("URL")
|
|
2674
3865
|
}).noUnknown().strict().label("Certification");
|
|
2675
3866
|
|
|
2676
3867
|
// src/user/user-interest.schema.ts
|
|
2677
|
-
import { string as
|
|
2678
|
-
var UserInterestSchema =
|
|
3868
|
+
import { object as object44, string as string33 } from "yup";
|
|
3869
|
+
var UserInterestSchema = object44({
|
|
3870
|
+
/** The taxonomy entry's id. */
|
|
3871
|
+
id: string33().trim().required().label("Interest Id"),
|
|
3872
|
+
/** Display name, e.g. "Cloud Computing". */
|
|
3873
|
+
name: string33().trim().required().label("Interest"),
|
|
3874
|
+
/** Material-symbols icon name, e.g. "cloud". */
|
|
3875
|
+
logo: string33().trim().required().label("Interest Logo")
|
|
3876
|
+
}).required().label("Interest");
|
|
2679
3877
|
|
|
2680
3878
|
// src/user/user-language.schema.ts
|
|
2681
|
-
import { object as
|
|
2682
|
-
var UserLanguageSchema =
|
|
2683
|
-
name:
|
|
2684
|
-
proficiencyLevel:
|
|
3879
|
+
import { object as object45, string as string34 } from "yup";
|
|
3880
|
+
var UserLanguageSchema = object45({
|
|
3881
|
+
name: string34().trim().required().label("Name"),
|
|
3882
|
+
proficiencyLevel: string34().oneOf(SupportedProficiencyLevels).trim().max(50).required().label("Proficiency Level")
|
|
2685
3883
|
}).noUnknown().strict().label("Language");
|
|
2686
3884
|
|
|
2687
3885
|
// src/user/user-additional-info.schema.ts
|
|
2688
|
-
import { object as
|
|
2689
|
-
var UserAdditionalInfoSchema =
|
|
2690
|
-
title:
|
|
2691
|
-
description:
|
|
3886
|
+
import { object as object46, string as string35 } from "yup";
|
|
3887
|
+
var UserAdditionalInfoSchema = object46({
|
|
3888
|
+
title: string35().trim().max(60).required().label("Title"),
|
|
3889
|
+
description: string35().trim().max(1e3).required().label("Description")
|
|
2692
3890
|
}).noUnknown().strict().label("User Additional Info");
|
|
2693
3891
|
|
|
2694
3892
|
// src/user/general-detail.schema.ts
|
|
2695
|
-
import { number as
|
|
2696
|
-
var UserGeneralDetailSchema =
|
|
2697
|
-
id:
|
|
2698
|
-
name:
|
|
2699
|
-
first:
|
|
2700
|
-
last:
|
|
3893
|
+
import { number as number18, object as object47, string as string36 } from "yup";
|
|
3894
|
+
var UserGeneralDetailSchema = object47({
|
|
3895
|
+
id: string36().optional().label("ID"),
|
|
3896
|
+
name: object47().shape({
|
|
3897
|
+
first: string36().trim().max(50).required().label("First Name"),
|
|
3898
|
+
last: string36().trim().max(50).optional().label("Last Name")
|
|
2701
3899
|
}).required().label("Name"),
|
|
2702
|
-
headline:
|
|
2703
|
-
image:
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
3900
|
+
headline: string36().trim().max(200).optional().label("Headline"),
|
|
3901
|
+
image: string36().optional().label("Image"),
|
|
3902
|
+
// The DISPLAYED avatar (`image`) is a flattened export with any status frame
|
|
3903
|
+
// ("#OpenToWork", …) already burned into the pixels. `imageOriginal` keeps the
|
|
3904
|
+
// un-framed source the user uploaded, so the image builder can reopen it and
|
|
3905
|
+
// apply a DIFFERENT frame instead of stacking a second one on top of the first.
|
|
3906
|
+
// Optional and absent on every pre-existing user: treat a missing value as
|
|
3907
|
+
// "only the flattened image is available".
|
|
3908
|
+
imageOriginal: string36().optional().label("Original Image"),
|
|
3909
|
+
aboutMe: string36().trim().max(3e3).optional().label("About Me"),
|
|
3910
|
+
email: string36().required().label("Email"),
|
|
3911
|
+
mobile: string36().nullable().optional().label("Mobile"),
|
|
3912
|
+
emailVerified: string36().nullable().optional().label("Email Verified"),
|
|
3913
|
+
mobileVerified: string36().nullable().optional().label("Mobile Verified"),
|
|
3914
|
+
experienceLevel: string36().oneOf(SupportedExperienceLevels).required().label("Experience level"),
|
|
2710
3915
|
/**
|
|
2711
3916
|
* The user's HIGHEST qualification, e.g. `bachelor`.
|
|
2712
3917
|
*
|
|
@@ -2725,7 +3930,7 @@ var UserGeneralDetailSchema = object30({
|
|
|
2725
3930
|
* UNCONSTRAINED on education rather than filtering the user out — the same
|
|
2726
3931
|
* opt-out reasoning `notificationPrefs` uses.
|
|
2727
3932
|
*/
|
|
2728
|
-
educationLevel:
|
|
3933
|
+
educationLevel: string36().oneOf(SupportedEducationLevels).optional().label("Education level"),
|
|
2729
3934
|
location: LocationSchema.required().label("Location"),
|
|
2730
3935
|
/**
|
|
2731
3936
|
* Where the account was created, captured once and never rewritten.
|
|
@@ -2741,38 +3946,38 @@ var UserGeneralDetailSchema = object30({
|
|
|
2741
3946
|
* - `location` derived from the user's self-entered profile location
|
|
2742
3947
|
* - `default` assumed (no signal available); presumption, not evidence
|
|
2743
3948
|
*/
|
|
2744
|
-
signupContext:
|
|
3949
|
+
signupContext: object47().shape({
|
|
2745
3950
|
// ISO 3166-1 alpha-2, uppercase — the shape `cf-ipcountry` returns, so
|
|
2746
3951
|
// measured and inferred values are directly comparable.
|
|
2747
|
-
country:
|
|
2748
|
-
locale:
|
|
2749
|
-
source:
|
|
2750
|
-
at:
|
|
3952
|
+
country: string36().trim().uppercase().length(2).required().label("Signup Country"),
|
|
3953
|
+
locale: string36().trim().required().label("Signup Locale"),
|
|
3954
|
+
source: string36().oneOf(SupportedSignupContextSources).required().label("Signup Context Source"),
|
|
3955
|
+
at: number18().required().label("Signup Context At")
|
|
2751
3956
|
}).optional().default(void 0).label("Signup Context"),
|
|
2752
|
-
profileVisibility:
|
|
3957
|
+
profileVisibility: string36().oneOf(SupportedUserProfileVisibilities).default("public" /* Public */).label("Profile Visibility")
|
|
2753
3958
|
}).noUnknown().strict().label("General Detail");
|
|
2754
3959
|
|
|
2755
3960
|
// src/user/user-job-preferences.schema.ts
|
|
2756
|
-
import { array as
|
|
2757
|
-
var UserJobPreferencesSchema =
|
|
3961
|
+
import { array as array18, boolean as boolean21, date as date2, number as number19, object as object48, string as string37 } from "yup";
|
|
3962
|
+
var UserJobPreferencesSchema = object48({
|
|
2758
3963
|
/**
|
|
2759
3964
|
* Whether the job seeker is openly signalling availability. Surfaces the
|
|
2760
3965
|
* "Open to work" badge on the public profile and boosts visibility in
|
|
2761
3966
|
* recruiter search. Defaults to `false` - users must opt in.
|
|
2762
3967
|
*/
|
|
2763
|
-
openToWork:
|
|
3968
|
+
openToWork: boolean21().default(false).label("Open to Work"),
|
|
2764
3969
|
/**
|
|
2765
3970
|
* How urgently the user is looking for their next job. Drives match scoring
|
|
2766
3971
|
* and can downgrade the visibility of expired or low-relevance postings for
|
|
2767
3972
|
* users in `passively_browsing` mode.
|
|
2768
3973
|
*/
|
|
2769
|
-
jobSearchUrgency:
|
|
3974
|
+
jobSearchUrgency: string37().oneOf(SupportedJobSearchUrgencies).required().label("Job Search Urgency"),
|
|
2770
3975
|
/**
|
|
2771
3976
|
* Locations the user wants to work in. Stores the full `LocationSchema`
|
|
2772
3977
|
* shape (country, city, state, geo, etc.) as returned by the locations
|
|
2773
3978
|
* autocomplete API.
|
|
2774
3979
|
*/
|
|
2775
|
-
jobLocations:
|
|
3980
|
+
jobLocations: array18().of(LocationSchema.required().label("Job Location")).min(1, "Pick at least one preferred location.").required().label("Job Locations"),
|
|
2776
3981
|
/**
|
|
2777
3982
|
* Seniority levels the user wants jobs to be matched against. Multi-select
|
|
2778
3983
|
* (up to 2) for borderline candidates.
|
|
@@ -2783,7 +3988,7 @@ var UserJobPreferencesSchema = object31({
|
|
|
2783
3988
|
* Both reuse the same `ExperienceLevel` taxonomy from `common.constant.ts`
|
|
2784
3989
|
* so search/match logic doesn't have to translate between two vocabularies.
|
|
2785
3990
|
*/
|
|
2786
|
-
targetExperienceLevels:
|
|
3991
|
+
targetExperienceLevels: array18().of(string37().oneOf(SupportedExperienceLevels).required()).min(1, "Pick at least one experience level.").max(2, "You can select at most 2 experience levels.").required().label("Target Experience Levels"),
|
|
2787
3992
|
/**
|
|
2788
3993
|
* Specializations the user is searching for - free-form titles.
|
|
2789
3994
|
*
|
|
@@ -2792,19 +3997,19 @@ var UserJobPreferencesSchema = object31({
|
|
|
2792
3997
|
* surfaces a curated "Popular Roles" picker for discovery, but the user
|
|
2793
3998
|
* can ultimately add any title; matching/normalization happens downstream.
|
|
2794
3999
|
*/
|
|
2795
|
-
jobRoles:
|
|
4000
|
+
jobRoles: array18().of(string37().trim().max(120).required()).min(1, "Pick at least one role.").required().label("Job Roles"),
|
|
2796
4001
|
/**
|
|
2797
4002
|
* Minimum acceptable annual base salary, in `minSalaryCurrency`.
|
|
2798
4003
|
* Stored as an integer in the currency's major unit (e.g. dollars, not cents).
|
|
2799
4004
|
* Defaults to 0 ("any salary") rather than being optional - every job-seeker
|
|
2800
4005
|
* has a floor, even if it's zero.
|
|
2801
4006
|
*/
|
|
2802
|
-
minSalary:
|
|
2803
|
-
minSalaryCurrency:
|
|
4007
|
+
minSalary: number19().integer().min(MIN_SALARY_LOWER_BOUND).max(MIN_SALARY_UPPER_BOUND).default(0).required().label("Minimum Salary"),
|
|
4008
|
+
minSalaryCurrency: string37().oneOf(SupportedSalaryCurrencies).default("USD").required().label("Minimum Salary Currency"),
|
|
2804
4009
|
/**
|
|
2805
4010
|
* Marketing-attribution capture from the final onboarding step.
|
|
2806
4011
|
*/
|
|
2807
|
-
referralSource:
|
|
4012
|
+
referralSource: string37().oneOf(SupportedReferralSources).required().label("Referral Source"),
|
|
2808
4013
|
/**
|
|
2809
4014
|
* Resumes the user has uploaded. Embedded directly on the user document
|
|
2810
4015
|
* (capped at 5) - they're cheap to denormalize, every wizard step that
|
|
@@ -2818,18 +4023,18 @@ var UserJobPreferencesSchema = object31({
|
|
|
2818
4023
|
* AI resume-builder flow - onboarding uploads land here, AI-built resumes
|
|
2819
4024
|
* live in their own collection.
|
|
2820
4025
|
*/
|
|
2821
|
-
resumes:
|
|
2822
|
-
|
|
4026
|
+
resumes: array18().of(
|
|
4027
|
+
object48({
|
|
2823
4028
|
// Stable id assigned on upload - used to address a specific entry
|
|
2824
4029
|
// for delete / set-primary operations without exposing the GCS URL
|
|
2825
4030
|
// as a route key.
|
|
2826
|
-
id:
|
|
2827
|
-
url:
|
|
2828
|
-
filename:
|
|
2829
|
-
sizeBytes:
|
|
2830
|
-
mimeType:
|
|
4031
|
+
id: string37().required().label("ID"),
|
|
4032
|
+
url: string37().required().label("Resume URL"),
|
|
4033
|
+
filename: string37().trim().max(255).optional().label("Filename"),
|
|
4034
|
+
sizeBytes: number19().integer().min(0).optional().label("Size (bytes)"),
|
|
4035
|
+
mimeType: string37().trim().max(120).optional().label("MIME Type"),
|
|
2831
4036
|
uploadedAt: date2().required().label("Uploaded At"),
|
|
2832
|
-
isPrimary:
|
|
4037
|
+
isPrimary: boolean21().default(false).label("Is Primary")
|
|
2833
4038
|
}).noUnknown().strict().label("Resume")
|
|
2834
4039
|
).max(5, "You can keep at most 5 resumes on file.").default([]).label("Resumes"),
|
|
2835
4040
|
/**
|
|
@@ -2856,92 +4061,92 @@ var UserJobPreferencesSchema = object31({
|
|
|
2856
4061
|
* APIs. If `user.<field>` is empty there, the correct behavior is empty,
|
|
2857
4062
|
* because the user has not confirmed it yet.
|
|
2858
4063
|
*/
|
|
2859
|
-
pendingPrefill:
|
|
2860
|
-
headline:
|
|
2861
|
-
aboutMe:
|
|
2862
|
-
mobile:
|
|
4064
|
+
pendingPrefill: object48({
|
|
4065
|
+
headline: string37().trim().optional().label("Pending Headline"),
|
|
4066
|
+
aboutMe: string37().trim().optional().label("Pending About Me"),
|
|
4067
|
+
mobile: string37().trim().optional().label("Pending Mobile"),
|
|
2863
4068
|
location: LocationSchema.optional().default(void 0).label("Pending Location"),
|
|
2864
|
-
experienceLevel:
|
|
2865
|
-
socialAccounts:
|
|
2866
|
-
workExperiences:
|
|
2867
|
-
educations:
|
|
4069
|
+
experienceLevel: string37().oneOf(SupportedExperienceLevels).optional().label("Pending Experience Level"),
|
|
4070
|
+
socialAccounts: array18().of(SocialAccountSchema.required()).optional().label("Pending Social Accounts"),
|
|
4071
|
+
workExperiences: array18().of(WorkExperienceSchema.required()).optional().label("Pending Work Experiences"),
|
|
4072
|
+
educations: array18().of(EducationSchema.required()).optional().label("Pending Educations")
|
|
2868
4073
|
}).nullable().optional().default(void 0).label("Pending Prefill")
|
|
2869
4074
|
}).noUnknown().strict().label("User Job Preferences Schema");
|
|
2870
4075
|
|
|
2871
4076
|
// src/user/user-recruiter-profile.schema.ts
|
|
2872
|
-
import { array as
|
|
2873
|
-
var RecruiterPageLinkSchema =
|
|
4077
|
+
import { array as array19, object as object49, string as string38 } from "yup";
|
|
4078
|
+
var RecruiterPageLinkSchema = object49({
|
|
2874
4079
|
page: PageSchema.pick(["name", "slug", "type", "logo"]).deepPartial().concat(PageSchema.pick(["name", "type"])).required().label("Company Page"),
|
|
2875
|
-
jobTitle:
|
|
4080
|
+
jobTitle: string38().trim().max(100).optional().label("Job Title")
|
|
2876
4081
|
}).noUnknown().strict().label("Recruiter Page Link");
|
|
2877
|
-
var UserRecruiterProfileSchema =
|
|
2878
|
-
recruiterProfile:
|
|
2879
|
-
hiringFor:
|
|
4082
|
+
var UserRecruiterProfileSchema = object49({
|
|
4083
|
+
recruiterProfile: object49({
|
|
4084
|
+
hiringFor: array19().of(RecruiterPageLinkSchema).default([]).label("Hiring For")
|
|
2880
4085
|
}).optional().default(void 0).label("Recruiter Profile")
|
|
2881
4086
|
}).noUnknown().strict().label("User Recruiter Profile");
|
|
2882
4087
|
|
|
2883
4088
|
// src/user/user-coordinator-profile.schema.ts
|
|
2884
|
-
import { array as
|
|
2885
|
-
var CoordinatorPageLinkSchema =
|
|
4089
|
+
import { array as array20, object as object50, string as string39 } from "yup";
|
|
4090
|
+
var CoordinatorPageLinkSchema = object50({
|
|
2886
4091
|
page: PageSchema.pick(["name", "slug", "type", "logo"]).deepPartial().concat(PageSchema.pick(["name", "type"])).required().label("Institute Page"),
|
|
2887
|
-
jobTitle:
|
|
4092
|
+
jobTitle: string39().trim().max(100).optional().label("Job Title")
|
|
2888
4093
|
}).noUnknown().strict().label("Coordinator Page Link");
|
|
2889
|
-
var UserCoordinatorProfileSchema =
|
|
2890
|
-
coordinatorProfile:
|
|
2891
|
-
coordinatesAt:
|
|
4094
|
+
var UserCoordinatorProfileSchema = object50({
|
|
4095
|
+
coordinatorProfile: object50({
|
|
4096
|
+
coordinatesAt: array20().of(CoordinatorPageLinkSchema).default([]).label("Coordinates At")
|
|
2892
4097
|
}).optional().default(void 0).label("Coordinator Profile")
|
|
2893
4098
|
}).noUnknown().strict().label("User Coordinator Profile");
|
|
2894
4099
|
|
|
2895
4100
|
// src/user/user.schema.ts
|
|
2896
|
-
var UserSchema =
|
|
4101
|
+
var UserSchema = object51({
|
|
2897
4102
|
/**
|
|
2898
4103
|
* Related auth account id from auth Server (e.g. Better auth https://auth.thejob.dev)
|
|
2899
4104
|
*/
|
|
2900
|
-
authAccountId:
|
|
4105
|
+
authAccountId: string40().required().label("Auth Account ID"),
|
|
2901
4106
|
/**
|
|
2902
4107
|
* Social media information about the user (e.g. LinkedIn, GitHub, etc.)
|
|
2903
4108
|
*/
|
|
2904
|
-
socialAccounts:
|
|
4109
|
+
socialAccounts: array21().of(SocialAccountSchema).default([]).label("Social Accounts"),
|
|
2905
4110
|
/**
|
|
2906
4111
|
* Work experience information about the user
|
|
2907
4112
|
*/
|
|
2908
|
-
workExperiences:
|
|
4113
|
+
workExperiences: array21().of(WorkExperienceSchema).required().default([]).label("Work Experiences"),
|
|
2909
4114
|
/**
|
|
2910
4115
|
* Education information about the user
|
|
2911
4116
|
*/
|
|
2912
|
-
educations:
|
|
4117
|
+
educations: array21().of(EducationSchema).required().default([]).label("Educations"),
|
|
2913
4118
|
/**
|
|
2914
4119
|
* Skills information about the user
|
|
2915
4120
|
*/
|
|
2916
|
-
skills:
|
|
4121
|
+
skills: array21().of(UserSkillSchema).required().default([]).label("Skills"),
|
|
2917
4122
|
/**
|
|
2918
4123
|
* Projects information about the user
|
|
2919
4124
|
*/
|
|
2920
|
-
projects:
|
|
4125
|
+
projects: array21().of(UserProjectSchema).default([]).label("Projects"),
|
|
2921
4126
|
/**
|
|
2922
4127
|
* Certifications information about the user
|
|
2923
4128
|
*/
|
|
2924
|
-
certifications:
|
|
4129
|
+
certifications: array21().of(UserCertificationSchema).default([]).label("Certifications"),
|
|
2925
4130
|
/**
|
|
2926
4131
|
* Interests information about the user.
|
|
2927
4132
|
*/
|
|
2928
|
-
interests:
|
|
4133
|
+
interests: array21().of(UserInterestSchema).optional().default([]).label("Interests"),
|
|
2929
4134
|
/**
|
|
2930
4135
|
* Languages information about the user
|
|
2931
4136
|
*/
|
|
2932
|
-
languages:
|
|
4137
|
+
languages: array21().of(UserLanguageSchema).required().default([]).label("Languages"),
|
|
2933
4138
|
/**
|
|
2934
4139
|
* Additional information about the user
|
|
2935
4140
|
*/
|
|
2936
|
-
additionalInfo:
|
|
4141
|
+
additionalInfo: array21().of(UserAdditionalInfoSchema).default([]).label("Additional Information"),
|
|
2937
4142
|
/**
|
|
2938
4143
|
* Status of the user account
|
|
2939
4144
|
*/
|
|
2940
|
-
status:
|
|
4145
|
+
status: string40().oneOf(SupportedUserStatuses).required().label("Status"),
|
|
2941
4146
|
/**
|
|
2942
4147
|
* Roles assigned to the user
|
|
2943
4148
|
*/
|
|
2944
|
-
roles:
|
|
4149
|
+
roles: array21().of(string40().oneOf(SupportedUserRoles)).required().default(DefaultUserRoles).label("Roles"),
|
|
2945
4150
|
/**
|
|
2946
4151
|
* Explicit consent to receive marketing email (newsletters, job alerts by
|
|
2947
4152
|
* broadcast, product updates). Marketing sends are HARD-GATED on
|
|
@@ -2953,18 +4158,18 @@ var UserSchema = object34({
|
|
|
2953
4158
|
* unsubscribe sets `subscribed=false` so consent and the email-service
|
|
2954
4159
|
* suppression list stay consistent. Defaults to NOT subscribed (opt-in).
|
|
2955
4160
|
*/
|
|
2956
|
-
marketingConsent:
|
|
2957
|
-
subscribed:
|
|
4161
|
+
marketingConsent: object51({
|
|
4162
|
+
subscribed: boolean22().default(false).label("Subscribed to marketing email"),
|
|
2958
4163
|
/** Epoch ms of the last consent change (opt-in or opt-out). */
|
|
2959
|
-
updatedAt:
|
|
4164
|
+
updatedAt: number20().optional().label("Consent updated at"),
|
|
2960
4165
|
/** Where the consent change originated, e.g. `onboarding`, `unsubscribe`, `settings`. */
|
|
2961
|
-
source:
|
|
4166
|
+
source: string40().optional().label("Consent source"),
|
|
2962
4167
|
/**
|
|
2963
4168
|
* How often the user receives job-alert emails. `off` disables job alerts
|
|
2964
4169
|
* only; other marketing stays governed by `subscribed`. Unset means the
|
|
2965
4170
|
* user has not chosen a cadence yet.
|
|
2966
4171
|
*/
|
|
2967
|
-
jobAlertFrequency:
|
|
4172
|
+
jobAlertFrequency: string40().oneOf(SupportedJobAlertFrequencies).optional().label("Job alert frequency")
|
|
2968
4173
|
}).default({ subscribed: false }).label("Marketing Consent"),
|
|
2969
4174
|
/**
|
|
2970
4175
|
* Per-channel toggles for NOTIFICATION email — mail about activity on the
|
|
@@ -2979,31 +4184,132 @@ var UserSchema = object34({
|
|
|
2979
4184
|
*
|
|
2980
4185
|
* Transactional mail (magic-link, verification) ignores this object entirely.
|
|
2981
4186
|
*/
|
|
2982
|
-
notificationPrefs:
|
|
2983
|
-
groupActivity:
|
|
2984
|
-
jobActivity:
|
|
2985
|
-
applicationUpdates:
|
|
2986
|
-
productUpdates:
|
|
4187
|
+
notificationPrefs: object51({
|
|
4188
|
+
groupActivity: boolean22().default(true).label("Group activity email"),
|
|
4189
|
+
jobActivity: boolean22().default(true).label("Job activity email"),
|
|
4190
|
+
applicationUpdates: boolean22().default(true).label("Application update email"),
|
|
4191
|
+
productUpdates: boolean22().default(false).label("Product update email")
|
|
2987
4192
|
}).default({
|
|
2988
4193
|
groupActivity: true,
|
|
2989
4194
|
jobActivity: true,
|
|
2990
4195
|
applicationUpdates: true,
|
|
2991
4196
|
productUpdates: false
|
|
2992
4197
|
}).label("Notification Preferences"),
|
|
4198
|
+
/**
|
|
4199
|
+
* Consent to receive WEB PUSH notifications.
|
|
4200
|
+
*
|
|
4201
|
+
* Push is a TRANSPORT, not a category: it carries the same notifications
|
|
4202
|
+
* `notificationPrefs` already governs, delivered to a device instead of an
|
|
4203
|
+
* inbox. That is why no push value is added to `NotificationType` — the user
|
|
4204
|
+
* chooses whether push is used at all, and WHICH kinds are worth pushing
|
|
4205
|
+
* stays a product decision.
|
|
4206
|
+
*
|
|
4207
|
+
* Opt-IN, and it must not be folded into `notificationPrefs`: those are
|
|
4208
|
+
* opt-OUT and read `!== false`, so an absent value means ENABLED. Applied to
|
|
4209
|
+
* push, every user who predates this field would read as having granted
|
|
4210
|
+
* permission they were never asked for. This mirrors `marketingConsent`
|
|
4211
|
+
* instead, which is the opt-in shape in this file, down to the provenance
|
|
4212
|
+
* fields.
|
|
4213
|
+
*
|
|
4214
|
+
* Consent alone never delivers anything: a push send also needs a live
|
|
4215
|
+
* subscription (see `PushSubscriptionSchema`). This flag is the user's
|
|
4216
|
+
* intent, the subscription is the browser grant, and both are required.
|
|
4217
|
+
*/
|
|
4218
|
+
pushConsent: object51({
|
|
4219
|
+
subscribed: boolean22().default(false).label("Subscribed to push notifications"),
|
|
4220
|
+
/** Epoch ms of the last consent change (opt-in or opt-out). */
|
|
4221
|
+
updatedAt: number20().optional().label("Push consent updated at"),
|
|
4222
|
+
/** Where the change originated, e.g. `onboarding`, `settings`, `prompt`. */
|
|
4223
|
+
source: string40().optional().label("Push consent source")
|
|
4224
|
+
}).default({ subscribed: false }).label("Push Consent"),
|
|
2993
4225
|
/**
|
|
2994
4226
|
* Vector embedding of the user's profile for semantic/hybrid search.
|
|
2995
4227
|
* Generated from a structured summary of skills, experience, education, etc.
|
|
2996
4228
|
* Only generated for public profiles with sufficient completeness (≥60%).
|
|
2997
4229
|
*/
|
|
2998
|
-
embedding:
|
|
2999
|
-
vector:
|
|
3000
|
-
model:
|
|
3001
|
-
}).nullable().optional().label("Embedding")
|
|
4230
|
+
embedding: object51({
|
|
4231
|
+
vector: array21(number20().required()).optional().label("Embedding vector"),
|
|
4232
|
+
model: string40().optional().label("Embedding model")
|
|
4233
|
+
}).nullable().optional().label("Embedding"),
|
|
4234
|
+
/**
|
|
4235
|
+
* Epoch ms the account was created, assigned by the storing service.
|
|
4236
|
+
*
|
|
4237
|
+
* Declared here, matching `GroupSchema`, because it is not merely stored: all
|
|
4238
|
+
* 103,608 user documents carry it, `UserFilterSchema` filters on it
|
|
4239
|
+
* (`createdAtWithinDays` / `createdAtOlderThanDays`), and the admin grid sorts
|
|
4240
|
+
* and displays it. It was absent, so a consumer deriving its row type from
|
|
4241
|
+
* this schema could not name the column it renders, and this object is
|
|
4242
|
+
* `.noUnknown().strict()` — an undeclared key is stripped on validation.
|
|
4243
|
+
*
|
|
4244
|
+
* `updatedAt` is deliberately NOT added alongside it: nothing derives from it
|
|
4245
|
+
* yet, and an optional field is only safe to add when something needs it.
|
|
4246
|
+
*/
|
|
4247
|
+
createdAt: number20().optional().label("Created At")
|
|
3002
4248
|
}).concat(UserGeneralDetailSchema).concat(UserJobPreferencesSchema).concat(UserRecruiterProfileSchema).concat(UserCoordinatorProfileSchema).noUnknown().strict().label("User Schema");
|
|
3003
4249
|
|
|
4250
|
+
// src/user/push-subscription.schema.ts
|
|
4251
|
+
import { number as number21, object as object52, string as string41 } from "yup";
|
|
4252
|
+
var PushSubscriptionSchema = object52({
|
|
4253
|
+
/**
|
|
4254
|
+
* The user this subscription belongs to, by the `shortId` every service
|
|
4255
|
+
* keys a user on, matching `UserSchema.shortId`.
|
|
4256
|
+
*
|
|
4257
|
+
* NOT the Better Auth account id. That id is meaningful only inside the auth
|
|
4258
|
+
* service, and owning these rows by one would force the service that sends
|
|
4259
|
+
* push to translate an id it has no other use for. The signed-in browser
|
|
4260
|
+
* gets its own shortId straight off the JWT claim.
|
|
4261
|
+
*
|
|
4262
|
+
* Named `userShortId` rather than `shortId` because a stored row has a
|
|
4263
|
+
* `shortId` of its OWN (see `PushSubscriptionRecord`); one field cannot be
|
|
4264
|
+
* both the row's id and its owner's.
|
|
4265
|
+
*/
|
|
4266
|
+
userShortId: string41().required().label("User Short ID"),
|
|
4267
|
+
/**
|
|
4268
|
+
* The push service URL the browser issued. Vendor-specific (FCM, Mozilla,
|
|
4269
|
+
* Apple) and effectively unbounded in length, so it carries no length cap.
|
|
4270
|
+
*
|
|
4271
|
+
* This is the IDENTITY of a subscription: it is what a re-subscribing
|
|
4272
|
+
* browser sends back unchanged, and what a 404/410 response identifies as
|
|
4273
|
+
* dead. Upserts and deletes both key on it.
|
|
4274
|
+
*/
|
|
4275
|
+
endpoint: string41().required().label("Push Endpoint"),
|
|
4276
|
+
/**
|
|
4277
|
+
* Encryption material from the browser. Web Push payloads are encrypted to
|
|
4278
|
+
* these keys, so a row missing either one cannot be delivered to at all.
|
|
4279
|
+
*/
|
|
4280
|
+
keys: object52({
|
|
4281
|
+
/** Client public key, base64url. */
|
|
4282
|
+
p256dh: string41().required().label("P256DH Key"),
|
|
4283
|
+
/** Client auth secret, base64url. */
|
|
4284
|
+
auth: string41().required().label("Auth Secret")
|
|
4285
|
+
}).required().label("Push Keys"),
|
|
4286
|
+
/**
|
|
4287
|
+
* Epoch ms the push service says the subscription expires, when it says
|
|
4288
|
+
* anything at all. Usually absent — most browsers return null — so this is
|
|
4289
|
+
* informational and never the basis for pruning. A 404/410 on send is the
|
|
4290
|
+
* only reliable death signal.
|
|
4291
|
+
*/
|
|
4292
|
+
expirationTime: number21().nullable().optional().label("Expiration Time"),
|
|
4293
|
+
/**
|
|
4294
|
+
* The subscribing browser's user agent, kept so a user can recognise their
|
|
4295
|
+
* own devices in a "where you receive notifications" list. Display only:
|
|
4296
|
+
* never parsed, and never used to decide delivery.
|
|
4297
|
+
*/
|
|
4298
|
+
userAgent: string41().optional().label("User Agent"),
|
|
4299
|
+
/** Epoch ms the subscription was first registered. */
|
|
4300
|
+
createdAt: number21().optional().label("Created At"),
|
|
4301
|
+
/**
|
|
4302
|
+
* Epoch ms this subscription was last confirmed live, refreshed when the
|
|
4303
|
+
* browser re-registers it. A subscription that stops being refreshed is a
|
|
4304
|
+
* device that stopped visiting, which is useful for pruning rows that never
|
|
4305
|
+
* fail loudly enough to be caught by a 410.
|
|
4306
|
+
*/
|
|
4307
|
+
lastSeenAt: number21().optional().label("Last Seen At")
|
|
4308
|
+
}).noUnknown().strict().label("Push Subscription Schema");
|
|
4309
|
+
|
|
3004
4310
|
// src/user/user-completeness.schema.ts
|
|
3005
|
-
import { object as
|
|
3006
|
-
var UserCompletenessSchema =
|
|
4311
|
+
import { object as object53 } from "yup";
|
|
4312
|
+
var UserCompletenessSchema = object53({
|
|
3007
4313
|
additionalInfo: CompletenessScoreSchema(0).label("Additional Info"),
|
|
3008
4314
|
// Optional
|
|
3009
4315
|
certifications: CompletenessScoreSchema(0).label("Certifications"),
|
|
@@ -3027,22 +4333,29 @@ var StudentCompletenessSchema = UserCompletenessSchema.omit([
|
|
|
3027
4333
|
export {
|
|
3028
4334
|
AnswerChoiceType,
|
|
3029
4335
|
ApplicationReceivePreference,
|
|
3030
|
-
|
|
3031
|
-
|
|
4336
|
+
BillingPeriod,
|
|
4337
|
+
BillingSubscriptionSchema,
|
|
4338
|
+
BillingTransactionSchema,
|
|
3032
4339
|
CAMPAIGN_MAX_RECIPIENTS_CEILING,
|
|
3033
|
-
|
|
4340
|
+
CONSUME_MAX_UNITS,
|
|
4341
|
+
CampaignDataSourceSchema,
|
|
3034
4342
|
CampaignEndKind,
|
|
3035
4343
|
CampaignIntervalUnit,
|
|
3036
4344
|
CampaignSchema,
|
|
3037
4345
|
CampaignStatus,
|
|
4346
|
+
CampaignStatusTransitions,
|
|
3038
4347
|
CampaignTriggerKind,
|
|
3039
4348
|
CampaignTriggerSchema,
|
|
3040
4349
|
ChoiceQuestionSchema,
|
|
4350
|
+
CityFilterSchema,
|
|
4351
|
+
CitySchema,
|
|
3041
4352
|
Common,
|
|
3042
4353
|
CompensationType,
|
|
3043
4354
|
CompletenessScoreSchema,
|
|
3044
4355
|
ContactTypes,
|
|
3045
4356
|
CoordinatorPageLinkSchema,
|
|
4357
|
+
CountryFilterSchema,
|
|
4358
|
+
CountrySchema,
|
|
3046
4359
|
DISPLAY_DATE_FORMAT,
|
|
3047
4360
|
DISPLAY_DATE_FORMAT_SHORT,
|
|
3048
4361
|
DateStringSchema,
|
|
@@ -3056,12 +4369,29 @@ export {
|
|
|
3056
4369
|
EducationSchema,
|
|
3057
4370
|
EmployeeCount,
|
|
3058
4371
|
EmploymentType,
|
|
4372
|
+
EntitlementConsumeSchema,
|
|
4373
|
+
EntitlementDenialReason,
|
|
4374
|
+
EntitlementDenialSchema,
|
|
4375
|
+
EntitlingSubscriptionStatuses,
|
|
3059
4376
|
ExperienceLevel,
|
|
3060
4377
|
FEEDBACK_MESSAGE_MAX,
|
|
4378
|
+
FeatureEntitlementSchema,
|
|
4379
|
+
FeatureKey,
|
|
4380
|
+
FeedbackFilterSchema,
|
|
3061
4381
|
FeedbackSchema,
|
|
3062
4382
|
FeedbackStatus,
|
|
3063
4383
|
FeedbackType,
|
|
4384
|
+
GEO_COLLECTIONS,
|
|
4385
|
+
GEO_SEARCH_MAX,
|
|
4386
|
+
GRANT_MAX_LIMIT,
|
|
3064
4387
|
GeneraDetailFields,
|
|
4388
|
+
GeoKind,
|
|
4389
|
+
GeoSyncPhase,
|
|
4390
|
+
GeoSyncRunSchema,
|
|
4391
|
+
GeoSyncStatus,
|
|
4392
|
+
GeoSyncTrigger,
|
|
4393
|
+
GrantType,
|
|
4394
|
+
GroupFilterSchema,
|
|
3065
4395
|
GroupManagedBy,
|
|
3066
4396
|
GroupMembershipSchema,
|
|
3067
4397
|
GroupMembershipStatus,
|
|
@@ -3073,31 +4403,41 @@ export {
|
|
|
3073
4403
|
JOB_TAXONOMY,
|
|
3074
4404
|
JobAlertFrequency,
|
|
3075
4405
|
JobCategory,
|
|
4406
|
+
JobFilterSchema,
|
|
3076
4407
|
JobIndustry,
|
|
3077
4408
|
JobSchema,
|
|
3078
4409
|
JobSearchUrgency,
|
|
3079
4410
|
JobStatus,
|
|
3080
4411
|
JobSubCategory,
|
|
3081
|
-
|
|
3082
|
-
ListFilterBranchSchema,
|
|
3083
|
-
ListFilterSchema,
|
|
4412
|
+
LanguageFilterSchema,
|
|
3084
4413
|
LocationSchema,
|
|
3085
4414
|
MIN_SALARY_LOWER_BOUND,
|
|
3086
4415
|
MIN_SALARY_UPPER_BOUND,
|
|
4416
|
+
MailingListFilterSchema,
|
|
3087
4417
|
MailingListSchema,
|
|
3088
|
-
|
|
4418
|
+
MoneySchema,
|
|
4419
|
+
NotificationType,
|
|
3089
4420
|
OccurrenceStatus,
|
|
3090
4421
|
PREFILL_MIRRORED_KEYS,
|
|
3091
4422
|
PRIVATE_PROFILE_FIELDS,
|
|
4423
|
+
PaddleEnvironment,
|
|
4424
|
+
PaddlePriceIdsSchema,
|
|
4425
|
+
PageFilterSchema,
|
|
3092
4426
|
PageSchema,
|
|
3093
4427
|
PageStatus,
|
|
3094
4428
|
PageType,
|
|
3095
4429
|
PaginationSchema,
|
|
4430
|
+
PlanGrantSchema,
|
|
4431
|
+
PlanKind,
|
|
4432
|
+
PlanSchema,
|
|
4433
|
+
PlanStatus,
|
|
3096
4434
|
PostSchema,
|
|
3097
4435
|
PostStatus,
|
|
3098
4436
|
ProficiencyLevel,
|
|
4437
|
+
PushSubscriptionSchema,
|
|
3099
4438
|
QuestionSchema,
|
|
3100
4439
|
QuestionType,
|
|
4440
|
+
QuotaPeriod,
|
|
3101
4441
|
ReadAndAcknowledgeQuestionSchema,
|
|
3102
4442
|
RecruiterPageLinkSchema,
|
|
3103
4443
|
ReferralSource,
|
|
@@ -3109,15 +4449,18 @@ export {
|
|
|
3109
4449
|
SavedSearchSchema,
|
|
3110
4450
|
SavedSearchStatus,
|
|
3111
4451
|
SignupContextSource,
|
|
4452
|
+
SkillFilterSchema,
|
|
3112
4453
|
SkillSchema,
|
|
3113
4454
|
SocialAccount,
|
|
3114
4455
|
SocialAccountSchema,
|
|
4456
|
+
StateFilterSchema,
|
|
4457
|
+
StateSchema,
|
|
3115
4458
|
StudentCompletenessSchema,
|
|
3116
4459
|
StudyType,
|
|
4460
|
+
SubscriptionStatus,
|
|
3117
4461
|
SupportedAnswerChoiceTypes,
|
|
3118
4462
|
SupportedApplicationReceivePreferences,
|
|
3119
|
-
|
|
3120
|
-
SupportedCampaignAnchorFields,
|
|
4463
|
+
SupportedBillingPeriods,
|
|
3121
4464
|
SupportedCampaignEndKinds,
|
|
3122
4465
|
SupportedCampaignIntervalUnits,
|
|
3123
4466
|
SupportedCampaignStatuses,
|
|
@@ -3127,22 +4470,37 @@ export {
|
|
|
3127
4470
|
SupportedEducationLevels,
|
|
3128
4471
|
SupportedEmployeeCounts,
|
|
3129
4472
|
SupportedEmploymentTypes,
|
|
4473
|
+
SupportedEntitlementDenialReasons,
|
|
3130
4474
|
SupportedExperienceLevels,
|
|
4475
|
+
SupportedFeatureKeys,
|
|
3131
4476
|
SupportedFeedbackStatuses,
|
|
3132
4477
|
SupportedFeedbackTypes,
|
|
4478
|
+
SupportedGeoKinds,
|
|
4479
|
+
SupportedGeoSyncPhases,
|
|
4480
|
+
SupportedGeoSyncStatuses,
|
|
4481
|
+
SupportedGeoSyncTriggers,
|
|
4482
|
+
SupportedGrantTypes,
|
|
4483
|
+
SupportedGroupManagedBy,
|
|
4484
|
+
SupportedGroupMembershipStatuses,
|
|
4485
|
+
SupportedGroupStatuses,
|
|
4486
|
+
SupportedGroupVisibilities,
|
|
3133
4487
|
SupportedJobAlertFrequencies,
|
|
3134
4488
|
SupportedJobCategories,
|
|
3135
4489
|
SupportedJobIndustries,
|
|
3136
4490
|
SupportedJobSearchUrgencies,
|
|
3137
4491
|
SupportedJobStatuses,
|
|
3138
4492
|
SupportedJobSubCategories,
|
|
3139
|
-
|
|
4493
|
+
SupportedNotificationTypes,
|
|
3140
4494
|
SupportedOccurrenceStatuses,
|
|
4495
|
+
SupportedPaddleEnvironments,
|
|
3141
4496
|
SupportedPageStatuses,
|
|
3142
4497
|
SupportedPageTypes,
|
|
4498
|
+
SupportedPlanKinds,
|
|
4499
|
+
SupportedPlanStatuses,
|
|
3143
4500
|
SupportedPostStatuses,
|
|
3144
4501
|
SupportedProficiencyLevels,
|
|
3145
4502
|
SupportedQuestionTypes,
|
|
4503
|
+
SupportedQuotaPeriods,
|
|
3146
4504
|
SupportedReferralSources,
|
|
3147
4505
|
SupportedReportReasons,
|
|
3148
4506
|
SupportedResourceTypes,
|
|
@@ -3151,17 +4509,26 @@ export {
|
|
|
3151
4509
|
SupportedSignupContextSources,
|
|
3152
4510
|
SupportedSocialAccounts,
|
|
3153
4511
|
SupportedStudyTypes,
|
|
4512
|
+
SupportedSubscriptionStatuses,
|
|
4513
|
+
SupportedTemplateVariableBindingKinds,
|
|
4514
|
+
SupportedTransactionStatuses,
|
|
3154
4515
|
SupportedUserProfileVisibilities,
|
|
3155
4516
|
SupportedUserRoles,
|
|
3156
4517
|
SupportedUserStatuses,
|
|
3157
4518
|
SupportedWorkModes,
|
|
3158
4519
|
TAXONOMY_LABELS,
|
|
4520
|
+
TemplateVariableBindingKind,
|
|
4521
|
+
TemplateVariableBindingSchema,
|
|
4522
|
+
TerminalCampaignStatuses,
|
|
4523
|
+
TerminalGeoSyncStatuses,
|
|
3159
4524
|
TermsAcceptedSchema,
|
|
4525
|
+
TransactionStatus,
|
|
3160
4526
|
UserAdditionalInfoSchema,
|
|
3161
4527
|
UserCertificationSchema,
|
|
3162
4528
|
UserCompletenessSchema,
|
|
3163
4529
|
UserCoordinatorProfileSchema,
|
|
3164
4530
|
UserDetailType,
|
|
4531
|
+
UserFilterSchema,
|
|
3165
4532
|
UserGeneralDetailSchema,
|
|
3166
4533
|
UserIdAndCreatedAtSchema,
|
|
3167
4534
|
UserInterestSchema,
|
|
@@ -3176,12 +4543,24 @@ export {
|
|
|
3176
4543
|
UserStatus,
|
|
3177
4544
|
WorkExperienceSchema,
|
|
3178
4545
|
WorkMode,
|
|
4546
|
+
canTransitionCampaign,
|
|
3179
4547
|
categoryOfIndustry,
|
|
3180
4548
|
categoryOfSubCategory,
|
|
4549
|
+
codeTokens,
|
|
4550
|
+
conjunction,
|
|
4551
|
+
currencyBound,
|
|
3181
4552
|
dateString,
|
|
4553
|
+
days,
|
|
3182
4554
|
deriveIndustry,
|
|
4555
|
+
enumFacet,
|
|
4556
|
+
epoch,
|
|
3183
4557
|
getSchemaByQuestion,
|
|
3184
4558
|
industriesOfCategory,
|
|
3185
4559
|
industryOfSubCategory,
|
|
3186
|
-
|
|
4560
|
+
months,
|
|
4561
|
+
percent,
|
|
4562
|
+
qualified,
|
|
4563
|
+
rejectUnknownKeys,
|
|
4564
|
+
subCategoriesOfIndustry,
|
|
4565
|
+
tokens
|
|
3187
4566
|
};
|