@thejob/schema 2.0.1 → 2.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thejob/schema",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -1,3 +1,33 @@
1
+ /**
2
+ * Common option keys shared across schemas. `Yes`/`No` are used by acknowledge
3
+ * questions; `Skill`/`EducationLevel`/`ExperienceLevel`/etc. tag where a choice
4
+ * question sources its options from. Migrated from schema v1.
5
+ */
6
+ export enum Common {
7
+ Yes = "yes",
8
+ No = "no",
9
+ DateRangeOption = "date_range_option",
10
+ ExperienceLevel = "experience_level",
11
+ Company = "company",
12
+ EmploymentType = "employment_type",
13
+ WorkMode = "work_mode",
14
+ Location = "location",
15
+ CareerLevel = "career_level",
16
+ Category = "category",
17
+ Designation = "designation",
18
+ EducationLevel = "education_level",
19
+ Language = "language",
20
+ Skill = "skill",
21
+ }
22
+
23
+ // Common constants
24
+ export const EMPTY_STRING = "eMpTyStRiNg";
25
+ // DayJS formats: https://day.js.org/docs/en/display/format
26
+ export const SYSTEM_DATE_FORMAT = "YYYY-MM-DD";
27
+ export const DISPLAY_DATE_FORMAT = "LL";
28
+ export const DISPLAY_DATE_FORMAT_SHORT = "MMMM, YYYY";
29
+ export const SITEMAP_FORMAT = "YYYY-MM-DDThh:mm:ssZ";
30
+
1
31
  // Education-related enums
2
32
  export enum StudyType {
3
33
  FullTime = "full_time",
@@ -1,4 +1,5 @@
1
1
  import { boolean, number, object, string } from "yup";
2
+ import type { InferType } from "yup";
2
3
 
3
4
  export const TermsAcceptedSchema = object({
4
5
  termsAccepted: boolean()
@@ -26,3 +27,16 @@ export const CompletenessScoreSchema = (minScore: number = 50) =>
26
27
  totalSteps: number().min(0).default(0).label("Total Steps"),
27
28
  completedSteps: number().min(0).default(0).label("Completed Steps"),
28
29
  });
30
+
31
+ /**
32
+ * Tracks a user reference plus when the relation was created (bookmarks,
33
+ * applicants, reports on a job). The v1 `userId` used `.objectId()`; plain
34
+ * `string()` in v2.
35
+ */
36
+ export const UserIdAndCreatedAtSchema = object().shape({
37
+ userId: string().required().label("User Id"),
38
+ createdAt: string().required().label("Created At"),
39
+ });
40
+ export type TUserIdAndCreatedAtSchema = InferType<
41
+ typeof UserIdAndCreatedAtSchema
42
+ >;
package/src/index.ts CHANGED
@@ -20,6 +20,17 @@ export * from "./extensions/date-string.extension.js";
20
20
  export * from "./group/group.schema.js";
21
21
  export * from "./group/group.constant.js";
22
22
 
23
+ /**
24
+ * Job schemas and constants.
25
+ */
26
+ export * from "./job/job.schema.js";
27
+ export * from "./job/job.constant.js";
28
+
29
+ /**
30
+ * Question schemas and constants (job questionnaire).
31
+ */
32
+ export * from "./question/index.js";
33
+
23
34
  /**
24
35
  * Location schemas and constants.
25
36
  */
@@ -0,0 +1,66 @@
1
+ export enum JobStatus {
2
+ Published = "published",
3
+ Draft = "draft",
4
+ Deleted = "deleted",
5
+ Closed = "closed",
6
+ Expired = "expired",
7
+ }
8
+
9
+ export const SupportedJobStatuses = Object.values(JobStatus);
10
+
11
+ export enum EmploymentType {
12
+ Fulltime = "full_time",
13
+ PartTime = "part_time",
14
+ Temporary = "temporary",
15
+ Contract = "contract",
16
+ Internship = "internship",
17
+ Freelance = "freelance",
18
+ Volunteer = "volunteer",
19
+ PerDiem = "per_diem",
20
+ }
21
+
22
+ export const SupportedEmploymentTypes = Object.values(EmploymentType);
23
+
24
+ export enum WorkMode {
25
+ OnSite = "on_site",
26
+ Hybrid = "hybrid",
27
+ Remote = "remote",
28
+ }
29
+
30
+ export const SupportedWorkModes = Object.values(WorkMode);
31
+
32
+ export enum CompensationType {
33
+ Hourly = "hourly",
34
+ Weekly = "weekly",
35
+ Monthly = "monthly",
36
+ Yearly = "yearly",
37
+ OneTime = "one-time",
38
+ Other = "other",
39
+ }
40
+
41
+ export const SupportedCompensationTypes = Object.values(CompensationType);
42
+
43
+ export enum EmployeeCount {
44
+ _1_2 = "1_2_employees",
45
+ _3_10 = "3_10_employees",
46
+ _11_100 = "11_100_employees",
47
+ _101_1000 = "101_1000_employees",
48
+ _1001_5000 = "1001_5000_employees",
49
+ _5001_10000 = "5001_10000_employees",
50
+ _10001_Plus = "10001_plus_employees",
51
+ }
52
+
53
+ export const SupportedEmployeeCounts = Object.values(EmployeeCount);
54
+
55
+ export enum ApplicationReceivePreference {
56
+ Inbox = "inbox",
57
+ Email = "email",
58
+ ExternalWebsite = "external_website",
59
+ }
60
+
61
+ export const SupportedApplicationReceivePreferences = Object.values(
62
+ ApplicationReceivePreference,
63
+ );
64
+
65
+ // Note: ExperienceLevel lives in common.constant.ts (single source of truth);
66
+ // it is intentionally not re-declared here.
@@ -0,0 +1,256 @@
1
+ import { array, boolean, lazy, mixed, number, object, string } from "yup";
2
+ import type { InferType, ObjectSchema } from "yup";
3
+ import {
4
+ DbDefaultSchema,
5
+ UserIdAndCreatedAtSchema,
6
+ } from "../common/common.schema.js";
7
+ import {
8
+ SupportedEducationLevels,
9
+ SupportedExperienceLevels,
10
+ SYSTEM_DATE_FORMAT,
11
+ } from "../common/common.constant.js";
12
+ import { DurationSchema } from "../duration/duration.schema.js";
13
+ import { LocationSchema } from "../location/location.schema.js";
14
+ import { PageSchema } from "../page/page.schema.js";
15
+ import { SkillSchema } from "../skill/skill.schema.js";
16
+ import { QuestionType } from "../question/index.js";
17
+ import type {
18
+ TChoiceQuestionSchema,
19
+ TInputQuestionSchema,
20
+ TQuestionSchema,
21
+ TReadAndAcknowledgeQuestionSchema,
22
+ } from "../question/index.js";
23
+ import { getSchemaByQuestion } from "../question/question.utils.js";
24
+ import {
25
+ ApplicationReceivePreference,
26
+ JobStatus,
27
+ SupportedCompensationTypes,
28
+ SupportedJobStatuses,
29
+ } from "./job.constant.js";
30
+
31
+ export const JobSchema = object({
32
+ /** Unique id usable by external systems to reference the job. */
33
+ uniqueId: string().optional().label("Unique ID"),
34
+
35
+ /** Job title shown in listings and search. */
36
+ headline: string().required().min(5).max(150).label("Headline"),
37
+
38
+ /** User-friendly, unique URL identifier. */
39
+ slug: string().required().max(250).label("Slug"),
40
+
41
+ /** How applications are received (inbox vs external website). */
42
+ applicationReceivePreference: mixed<ApplicationReceivePreference>()
43
+ .oneOf([
44
+ ApplicationReceivePreference.Inbox,
45
+ ApplicationReceivePreference.ExternalWebsite,
46
+ ])
47
+ .required()
48
+ .label("Preference"),
49
+
50
+ /** Required when applicationReceivePreference is ExternalWebsite. */
51
+ externalApplyLink: string().when("applicationReceivePreference", {
52
+ is: (value: ApplicationReceivePreference) =>
53
+ value === ApplicationReceivePreference.ExternalWebsite,
54
+ then: (schema) => schema.url().required().label("Apply for Job link"),
55
+ otherwise: (schema) => schema.optional(),
56
+ }),
57
+
58
+ /** Company the job belongs to (subset of PageSchema). */
59
+ company: PageSchema.pick(["name", "slug", "type", "logo"])
60
+ .deepPartial()
61
+ .concat(PageSchema.pick(["name", "type"]))
62
+ .required()
63
+ .label("Company"),
64
+
65
+ /** @deprecated kept for backwards compatibility. */
66
+ designation: string().optional().nullable().label("Designation"),
67
+
68
+ employmentType: string().required().label("Employment type"),
69
+
70
+ workMode: string().required().label("Work mode"),
71
+
72
+ skills: array()
73
+ .of(SkillSchema.pick(["name", "logo"]).required().label("Skill"))
74
+ .nullable()
75
+ .optional()
76
+ .label("Skills"),
77
+
78
+ bookmarks: array().of(UserIdAndCreatedAtSchema).optional().label("Bookmarks"),
79
+
80
+ embedding: object({
81
+ vector: array(number().required()).optional().label("Embedding vector"),
82
+ model: string().optional().label("Embedding model"),
83
+ })
84
+ .nullable()
85
+ .optional()
86
+ .label("Embedding"),
87
+
88
+ applicants: array()
89
+ .of(UserIdAndCreatedAtSchema)
90
+ .optional()
91
+ .label("Applicants"),
92
+
93
+ applicantCount: number().optional().label("Applicant count"),
94
+
95
+ externalApplicationLinkClickCount: number()
96
+ .optional()
97
+ .label("External application link click count"),
98
+
99
+ description: string().required().min(100).max(50000).label("Description"),
100
+
101
+ descriptionMarkdown: string().required().label("Description Markdown"),
102
+
103
+ descriptionHTML: string().optional().label("Description HTML"),
104
+
105
+ /**
106
+ * Job locations. Required even for remote roles (compliance/legal). Defaults
107
+ * to an empty array; min(1) is intentionally not enforced yet.
108
+ */
109
+ locations: array().of(LocationSchema).default([]).label("Location"),
110
+
111
+ educationLevel: array(string().oneOf(SupportedEducationLevels))
112
+ .required()
113
+ .min(1)
114
+ .label("Educational level"),
115
+
116
+ /** Posting validity window (start/expiry). */
117
+ validity: DurationSchema({
118
+ format: SYSTEM_DATE_FORMAT,
119
+ startLabel: "Start Date",
120
+ endLabel: "Expiry Date",
121
+ }).label("Validity"),
122
+
123
+ /**
124
+ * When the auto-close system last re-checked this job's liveness (epoch ms).
125
+ * Server-owned operational field: set by the expiry re-crawl track when a job
126
+ * has no `validity.endDate` and must be verified by crawling its page. Guards
127
+ * re-check cadence so the same job is not re-crawled on every sweep.
128
+ */
129
+ lastExpiryCheckAt: number()
130
+ .optional()
131
+ .nullable()
132
+ .label("Last expiry check at"),
133
+
134
+ experienceLevel: string()
135
+ .oneOf(SupportedExperienceLevels)
136
+ .nullable()
137
+ .optional()
138
+ .label("Experience level"),
139
+
140
+ contactEmail: string().email().nullable().optional().label("Contact email"),
141
+
142
+ workingHoursPerWeek: number()
143
+ .nullable()
144
+ .optional()
145
+ .min(1)
146
+ .max(24 * 7)
147
+ .label("Working hours per week"),
148
+
149
+ tags: array().max(50).optional().label("Tags"),
150
+
151
+ /** Direct/Quick-apply questionnaire. Each item is validated against the
152
+ * concrete question schema for its `type`, with the stored `answers` omitted. */
153
+ questionnaire: array()
154
+ .of(
155
+ lazy((question: TQuestionSchema) => {
156
+ const schema = getSchemaByQuestion(question.type);
157
+ if (question.type === QuestionType.Input) {
158
+ return (schema as ObjectSchema<TInputQuestionSchema>).omit([
159
+ "answers",
160
+ ]);
161
+ } else if (question.type === QuestionType.Choice) {
162
+ return (schema as ObjectSchema<TChoiceQuestionSchema>).omit([
163
+ "answers",
164
+ ]);
165
+ } else if (question.type === QuestionType.ReadAndAcknowledge) {
166
+ return (
167
+ schema as ObjectSchema<TReadAndAcknowledgeQuestionSchema>
168
+ ).omit(["answers"]);
169
+ }
170
+ return schema;
171
+ }),
172
+ )
173
+ .label("Questionnaire"),
174
+
175
+ status: string()
176
+ .oneOf(SupportedJobStatuses)
177
+ .default(JobStatus.Draft)
178
+ .required()
179
+ .label("Status"),
180
+
181
+ seoTags: object({
182
+ description: string().optional().label("Description"),
183
+ keywords: array().of(string()).optional().label("Keywords"),
184
+ })
185
+ .nullable()
186
+ .optional()
187
+ .label("SEO Tags"),
188
+
189
+ jdSummary: string().optional().label("JD Summary"),
190
+
191
+ canCreateAlert: boolean().optional().label("Can create alert"),
192
+
193
+ canDirectApply: boolean().optional().label("Can direct apply"),
194
+
195
+ hasApplied: boolean().optional().label("Has applied"),
196
+
197
+ isOwner: boolean().optional().label("Is owner"),
198
+
199
+ hasBookmarked: boolean().optional().label("Has bookmarked"),
200
+
201
+ hasReported: boolean().optional().label("Has reported"),
202
+
203
+ ratePerHour: number().optional().min(0).label("Rate per hour"),
204
+
205
+ isPromoted: boolean().optional().label("Is promoted"),
206
+
207
+ salaryRange: object({
208
+ currency: string().required().label("Currency").default("USD"),
209
+ min: number().nullable().optional().min(0).label("Minimum Salary"),
210
+ max: number().nullable().optional().min(0).label("Maximum Salary"),
211
+ compensationType: string()
212
+ .oneOf(SupportedCompensationTypes)
213
+ .nullable()
214
+ .optional()
215
+ .label("Compensation Type"),
216
+ isNegotiable: boolean()
217
+ .nullable()
218
+ .optional()
219
+ .default(true)
220
+ .label("Is Negotiable"),
221
+ })
222
+ .nullable()
223
+ .optional()
224
+ .label("Salary Range"),
225
+
226
+ reports: array().of(UserIdAndCreatedAtSchema).optional().label("Reports"),
227
+
228
+ category: string().nullable().optional().label("Category"),
229
+
230
+ industry: string().nullable().optional().label("Industry"),
231
+
232
+ subCategories: array()
233
+ .of(string().trim().required())
234
+ .nullable()
235
+ .optional()
236
+ .label("Sub Categories"),
237
+
238
+ termsAccepted: boolean()
239
+ .oneOf([true], "Accept terms before proceeding")
240
+ .required("Accept terms before proceeding")
241
+ .label("Terms accepted"),
242
+
243
+ isFeatured: boolean().optional().default(false).label("Is featured"),
244
+
245
+ /** Market country codes to feature this job in; empty = global. */
246
+ featuredMarkets: array()
247
+ .of(string().required())
248
+ .optional()
249
+ .default([])
250
+ .label("Featured markets"),
251
+ })
252
+ .concat(DbDefaultSchema)
253
+ .noUnknown()
254
+ .label("Job");
255
+
256
+ export type TJobSchema = InferType<typeof JobSchema>;
@@ -0,0 +1,96 @@
1
+ import { array, mixed, object, string } from "yup";
2
+ import type { InferType } from "yup";
3
+ import { Common } from "../common/common.constant.js";
4
+ import {
5
+ AnswerChoiceType,
6
+ QuestionType,
7
+ SupportedAnswerChoiceTypes,
8
+ } from "./question.constant.js";
9
+ import { QuestionSchema } from "./question.schema.js";
10
+
11
+ /**
12
+ * Validates that the selected answer(s) come from the question's own `options`.
13
+ * In v2 options and answers are plain strings (the v1 name/id/logo and "others"
14
+ * reference-object variants were dropped, matching the rest of the v2 schema).
15
+ */
16
+ const ChoicePreferredAnswerSchema = (label: string = "Preferred answer") =>
17
+ mixed<string | string[]>().when("answerChoiceType", {
18
+ is: (value: AnswerChoiceType) => value === AnswerChoiceType.Multiple,
19
+ then: () =>
20
+ array(string())
21
+ .test(
22
+ "is-valid-option",
23
+ `${label} must be from the provided options`,
24
+ function (answer) {
25
+ const { options = [] } = this.parent;
26
+ if (answer && answer.length > 0) {
27
+ return answer.every((a) => options.includes(a));
28
+ }
29
+ return true;
30
+ },
31
+ )
32
+ .optional()
33
+ .label(label),
34
+ otherwise: () =>
35
+ string()
36
+ .test(
37
+ "is-valid-option",
38
+ `${label} must be from the provided options`,
39
+ function (answer) {
40
+ const { options = [] } = this.parent;
41
+ return !answer || options.includes(answer);
42
+ },
43
+ )
44
+ .optional()
45
+ .label(label),
46
+ });
47
+
48
+ const getChoiceAnswerSchema = (
49
+ answerChoiceType: AnswerChoiceType,
50
+ isOptional: boolean,
51
+ ) => {
52
+ if (answerChoiceType === AnswerChoiceType.Multiple) {
53
+ const multiAnswerSchema = array(string().max(500));
54
+ return isOptional
55
+ ? multiAnswerSchema.optional().default([])
56
+ : multiAnswerSchema
57
+ .min(1, "Select at least one option.")
58
+ .required("Select at least one option.")
59
+ .default([]);
60
+ }
61
+ // Single choice
62
+ const singleAnswerSchema = string().max(500);
63
+ return isOptional
64
+ ? singleAnswerSchema.optional()
65
+ : singleAnswerSchema.required("Select an option.");
66
+ };
67
+
68
+ export const ChoiceQuestionSchema = QuestionSchema.concat(
69
+ object().shape({
70
+ type: string()
71
+ .oneOf([QuestionType.Choice])
72
+ .required()
73
+ .label("Question type"),
74
+ optionsFrom: string()
75
+ .oneOf([Common.Skill, Common.EducationLevel, Common.ExperienceLevel])
76
+ .optional()
77
+ .label("Option field"),
78
+ options: array(string().required())
79
+ .min(1)
80
+ .required()
81
+ .label("Options"),
82
+ answerChoiceType: string()
83
+ .oneOf(SupportedAnswerChoiceTypes)
84
+ .required()
85
+ .default(AnswerChoiceType.Single)
86
+ .label("Type"),
87
+ preferredAnswer: ChoicePreferredAnswerSchema("Preferred answer"),
88
+ answers: mixed<string | string[]>().when(
89
+ ["answerChoiceType", "isOptional"],
90
+ ([answerChoiceType, isOptional]) =>
91
+ getChoiceAnswerSchema(answerChoiceType, isOptional),
92
+ ),
93
+ }),
94
+ );
95
+
96
+ export type TChoiceQuestionSchema = InferType<typeof ChoiceQuestionSchema>;
@@ -0,0 +1,6 @@
1
+ export * from "./question.constant.js";
2
+ export * from "./question.schema.js";
3
+ export * from "./input-question.schema.js";
4
+ export * from "./choice-question.schema.js";
5
+ export * from "./read-and-acknowledge.schema.js";
6
+ export * from "./question.utils.js";
@@ -0,0 +1,24 @@
1
+ import { object, string } from "yup";
2
+ import type { InferType } from "yup";
3
+ import { QuestionType } from "./question.constant.js";
4
+ import { QuestionSchema } from "./question.schema.js";
5
+
6
+ export const InputQuestionSchema = QuestionSchema.concat(
7
+ object().shape({
8
+ type: string()
9
+ .oneOf([QuestionType.Input])
10
+ .required()
11
+ .label("Question type"),
12
+ preferredAnswer: string().max(100).optional().label("Preferred Answer"),
13
+ answers: string()
14
+ .max(500)
15
+ .when("isOptional", {
16
+ is: (isOptional: boolean) => Boolean(isOptional),
17
+ then: (schema) => schema.optional(),
18
+ otherwise: (schema) => schema.required(),
19
+ })
20
+ .label("Answer"),
21
+ }),
22
+ );
23
+
24
+ export type TInputQuestionSchema = InferType<typeof InputQuestionSchema>;
@@ -0,0 +1,14 @@
1
+ export enum QuestionType {
2
+ Input = "input",
3
+ Choice = "choice",
4
+ ReadAndAcknowledge = "readAndAcknowledge",
5
+ }
6
+
7
+ export const SupportedQuestionTypes = Object.values(QuestionType);
8
+
9
+ export enum AnswerChoiceType {
10
+ Single = "single",
11
+ Multiple = "multiple",
12
+ }
13
+
14
+ export const SupportedAnswerChoiceTypes = Object.values(AnswerChoiceType);
@@ -0,0 +1,22 @@
1
+ import { boolean, object, string } from "yup";
2
+ import { SupportedQuestionTypes } from "./question.constant.js";
3
+ import type { TChoiceQuestionSchema } from "./choice-question.schema.js";
4
+ import type { TInputQuestionSchema } from "./input-question.schema.js";
5
+ import type { TReadAndAcknowledgeQuestionSchema } from "./read-and-acknowledge.schema.js";
6
+
7
+ /** Base fields shared by every question variant. The concrete variants concat
8
+ * their own `type`-narrowed fields onto this (see input/choice/acknowledge). */
9
+ export const QuestionSchema = object().shape({
10
+ label: string().max(50).optional().label("Label"),
11
+ default: boolean().optional().default(false).label("Default"),
12
+ title: string().required().max(200).label("Title"),
13
+ type: string().oneOf(SupportedQuestionTypes).required().label("Type"),
14
+ isOptional: boolean().optional().label("Mark if the question is optional"),
15
+ readonly: boolean().optional().label("Readonly"),
16
+ isNew: boolean().optional().label("Is New"),
17
+ });
18
+
19
+ export type TQuestionSchema =
20
+ | TInputQuestionSchema
21
+ | TChoiceQuestionSchema
22
+ | TReadAndAcknowledgeQuestionSchema;
@@ -0,0 +1,23 @@
1
+ import type { ObjectSchema } from "yup";
2
+ import { ChoiceQuestionSchema } from "./choice-question.schema.js";
3
+ import { InputQuestionSchema } from "./input-question.schema.js";
4
+ import { QuestionType } from "./question.constant.js";
5
+ import type { TQuestionSchema } from "./question.schema.js";
6
+ import { ReadAndAcknowledgeQuestionSchema } from "./read-and-acknowledge.schema.js";
7
+
8
+ /** Resolve the concrete question schema for a question's `type`. Drives the
9
+ * `lazy` validation of the job questionnaire. */
10
+ export const getSchemaByQuestion = (
11
+ type: QuestionType,
12
+ ): ObjectSchema<TQuestionSchema> => {
13
+ switch (type) {
14
+ case QuestionType.Input:
15
+ return InputQuestionSchema as unknown as ObjectSchema<TQuestionSchema>;
16
+ case QuestionType.Choice:
17
+ return ChoiceQuestionSchema as unknown as ObjectSchema<TQuestionSchema>;
18
+ case QuestionType.ReadAndAcknowledge:
19
+ return ReadAndAcknowledgeQuestionSchema as unknown as ObjectSchema<TQuestionSchema>;
20
+ default:
21
+ throw new Error("Unsupported schema: " + type);
22
+ }
23
+ };
@@ -0,0 +1,25 @@
1
+ import { object, string } from "yup";
2
+ import type { InferType } from "yup";
3
+ import { Common } from "../common/common.constant.js";
4
+ import { QuestionType } from "./question.constant.js";
5
+ import { QuestionSchema } from "./question.schema.js";
6
+
7
+ const AcknowledgeAnswerSchema = string()
8
+ .oneOf([Common.Yes, Common.No])
9
+ .required("This question must be acknowledged.");
10
+
11
+ export const ReadAndAcknowledgeQuestionSchema = QuestionSchema.concat(
12
+ object().shape({
13
+ type: string()
14
+ .oneOf([QuestionType.ReadAndAcknowledge])
15
+ .required()
16
+ .label("Question type"),
17
+ description: string().max(10000).required().label("Content"),
18
+ preferredAnswer: AcknowledgeAnswerSchema.label("Preferred Answer"),
19
+ answers: AcknowledgeAnswerSchema.label("Answer"),
20
+ }),
21
+ );
22
+
23
+ export type TReadAndAcknowledgeQuestionSchema = InferType<
24
+ typeof ReadAndAcknowledgeQuestionSchema
25
+ >;