@thejob/schema 2.1.2 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +5 -5
- package/CLAUDE.md +62 -0
- package/dist/index.cjs +235 -132
- package/dist/index.d.cts +187 -102
- package/dist/index.d.ts +187 -102
- package/dist/index.js +203 -105
- 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/user.constant.ts +23 -0
- package/src/user/user.schema.ts +27 -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();
|
|
@@ -97,6 +97,29 @@ export enum JobAlertFrequency {
|
|
|
97
97
|
|
|
98
98
|
export const SupportedJobAlertFrequencies = Object.values(JobAlertFrequency);
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Channels of NOTIFICATION email — mail about activity on the user's own
|
|
102
|
+
* account (a group invite, a job of theirs expiring). Distinct from marketing:
|
|
103
|
+
* notifications are opt-OUT (default on, see `notificationPrefs`), marketing is
|
|
104
|
+
* opt-IN (`marketingConsent.subscribed`). Transactional mail (magic-link,
|
|
105
|
+
* verification) is governed by neither and always sends.
|
|
106
|
+
*
|
|
107
|
+
* Deliberately coarse: four channels a user will actually reason about, rather
|
|
108
|
+
* than one toggle per email template.
|
|
109
|
+
*/
|
|
110
|
+
export enum NotificationChannel {
|
|
111
|
+
/** Group invites, join requests, membership approved/rejected. */
|
|
112
|
+
GroupActivity = "groupActivity",
|
|
113
|
+
/** Activity on jobs the user posted, e.g. a listing expiring. */
|
|
114
|
+
JobActivity = "jobActivity",
|
|
115
|
+
/** Status changes on jobs the user applied to. */
|
|
116
|
+
ApplicationUpdates = "applicationUpdates",
|
|
117
|
+
/** Product announcements and feature news. */
|
|
118
|
+
ProductUpdates = "productUpdates",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const SupportedNotificationChannels = Object.values(NotificationChannel);
|
|
122
|
+
|
|
100
123
|
/** ISO-4217 currency codes accepted for the salary preference (extend as needed). */
|
|
101
124
|
export const SupportedSalaryCurrencies = ["USD", "EUR", "GBP", "SEK", "INR"] as const;
|
|
102
125
|
export type SupportedSalaryCurrency = (typeof SupportedSalaryCurrencies)[number];
|
package/src/user/user.schema.ts
CHANGED
|
@@ -141,6 +141,33 @@ export const UserSchema = object({
|
|
|
141
141
|
.default({ subscribed: false })
|
|
142
142
|
.label("Marketing Consent"),
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Per-channel toggles for NOTIFICATION email — mail about activity on the
|
|
146
|
+
* user's own account (a group invite, a job of theirs expiring).
|
|
147
|
+
*
|
|
148
|
+
* Deliberately separate from `marketingConsent`, which they must NOT be
|
|
149
|
+
* collapsed into: the two have different legal footing, which the
|
|
150
|
+
* email-service footer rules already encode. Notifications are opt-OUT
|
|
151
|
+
* (default `true`) because a user who declined marketing still needs to hear
|
|
152
|
+
* that their group invite arrived; marketing stays opt-IN. `productUpdates`
|
|
153
|
+
* is the exception — it is promotional in character, so it defaults off.
|
|
154
|
+
*
|
|
155
|
+
* Transactional mail (magic-link, verification) ignores this object entirely.
|
|
156
|
+
*/
|
|
157
|
+
notificationPrefs: object({
|
|
158
|
+
groupActivity: boolean().default(true).label("Group activity email"),
|
|
159
|
+
jobActivity: boolean().default(true).label("Job activity email"),
|
|
160
|
+
applicationUpdates: boolean().default(true).label("Application update email"),
|
|
161
|
+
productUpdates: boolean().default(false).label("Product update email"),
|
|
162
|
+
})
|
|
163
|
+
.default({
|
|
164
|
+
groupActivity: true,
|
|
165
|
+
jobActivity: true,
|
|
166
|
+
applicationUpdates: true,
|
|
167
|
+
productUpdates: false,
|
|
168
|
+
})
|
|
169
|
+
.label("Notification Preferences"),
|
|
170
|
+
|
|
144
171
|
/**
|
|
145
172
|
* Vector embedding of the user's profile for semantic/hybrid search.
|
|
146
173
|
* Generated from a structured summary of skills, experience, education, etc.
|