@thejob/schema 2.1.3 → 2.1.5
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/settings.local.json +5 -5
- package/CLAUDE.md +85 -0
- package/dist/index.cjs +234 -138
- package/dist/index.d.cts +203 -129
- package/dist/index.d.ts +203 -129
- package/dist/index.js +198 -107
- package/package.json +1 -1
- package/src/index.ts +6 -0
- package/src/saved-search/saved-search.constant.ts +17 -0
- package/src/saved-search/saved-search.schema.ts +84 -0
- package/src/user/general-detail.schema.ts +27 -5
- package/src/user/user.constant.ts +20 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { number, object, string } from "yup";
|
|
2
|
+
import { JobAlertFrequency, SupportedJobAlertFrequencies } from "../user/user.constant.js";
|
|
3
|
+
import { SavedSearchStatus, SupportedSavedSearchStatuses } from "./saved-search.constant.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A search a user asked to be kept, and optionally to be alerted about.
|
|
7
|
+
*
|
|
8
|
+
* Two jobs in one document, deliberately: the query the user built, and the
|
|
9
|
+
* cadence at which they want it re-run for them. Splitting them would mean a
|
|
10
|
+
* user with three saved searches and one alert preference has no way to say
|
|
11
|
+
* which search the alert is for.
|
|
12
|
+
*
|
|
13
|
+
* The stored query mirrors what `GET /jobs/search-v3` accepts — a free-text
|
|
14
|
+
* `search` plus a `filter` map — so replaying a saved search is running the same
|
|
15
|
+
* search the user originally ran, not a second, subtly different implementation
|
|
16
|
+
* that drifts from it.
|
|
17
|
+
*/
|
|
18
|
+
export const SavedSearchSchema = object({
|
|
19
|
+
/** Public identifier. Server-assigned. */
|
|
20
|
+
shortId: string().trim().label("Short Id"),
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Owner's Better Auth account id. Server-assigned from the verified token,
|
|
24
|
+
* never accepted from a request body.
|
|
25
|
+
*/
|
|
26
|
+
ownerUserId: string().trim().label("Owner"),
|
|
27
|
+
|
|
28
|
+
/** What the user calls this search, e.g. "Remote React roles". */
|
|
29
|
+
label: string().trim().max(120).required().label("Label"),
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The free-text query. Optional because a search can be filters-only ("all
|
|
33
|
+
* remote jobs in Sweden"), which is a legitimate alert to want.
|
|
34
|
+
*/
|
|
35
|
+
search: string().trim().max(500).optional().label("Search Text"),
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Field filters, in the same `{ field: values }` shape the search endpoint
|
|
39
|
+
* parses from `filter[field]=a,b`. Held as a free-form object rather than a
|
|
40
|
+
* closed schema because the job facets it addresses live in jobs-service and
|
|
41
|
+
* change independently; validating them here would put this package in the
|
|
42
|
+
* business of tracking that vocabulary.
|
|
43
|
+
*/
|
|
44
|
+
filter: object().optional().default({}).label("Filters"),
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* How often to mail matches. `off` keeps the search saved but silent, which is
|
|
48
|
+
* the difference between "I don't want mail" and "delete my search".
|
|
49
|
+
*
|
|
50
|
+
* Reuses the cadence enum from `marketingConsent.jobAlertFrequency` rather
|
|
51
|
+
* than defining a second one: two vocabularies for the same concept is how
|
|
52
|
+
* they drift.
|
|
53
|
+
*/
|
|
54
|
+
frequency: string()
|
|
55
|
+
.oneOf(SupportedJobAlertFrequencies)
|
|
56
|
+
.default(JobAlertFrequency.Off)
|
|
57
|
+
.label("Alert Frequency"),
|
|
58
|
+
|
|
59
|
+
status: string()
|
|
60
|
+
.oneOf(SupportedSavedSearchStatuses)
|
|
61
|
+
.default(SavedSearchStatus.Active)
|
|
62
|
+
.label("Status"),
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Epoch ms of the last alert sent for this search. The matching sweep reads it
|
|
66
|
+
* to decide whether the cadence is due, and writes it only AFTER a successful
|
|
67
|
+
* publish, so a failed run retries next tick rather than silently skipping a
|
|
68
|
+
* period.
|
|
69
|
+
*/
|
|
70
|
+
lastNotifiedAt: number().optional().label("Last Notified At"),
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Epoch ms high-water mark: only jobs created after this are "new" for the
|
|
74
|
+
* next alert. Kept separate from `lastNotifiedAt` because a run that finds no
|
|
75
|
+
* matches should still advance what counts as new, without implying mail was
|
|
76
|
+
* sent.
|
|
77
|
+
*/
|
|
78
|
+
lastMatchedAt: number().optional().label("Last Matched At"),
|
|
79
|
+
})
|
|
80
|
+
// `createdAt`/`updatedAt` are deliberately absent, matching every other schema
|
|
81
|
+
// here: audit timestamps are the storing service's concern, not part of the
|
|
82
|
+
// shape a client sends or validates.
|
|
83
|
+
.noUnknown()
|
|
84
|
+
.strict();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { object, string } from "yup";
|
|
1
|
+
import { number, object, string } from "yup";
|
|
2
2
|
import { SupportedExperienceLevels } from "../common/common.constant.js";
|
|
3
3
|
import { LocationSchema } from "../location/location.schema.js";
|
|
4
4
|
import {
|
|
5
|
+
SupportedSignupContextSources,
|
|
5
6
|
SupportedUserProfileVisibilities,
|
|
6
7
|
UserProfileVisibility,
|
|
7
8
|
} from "./user.constant.js";
|
|
@@ -30,14 +31,35 @@ export const UserGeneralDetailSchema = object({
|
|
|
30
31
|
.required()
|
|
31
32
|
.label("Experience level"),
|
|
32
33
|
location: LocationSchema.required().label("Location"),
|
|
33
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Where the account was created, captured once and never rewritten.
|
|
36
|
+
*
|
|
37
|
+
* Server-owned: listed in user-service's SERVER_CONTROLLED_FIELDS and absent
|
|
38
|
+
* from every user-writable schema, so a client cannot set or amend it. That
|
|
39
|
+
* immutability is the point — this answers "which market/jurisdiction governed
|
|
40
|
+
* this user at sign-up?", which a field the user can PATCH could never do.
|
|
41
|
+
*
|
|
42
|
+
* `source` records HOW the value was obtained, because most of it is inferred
|
|
43
|
+
* rather than measured, and an audit has to be able to tell those apart:
|
|
44
|
+
* - `cf-header` measured from Cloudflare's `cf-ipcountry` at sign-up
|
|
45
|
+
* - `location` derived from the user's self-entered profile location
|
|
46
|
+
* - `default` assumed (no signal available); presumption, not evidence
|
|
47
|
+
*/
|
|
48
|
+
signupContext: object()
|
|
34
49
|
.shape({
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
// ISO 3166-1 alpha-2, uppercase — the shape `cf-ipcountry` returns, so
|
|
51
|
+
// measured and inferred values are directly comparable.
|
|
52
|
+
country: string().trim().uppercase().length(2).required().label("Signup Country"),
|
|
53
|
+
locale: string().trim().required().label("Signup Locale"),
|
|
54
|
+
source: string()
|
|
55
|
+
.oneOf(SupportedSignupContextSources)
|
|
56
|
+
.required()
|
|
57
|
+
.label("Signup Context Source"),
|
|
58
|
+
at: number().required().label("Signup Context At"),
|
|
37
59
|
})
|
|
38
60
|
.optional()
|
|
39
61
|
.default(undefined)
|
|
40
|
-
.label("
|
|
62
|
+
.label("Signup Context"),
|
|
41
63
|
profileVisibility: string()
|
|
42
64
|
.oneOf(SupportedUserProfileVisibilities)
|
|
43
65
|
.default(UserProfileVisibility.Public)
|
|
@@ -34,6 +34,26 @@ export const SupportedUserProfileVisibilities = Object.values(
|
|
|
34
34
|
UserProfileVisibility,
|
|
35
35
|
);
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* How a user's `signupContext` was obtained. Stored on the document, so these
|
|
39
|
+
* strings are data: lowercase, and a rename is a migration.
|
|
40
|
+
*
|
|
41
|
+
* The distinction is deliberate. Only `CfHeader` is measured; the other two are
|
|
42
|
+
* inferred, and `Default` is a bare presumption with no signal behind it at all.
|
|
43
|
+
* Collapsing them would make an assumed country indistinguishable from an
|
|
44
|
+
* observed one, which is precisely what a compliance record must not do.
|
|
45
|
+
*/
|
|
46
|
+
export enum SignupContextSource {
|
|
47
|
+
/** Measured from Cloudflare's `cf-ipcountry` header at sign-up. */
|
|
48
|
+
CfHeader = "cf-header",
|
|
49
|
+
/** Derived from the user's self-entered profile location (backfill). */
|
|
50
|
+
Location = "location",
|
|
51
|
+
/** Assumed; no signal was available. A presumption, not evidence. */
|
|
52
|
+
Default = "default",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const SupportedSignupContextSources = Object.values(SignupContextSource);
|
|
56
|
+
|
|
37
57
|
export enum UserDetailType {
|
|
38
58
|
Overview = "overview",
|
|
39
59
|
AdditionalInfo = "additionalInfo",
|