@thejob/schema 2.1.9 → 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.
@@ -118,16 +118,28 @@ export enum JobAlertFrequency {
118
118
  export const SupportedJobAlertFrequencies = Object.values(JobAlertFrequency);
119
119
 
120
120
  /**
121
- * Channels of NOTIFICATION email — mail about activity on the user's own
122
- * account (a group invite, a job of theirs expiring). Distinct from marketing:
123
- * notifications are opt-OUT (default on, see `notificationPrefs`), marketing is
124
- * opt-IN (`marketingConsent.subscribed`). Transactional mail (magic-link,
125
- * verification) is governed by neither and always sends.
121
+ * KINDS of notification, by subject matter — what a message is ABOUT, not how
122
+ * it reaches the user. A notification of a given kind may be delivered over any
123
+ * transport (email, web push, and later whatsapp or in-app); this enum is the
124
+ * user's per-subject mute switch and applies across all of them.
126
125
  *
127
- * Deliberately coarse: four channels a user will actually reason about, rather
126
+ * Named `NotificationType` rather than `...Channel` precisely because "channel"
127
+ * reads as a delivery route. Muting `groupActivity` mutes group activity
128
+ * everywhere, not merely in the mail.
129
+ *
130
+ * Distinct from marketing: notifications are opt-OUT (default on, see
131
+ * `notificationPrefs`), marketing is opt-IN (`marketingConsent.subscribed`).
132
+ * Transactional mail (magic-link, verification) is governed by neither and
133
+ * always sends.
134
+ *
135
+ * Deliberately coarse: four kinds a user will actually reason about, rather
128
136
  * than one toggle per email template.
137
+ *
138
+ * The VALUES are stored in Mongo as the keys of `notificationPrefs`, so they
139
+ * are data, not style. Renaming the enum is a type-level change; renaming a
140
+ * value would be a data migration.
129
141
  */
130
- export enum NotificationChannel {
142
+ export enum NotificationType {
131
143
  /** Group invites, join requests, membership approved/rejected. */
132
144
  GroupActivity = "groupActivity",
133
145
  /** Activity on jobs the user posted, e.g. a listing expiring. */
@@ -138,7 +150,7 @@ export enum NotificationChannel {
138
150
  ProductUpdates = "productUpdates",
139
151
  }
140
152
 
141
- export const SupportedNotificationChannels = Object.values(NotificationChannel);
153
+ export const SupportedNotificationTypes = Object.values(NotificationType);
142
154
 
143
155
  /** ISO-4217 currency codes accepted for the salary preference (extend as needed). */
144
156
  export const SupportedSalaryCurrencies = ["USD", "EUR", "GBP", "SEK", "INR"] as const;
@@ -168,6 +168,36 @@ export const UserSchema = object({
168
168
  })
169
169
  .label("Notification Preferences"),
170
170
 
171
+ /**
172
+ * Consent to receive WEB PUSH notifications.
173
+ *
174
+ * Push is a TRANSPORT, not a category: it carries the same notifications
175
+ * `notificationPrefs` already governs, delivered to a device instead of an
176
+ * inbox. That is why no push value is added to `NotificationType` — the user
177
+ * chooses whether push is used at all, and WHICH kinds are worth pushing
178
+ * stays a product decision.
179
+ *
180
+ * Opt-IN, and it must not be folded into `notificationPrefs`: those are
181
+ * opt-OUT and read `!== false`, so an absent value means ENABLED. Applied to
182
+ * push, every user who predates this field would read as having granted
183
+ * permission they were never asked for. This mirrors `marketingConsent`
184
+ * instead, which is the opt-in shape in this file, down to the provenance
185
+ * fields.
186
+ *
187
+ * Consent alone never delivers anything: a push send also needs a live
188
+ * subscription (see `PushSubscriptionSchema`). This flag is the user's
189
+ * intent, the subscription is the browser grant, and both are required.
190
+ */
191
+ pushConsent: object({
192
+ subscribed: boolean().default(false).label("Subscribed to push notifications"),
193
+ /** Epoch ms of the last consent change (opt-in or opt-out). */
194
+ updatedAt: number().optional().label("Push consent updated at"),
195
+ /** Where the change originated, e.g. `onboarding`, `settings`, `prompt`. */
196
+ source: string().optional().label("Push consent source"),
197
+ })
198
+ .default({ subscribed: false })
199
+ .label("Push Consent"),
200
+
171
201
  /**
172
202
  * Vector embedding of the user's profile for semantic/hybrid search.
173
203
  * Generated from a structured summary of skills, experience, education, etc.
@@ -180,6 +210,21 @@ export const UserSchema = object({
180
210
  .nullable()
181
211
  .optional()
182
212
  .label("Embedding"),
213
+
214
+ /**
215
+ * Epoch ms the account was created, assigned by the storing service.
216
+ *
217
+ * Declared here, matching `GroupSchema`, because it is not merely stored: all
218
+ * 103,608 user documents carry it, `UserFilterSchema` filters on it
219
+ * (`createdAtWithinDays` / `createdAtOlderThanDays`), and the admin grid sorts
220
+ * and displays it. It was absent, so a consumer deriving its row type from
221
+ * this schema could not name the column it renders, and this object is
222
+ * `.noUnknown().strict()` — an undeclared key is stripped on validation.
223
+ *
224
+ * `updatedAt` is deliberately NOT added alongside it: nothing derives from it
225
+ * yet, and an optional field is only safe to add when something needs it.
226
+ */
227
+ createdAt: number().optional().label("Created At"),
183
228
  })
184
229
  .concat(UserGeneralDetailSchema)
185
230
  .concat(UserJobPreferencesSchema)
@@ -4,6 +4,41 @@ import type { LocationSchema } from "../location/location.schema.js";
4
4
  import type { EducationSchema } from "./education.schema.js";
5
5
  import type { UserGeneralDetailSchema } from "./general-detail.schema.js";
6
6
  import type { WorkExperienceSchema } from "./work-experience.schema.js";
7
+ import type { PushSubscriptionSchema } from "./push-subscription.schema.js";
8
+ import type { UserSchema } from "./user.schema.js";
9
+
10
+ /**
11
+ * A user, as this package defines one.
12
+ *
13
+ * DERIVED from `UserSchema`, never hand-written, so it cannot drift from the
14
+ * shape the schema validates. Consumers project subsets of it (a listing
15
+ * returns a handful of fields, a profile read returns more), which is why
16
+ * every field a projection may omit is optional at the schema level.
17
+ *
18
+ * `shortId` and `slug` are assigned by the STORING service and are not part of
19
+ * the validated input shape, so they are added here rather than in the schema.
20
+ * `shortId` is the identifier to use everywhere; `authAccountId` is Better
21
+ * Auth's id and belongs only in auth-related paths.
22
+ */
23
+ export type User = InferType<typeof UserSchema> & {
24
+ /** Public identifier, server-assigned. The id to key on. */
25
+ shortId?: string;
26
+ /** Public profile handle, server-assigned. */
27
+ slug?: string;
28
+ };
29
+
30
+ /**
31
+ * A stored push subscription, with the identifier the storing service assigns.
32
+ *
33
+ * Same split as `User`: `shortId` is server-assigned and so is not part of the
34
+ * validated input shape. Note that the identifier consumers actually key on
35
+ * here is `endpoint`, not `shortId` — it is what the browser re-sends on
36
+ * re-subscribe and what a dead-subscription response names.
37
+ */
38
+ export type PushSubscriptionRecord = InferType<typeof PushSubscriptionSchema> & {
39
+ /** Public identifier, server-assigned. */
40
+ shortId?: string;
41
+ };
7
42
 
8
43
  export type UserProfileOverview = {
9
44
  name: InferType<typeof UserGeneralDetailSchema>["name"];