@kelpie/schemas 0.1.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/LICENSE +661 -0
- package/README.md +33 -0
- package/dist/account.d.ts +79 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +66 -0
- package/dist/account.js.map +1 -0
- package/dist/activity.d.ts +23 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +27 -0
- package/dist/activity.js.map +1 -0
- package/dist/agentTask.d.ts +97 -0
- package/dist/agentTask.d.ts.map +1 -0
- package/dist/agentTask.js +127 -0
- package/dist/agentTask.js.map +1 -0
- package/dist/candidate.d.ts +39 -0
- package/dist/candidate.d.ts.map +1 -0
- package/dist/candidate.js +40 -0
- package/dist/candidate.js.map +1 -0
- package/dist/company.d.ts +38 -0
- package/dist/company.d.ts.map +1 -0
- package/dist/company.js +57 -0
- package/dist/company.js.map +1 -0
- package/dist/dashboard.d.ts +112 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +165 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/deal.d.ts +40 -0
- package/dist/deal.d.ts.map +1 -0
- package/dist/deal.js +59 -0
- package/dist/deal.js.map +1 -0
- package/dist/decision.d.ts +46 -0
- package/dist/decision.d.ts.map +1 -0
- package/dist/decision.js +48 -0
- package/dist/decision.js.map +1 -0
- package/dist/form.d.ts +97 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +98 -0
- package/dist/form.js.map +1 -0
- package/dist/formSubmission.d.ts +47 -0
- package/dist/formSubmission.d.ts.map +1 -0
- package/dist/formSubmission.js +47 -0
- package/dist/formSubmission.js.map +1 -0
- package/dist/handbookPage.d.ts +52 -0
- package/dist/handbookPage.d.ts.map +1 -0
- package/dist/handbookPage.js +56 -0
- package/dist/handbookPage.js.map +1 -0
- package/dist/importExport.d.ts +143 -0
- package/dist/importExport.d.ts.map +1 -0
- package/dist/importExport.js +239 -0
- package/dist/importExport.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/invite.d.ts +35 -0
- package/dist/invite.d.ts.map +1 -0
- package/dist/invite.js +31 -0
- package/dist/invite.js.map +1 -0
- package/dist/mcpTool.d.ts +26 -0
- package/dist/mcpTool.d.ts.map +1 -0
- package/dist/mcpTool.js +13 -0
- package/dist/mcpTool.js.map +1 -0
- package/dist/member.d.ts +26 -0
- package/dist/member.d.ts.map +1 -0
- package/dist/member.js +24 -0
- package/dist/member.js.map +1 -0
- package/dist/moduleSetting.d.ts +19 -0
- package/dist/moduleSetting.d.ts.map +1 -0
- package/dist/moduleSetting.js +16 -0
- package/dist/moduleSetting.js.map +1 -0
- package/dist/note.d.ts +34 -0
- package/dist/note.d.ts.map +1 -0
- package/dist/note.js +35 -0
- package/dist/note.js.map +1 -0
- package/dist/opportunity.d.ts +30 -0
- package/dist/opportunity.d.ts.map +1 -0
- package/dist/opportunity.js +41 -0
- package/dist/opportunity.js.map +1 -0
- package/dist/partnership.d.ts +36 -0
- package/dist/partnership.d.ts.map +1 -0
- package/dist/partnership.js +50 -0
- package/dist/partnership.js.map +1 -0
- package/dist/person.d.ts +45 -0
- package/dist/person.d.ts.map +1 -0
- package/dist/person.js +60 -0
- package/dist/person.js.map +1 -0
- package/dist/pipelineStage.d.ts +29 -0
- package/dist/pipelineStage.d.ts.map +1 -0
- package/dist/pipelineStage.js +38 -0
- package/dist/pipelineStage.js.map +1 -0
- package/dist/planItem.d.ts +43 -0
- package/dist/planItem.d.ts.map +1 -0
- package/dist/planItem.js +44 -0
- package/dist/planItem.js.map +1 -0
- package/dist/position.d.ts +22 -0
- package/dist/position.d.ts.map +1 -0
- package/dist/position.js +25 -0
- package/dist/position.js.map +1 -0
- package/dist/raise.d.ts +38 -0
- package/dist/raise.d.ts.map +1 -0
- package/dist/raise.js +53 -0
- package/dist/raise.js.map +1 -0
- package/dist/role.d.ts +21 -0
- package/dist/role.d.ts.map +1 -0
- package/dist/role.js +21 -0
- package/dist/role.js.map +1 -0
- package/dist/search.d.ts +41 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +41 -0
- package/dist/search.js.map +1 -0
- package/dist/session.d.ts +92 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +67 -0
- package/dist/session.js.map +1 -0
- package/dist/values.d.ts +216 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +329 -0
- package/dist/values.js.map +1 -0
- package/dist/webhook.d.ts +85 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +75 -0
- package/dist/webhook.js.map +1 -0
- package/dist/wire.d.ts +40 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +40 -0
- package/dist/wire.js.map +1 -0
- package/package.json +51 -0
- package/src/account.ts +149 -0
- package/src/activity.ts +52 -0
- package/src/agentTask.ts +254 -0
- package/src/candidate.ts +83 -0
- package/src/company.ts +98 -0
- package/src/dashboard.ts +307 -0
- package/src/deal.ts +102 -0
- package/src/decision.ts +106 -0
- package/src/form.ts +217 -0
- package/src/formSubmission.ts +99 -0
- package/src/handbookPage.ts +99 -0
- package/src/importExport.ts +339 -0
- package/src/index.ts +324 -0
- package/src/invite.ts +69 -0
- package/src/mcpTool.ts +32 -0
- package/src/member.ts +53 -0
- package/src/moduleSetting.ts +37 -0
- package/src/note.ts +73 -0
- package/src/opportunity.ts +74 -0
- package/src/partnership.ts +89 -0
- package/src/person.ts +116 -0
- package/src/pipelineStage.ts +71 -0
- package/src/planItem.ts +91 -0
- package/src/position.ts +50 -0
- package/src/raise.ts +94 -0
- package/src/role.ts +45 -0
- package/src/search.ts +86 -0
- package/src/session.ts +177 -0
- package/src/values.ts +427 -0
- package/src/webhook.ts +164 -0
- package/src/wire.ts +51 -0
package/src/form.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FORM_FIELD_MAP_TARGETS,
|
|
5
|
+
FORM_FIELD_TYPES,
|
|
6
|
+
FORM_OPTION_VALUE_TYPES,
|
|
7
|
+
FORM_STATUSES,
|
|
8
|
+
} from './values.ts'
|
|
9
|
+
import type { FormFieldMapTarget, FormFieldType, FormOptionValueType, FormStatus } from './values.ts'
|
|
10
|
+
import { definedFields, idSchema, recordTimestamps } from './wire.ts'
|
|
11
|
+
import type { RecordTimestamps } from './wire.ts'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wire and write shapes for `/v1/forms`, the embeddable inbound capture from
|
|
15
|
+
* [`forms.md`](../../../forms.md).
|
|
16
|
+
*
|
|
17
|
+
* Fields come back nested inside their form rather than as their own resource.
|
|
18
|
+
* A form without its fields cannot be rendered or validated, so every caller
|
|
19
|
+
* that wants one wants both, and the ordering is the form's rather than a
|
|
20
|
+
* property of any single field.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A select choice. `key` is the identifier: it is what a stored answer holds and
|
|
25
|
+
* what a submit is checked against, so the display `value` is free to change.
|
|
26
|
+
*/
|
|
27
|
+
export interface FormFieldOption {
|
|
28
|
+
readonly key: string
|
|
29
|
+
/** What the embed shows in the dropdown. */
|
|
30
|
+
readonly value: string
|
|
31
|
+
readonly valueType: FormOptionValueType
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FormField {
|
|
35
|
+
readonly id: string
|
|
36
|
+
readonly label: string
|
|
37
|
+
readonly type: FormFieldType
|
|
38
|
+
readonly required: boolean
|
|
39
|
+
readonly mapTo: FormFieldMapTarget
|
|
40
|
+
/** Empty for every type but `select`. */
|
|
41
|
+
readonly options: readonly FormFieldOption[]
|
|
42
|
+
readonly placeholder: string | null
|
|
43
|
+
/** Position in the form, contiguous from 0. The server renumbers on every write. */
|
|
44
|
+
readonly sortOrder: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Form extends RecordTimestamps {
|
|
48
|
+
readonly id: string
|
|
49
|
+
readonly name: string
|
|
50
|
+
readonly description: string | null
|
|
51
|
+
readonly status: FormStatus
|
|
52
|
+
readonly fields: readonly FormField[]
|
|
53
|
+
readonly thankYouMessage: string
|
|
54
|
+
readonly createDeal: boolean
|
|
55
|
+
/** Null means the deal pipeline's default stage is used at submit time. */
|
|
56
|
+
readonly dealStageId: string | null
|
|
57
|
+
/** Expands `{{company.name}}` and `{{person.name}}`. Null when `createDeal` is off. */
|
|
58
|
+
readonly dealNameTemplate: string | null
|
|
59
|
+
/**
|
|
60
|
+
* The handle the public submit and embed endpoints resolve a workspace from.
|
|
61
|
+
* Globally unique, and the only credential those endpoints take.
|
|
62
|
+
*/
|
|
63
|
+
readonly publicKey: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const formFieldOptionSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
key: z.string(),
|
|
69
|
+
value: z.string(),
|
|
70
|
+
value_type: z.enum(FORM_OPTION_VALUE_TYPES),
|
|
71
|
+
})
|
|
72
|
+
.transform(
|
|
73
|
+
(wire): FormFieldOption => ({
|
|
74
|
+
key: wire.key,
|
|
75
|
+
value: wire.value,
|
|
76
|
+
valueType: wire.value_type,
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const formFieldSchema = z
|
|
81
|
+
.object({
|
|
82
|
+
id: idSchema,
|
|
83
|
+
label: z.string(),
|
|
84
|
+
type: z.enum(FORM_FIELD_TYPES),
|
|
85
|
+
required: z.boolean(),
|
|
86
|
+
map_to: z.enum(FORM_FIELD_MAP_TARGETS),
|
|
87
|
+
options: z.array(formFieldOptionSchema),
|
|
88
|
+
placeholder: z.string().nullable(),
|
|
89
|
+
sort_order: z.number().int(),
|
|
90
|
+
})
|
|
91
|
+
.transform(
|
|
92
|
+
(wire): FormField => ({
|
|
93
|
+
id: wire.id,
|
|
94
|
+
label: wire.label,
|
|
95
|
+
type: wire.type,
|
|
96
|
+
required: wire.required,
|
|
97
|
+
mapTo: wire.map_to,
|
|
98
|
+
options: wire.options,
|
|
99
|
+
placeholder: wire.placeholder,
|
|
100
|
+
sortOrder: wire.sort_order,
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
export const formSchema: z.ZodType<Form, unknown> = z
|
|
105
|
+
.object({
|
|
106
|
+
id: idSchema,
|
|
107
|
+
name: z.string(),
|
|
108
|
+
description: z.string().nullable(),
|
|
109
|
+
status: z.enum(FORM_STATUSES),
|
|
110
|
+
fields: z.array(formFieldSchema),
|
|
111
|
+
thank_you_message: z.string(),
|
|
112
|
+
create_deal: z.boolean(),
|
|
113
|
+
deal_stage_id: idSchema.nullable(),
|
|
114
|
+
deal_name_template: z.string().nullable(),
|
|
115
|
+
public_key: z.string(),
|
|
116
|
+
...recordTimestamps,
|
|
117
|
+
})
|
|
118
|
+
.transform(
|
|
119
|
+
(wire): Form => ({
|
|
120
|
+
id: wire.id,
|
|
121
|
+
name: wire.name,
|
|
122
|
+
description: wire.description,
|
|
123
|
+
status: wire.status,
|
|
124
|
+
fields: wire.fields,
|
|
125
|
+
thankYouMessage: wire.thank_you_message,
|
|
126
|
+
createDeal: wire.create_deal,
|
|
127
|
+
dealStageId: wire.deal_stage_id,
|
|
128
|
+
dealNameTemplate: wire.deal_name_template,
|
|
129
|
+
publicKey: wire.public_key,
|
|
130
|
+
createdAt: wire.created_at,
|
|
131
|
+
updatedAt: wire.updated_at,
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
export interface FormFieldOptionInput {
|
|
136
|
+
readonly key: string
|
|
137
|
+
readonly value: string
|
|
138
|
+
readonly valueType?: FormOptionValueType
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A field as it is written.
|
|
143
|
+
*
|
|
144
|
+
* There is no id: a write replaces the whole field list, so the server assigns
|
|
145
|
+
* ids and positions from the array's order. Editing one field means sending the
|
|
146
|
+
* list back with that field changed, which is also what a drag-reorder sends.
|
|
147
|
+
*/
|
|
148
|
+
export interface FormFieldInput {
|
|
149
|
+
readonly label: string
|
|
150
|
+
readonly type: FormFieldType
|
|
151
|
+
readonly required?: boolean
|
|
152
|
+
readonly mapTo: FormFieldMapTarget
|
|
153
|
+
readonly options?: readonly FormFieldOptionInput[]
|
|
154
|
+
readonly placeholder?: string | null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface CreateFormInput {
|
|
158
|
+
readonly name: string
|
|
159
|
+
readonly description?: string | null
|
|
160
|
+
readonly status?: FormStatus
|
|
161
|
+
readonly fields: readonly FormFieldInput[]
|
|
162
|
+
readonly thankYouMessage?: string
|
|
163
|
+
readonly createDeal?: boolean
|
|
164
|
+
readonly dealStageId?: string | null
|
|
165
|
+
readonly dealNameTemplate?: string | null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** `fields` is absent or the whole list; there is no per-field patch. */
|
|
169
|
+
export interface FormInput {
|
|
170
|
+
readonly name?: string
|
|
171
|
+
readonly description?: string | null
|
|
172
|
+
readonly status?: FormStatus
|
|
173
|
+
readonly fields?: readonly FormFieldInput[]
|
|
174
|
+
readonly thankYouMessage?: string
|
|
175
|
+
readonly createDeal?: boolean
|
|
176
|
+
readonly dealStageId?: string | null
|
|
177
|
+
readonly dealNameTemplate?: string | null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function fieldBody(field: FormFieldInput): Record<string, unknown> {
|
|
181
|
+
return definedFields({
|
|
182
|
+
label: field.label,
|
|
183
|
+
type: field.type,
|
|
184
|
+
required: field.required,
|
|
185
|
+
map_to: field.mapTo,
|
|
186
|
+
options: field.options?.map((option) =>
|
|
187
|
+
definedFields({ key: option.key, value: option.value, value_type: option.valueType }),
|
|
188
|
+
),
|
|
189
|
+
placeholder: field.placeholder,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function createFormBody(input: CreateFormInput): Record<string, unknown> {
|
|
194
|
+
return definedFields({
|
|
195
|
+
name: input.name,
|
|
196
|
+
description: input.description,
|
|
197
|
+
status: input.status,
|
|
198
|
+
fields: input.fields.map(fieldBody),
|
|
199
|
+
thank_you_message: input.thankYouMessage,
|
|
200
|
+
create_deal: input.createDeal,
|
|
201
|
+
deal_stage_id: input.dealStageId,
|
|
202
|
+
deal_name_template: input.dealNameTemplate,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function formBody(input: FormInput): Record<string, unknown> {
|
|
207
|
+
return definedFields({
|
|
208
|
+
name: input.name,
|
|
209
|
+
description: input.description,
|
|
210
|
+
status: input.status,
|
|
211
|
+
fields: input.fields?.map(fieldBody),
|
|
212
|
+
thank_you_message: input.thankYouMessage,
|
|
213
|
+
create_deal: input.createDeal,
|
|
214
|
+
deal_stage_id: input.dealStageId,
|
|
215
|
+
deal_name_template: input.dealNameTemplate,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { idSchema, timestampSchema } from './wire.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wire shape for `/v1/forms/:id/submissions`, and for what the public submit
|
|
7
|
+
* endpoint answers with.
|
|
8
|
+
*
|
|
9
|
+
* Read-only over the API: a submission is evidence of what arrived, so there is
|
|
10
|
+
* no create body here (the public endpoint takes an answer map, not a
|
|
11
|
+
* submission) and no update at all.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface FormSubmission {
|
|
15
|
+
readonly id: string
|
|
16
|
+
readonly formId: string
|
|
17
|
+
readonly submittedAt: Date
|
|
18
|
+
/** Field id to the answer given: a select option's `key`, or free text. */
|
|
19
|
+
readonly answers: Readonly<Record<string, string>>
|
|
20
|
+
/**
|
|
21
|
+
* What the submit created or matched. Each is null when the rule did not
|
|
22
|
+
* apply, and each becomes null again if that record is later deleted: the
|
|
23
|
+
* submission outlives what it produced.
|
|
24
|
+
*/
|
|
25
|
+
readonly personId: string | null
|
|
26
|
+
readonly companyId: string | null
|
|
27
|
+
readonly positionId: string | null
|
|
28
|
+
readonly dealId: string | null
|
|
29
|
+
readonly createdAt: Date
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const formSubmissionSchema: z.ZodType<FormSubmission, unknown> = z
|
|
33
|
+
.object({
|
|
34
|
+
id: idSchema,
|
|
35
|
+
form_id: idSchema,
|
|
36
|
+
submitted_at: timestampSchema,
|
|
37
|
+
answers: z.record(z.string(), z.string()),
|
|
38
|
+
person_id: idSchema.nullable(),
|
|
39
|
+
company_id: idSchema.nullable(),
|
|
40
|
+
position_id: idSchema.nullable(),
|
|
41
|
+
deal_id: idSchema.nullable(),
|
|
42
|
+
created_at: timestampSchema,
|
|
43
|
+
})
|
|
44
|
+
.transform(
|
|
45
|
+
(wire): FormSubmission => ({
|
|
46
|
+
id: wire.id,
|
|
47
|
+
formId: wire.form_id,
|
|
48
|
+
submittedAt: wire.submitted_at,
|
|
49
|
+
answers: wire.answers,
|
|
50
|
+
personId: wire.person_id,
|
|
51
|
+
companyId: wire.company_id,
|
|
52
|
+
positionId: wire.position_id,
|
|
53
|
+
dealId: wire.deal_id,
|
|
54
|
+
createdAt: wire.created_at,
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* What the public submit endpoint answers with.
|
|
60
|
+
*
|
|
61
|
+
* Smaller than the stored submission, as `forms.md` specifies: the ids it
|
|
62
|
+
* produced, and nothing else. The caller is a stranger's browser, and echoing
|
|
63
|
+
* back the answers it just sent would tell it nothing it does not have.
|
|
64
|
+
*/
|
|
65
|
+
export interface FormSubmitResult {
|
|
66
|
+
readonly id: string
|
|
67
|
+
readonly formId: string
|
|
68
|
+
readonly submittedAt: Date
|
|
69
|
+
readonly personId: string
|
|
70
|
+
readonly companyId: string | null
|
|
71
|
+
readonly positionId: string | null
|
|
72
|
+
readonly dealId: string | null
|
|
73
|
+
/** The form's configured confirmation, so an embed can render it without a second request. */
|
|
74
|
+
readonly thankYouMessage: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const formSubmitResultSchema: z.ZodType<FormSubmitResult, unknown> = z
|
|
78
|
+
.object({
|
|
79
|
+
id: idSchema,
|
|
80
|
+
form_id: idSchema,
|
|
81
|
+
submitted_at: timestampSchema,
|
|
82
|
+
person_id: idSchema,
|
|
83
|
+
company_id: idSchema.nullable(),
|
|
84
|
+
position_id: idSchema.nullable(),
|
|
85
|
+
deal_id: idSchema.nullable(),
|
|
86
|
+
thank_you_message: z.string(),
|
|
87
|
+
})
|
|
88
|
+
.transform(
|
|
89
|
+
(wire): FormSubmitResult => ({
|
|
90
|
+
id: wire.id,
|
|
91
|
+
formId: wire.form_id,
|
|
92
|
+
submittedAt: wire.submitted_at,
|
|
93
|
+
personId: wire.person_id,
|
|
94
|
+
companyId: wire.company_id,
|
|
95
|
+
positionId: wire.position_id,
|
|
96
|
+
dealId: wire.deal_id,
|
|
97
|
+
thankYouMessage: wire.thank_you_message,
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { definedFields, idSchema, recordTimestamps } from './wire.ts'
|
|
4
|
+
import type { RecordTimestamps } from './wire.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wire and write shapes for `/v1/handbook_pages`, the workspace's nested
|
|
8
|
+
* markdown handbook.
|
|
9
|
+
*
|
|
10
|
+
* The response is flat: `parentId` and `sortOrder` are the tree, and the caller
|
|
11
|
+
* rebuilds it. `sortOrder` positions a page among its siblings only, so it is
|
|
12
|
+
* meaningless across the whole set and the list is not sorted by it.
|
|
13
|
+
*
|
|
14
|
+
* `updatedBy` is a workspace member id, not a user id, and is null when no
|
|
15
|
+
* member was behind the write (a workspace API key). Resolving it to a name is
|
|
16
|
+
* the caller's job, as with a note's author.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Top-level pages are 0, so `MAX_DEPTH` of 4 allows five levels. Mirrors the server's cap. */
|
|
20
|
+
export const MAX_HANDBOOK_DEPTH = 4
|
|
21
|
+
|
|
22
|
+
export interface HandbookPage extends RecordTimestamps {
|
|
23
|
+
readonly id: string
|
|
24
|
+
readonly title: string
|
|
25
|
+
/** The stable handle agent tasks name pages by. It does not follow the title. */
|
|
26
|
+
readonly slug: string
|
|
27
|
+
readonly parentId: string | null
|
|
28
|
+
readonly sortOrder: number
|
|
29
|
+
readonly body: string
|
|
30
|
+
readonly updatedBy: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const handbookPageSchema: z.ZodType<HandbookPage, unknown> = z
|
|
34
|
+
.object({
|
|
35
|
+
id: idSchema,
|
|
36
|
+
title: z.string(),
|
|
37
|
+
slug: z.string(),
|
|
38
|
+
parent_id: idSchema.nullable(),
|
|
39
|
+
sort_order: z.number().int(),
|
|
40
|
+
body: z.string(),
|
|
41
|
+
updated_by: idSchema.nullable(),
|
|
42
|
+
...recordTimestamps,
|
|
43
|
+
})
|
|
44
|
+
.transform(
|
|
45
|
+
(wire): HandbookPage => ({
|
|
46
|
+
id: wire.id,
|
|
47
|
+
title: wire.title,
|
|
48
|
+
slug: wire.slug,
|
|
49
|
+
parentId: wire.parent_id,
|
|
50
|
+
sortOrder: wire.sort_order,
|
|
51
|
+
body: wire.body,
|
|
52
|
+
updatedBy: wire.updated_by,
|
|
53
|
+
createdAt: wire.created_at,
|
|
54
|
+
updatedAt: wire.updated_at,
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
export interface CreateHandbookPageInput {
|
|
59
|
+
readonly title: string
|
|
60
|
+
/** Absent means empty: a page is created before it is written. */
|
|
61
|
+
readonly body?: string
|
|
62
|
+
/** Absent means derived from the title, with a numeric suffix if that is taken. */
|
|
63
|
+
readonly slug?: string
|
|
64
|
+
/** Absent or null puts the page at the top level. */
|
|
65
|
+
readonly parentId?: string | null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createHandbookPageBody(input: CreateHandbookPageInput): Record<string, unknown> {
|
|
69
|
+
return definedFields({
|
|
70
|
+
title: input.title,
|
|
71
|
+
body: input.body,
|
|
72
|
+
slug: input.slug,
|
|
73
|
+
parent_id: input.parentId,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `parentId` and `sortOrder` are the move; the rest is the edit. A drag sends
|
|
79
|
+
* both halves of the move together, because landing somewhere new is a parent
|
|
80
|
+
* and a position at once.
|
|
81
|
+
*/
|
|
82
|
+
export interface HandbookPageInput {
|
|
83
|
+
readonly title?: string
|
|
84
|
+
readonly body?: string
|
|
85
|
+
readonly slug?: string
|
|
86
|
+
/** Null lifts the page to the top level. */
|
|
87
|
+
readonly parentId?: string | null
|
|
88
|
+
readonly sortOrder?: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function handbookPageBody(input: HandbookPageInput): Record<string, unknown> {
|
|
92
|
+
return definedFields({
|
|
93
|
+
title: input.title,
|
|
94
|
+
body: input.body,
|
|
95
|
+
slug: input.slug,
|
|
96
|
+
parent_id: input.parentId,
|
|
97
|
+
sort_order: input.sortOrder,
|
|
98
|
+
})
|
|
99
|
+
}
|