@kelpie/schemas 0.8.0 → 0.10.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.
Files changed (91) hide show
  1. package/dist/company.d.ts +8 -0
  2. package/dist/company.d.ts.map +1 -1
  3. package/dist/company.js +4 -0
  4. package/dist/company.js.map +1 -1
  5. package/dist/consentPurpose.d.ts +49 -0
  6. package/dist/consentPurpose.d.ts.map +1 -0
  7. package/dist/consentPurpose.js +40 -0
  8. package/dist/consentPurpose.js.map +1 -0
  9. package/dist/customField.d.ts +99 -0
  10. package/dist/customField.d.ts.map +1 -0
  11. package/dist/customField.js +133 -0
  12. package/dist/customField.js.map +1 -0
  13. package/dist/deal.d.ts +8 -0
  14. package/dist/deal.d.ts.map +1 -1
  15. package/dist/deal.js +4 -0
  16. package/dist/deal.js.map +1 -1
  17. package/dist/enquiry.d.ts +43 -0
  18. package/dist/enquiry.d.ts.map +1 -0
  19. package/dist/enquiry.js +47 -0
  20. package/dist/enquiry.js.map +1 -0
  21. package/dist/form.d.ts +117 -2
  22. package/dist/form.d.ts.map +1 -1
  23. package/dist/form.js +102 -1
  24. package/dist/form.js.map +1 -1
  25. package/dist/formSubmission.d.ts +29 -0
  26. package/dist/formSubmission.d.ts.map +1 -1
  27. package/dist/formSubmission.js +34 -0
  28. package/dist/formSubmission.js.map +1 -1
  29. package/dist/importExport.d.ts +28 -4
  30. package/dist/importExport.d.ts.map +1 -1
  31. package/dist/importExport.js +24 -1
  32. package/dist/importExport.js.map +1 -1
  33. package/dist/index.d.ts +15 -5
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +7 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/list.d.ts +10 -0
  38. package/dist/list.d.ts.map +1 -1
  39. package/dist/list.js +6 -0
  40. package/dist/list.js.map +1 -1
  41. package/dist/names.d.ts +43 -0
  42. package/dist/names.d.ts.map +1 -0
  43. package/dist/names.js +35 -0
  44. package/dist/names.js.map +1 -0
  45. package/dist/opportunity.d.ts +10 -0
  46. package/dist/opportunity.d.ts.map +1 -1
  47. package/dist/opportunity.js +7 -0
  48. package/dist/opportunity.js.map +1 -1
  49. package/dist/partnership.d.ts +8 -0
  50. package/dist/partnership.d.ts.map +1 -1
  51. package/dist/partnership.js +4 -0
  52. package/dist/partnership.js.map +1 -1
  53. package/dist/person.d.ts +42 -1
  54. package/dist/person.d.ts.map +1 -1
  55. package/dist/person.js +23 -0
  56. package/dist/person.js.map +1 -1
  57. package/dist/personConsent.d.ts +31 -0
  58. package/dist/personConsent.d.ts.map +1 -0
  59. package/dist/personConsent.js +27 -0
  60. package/dist/personConsent.js.map +1 -0
  61. package/dist/raise.d.ts +8 -0
  62. package/dist/raise.d.ts.map +1 -1
  63. package/dist/raise.js +4 -0
  64. package/dist/raise.js.map +1 -1
  65. package/dist/sampleData.d.ts +1 -0
  66. package/dist/sampleData.d.ts.map +1 -1
  67. package/dist/sampleData.js +2 -0
  68. package/dist/sampleData.js.map +1 -1
  69. package/dist/values.d.ts +86 -8
  70. package/dist/values.d.ts.map +1 -1
  71. package/dist/values.js +140 -3
  72. package/dist/values.js.map +1 -1
  73. package/package.json +1 -1
  74. package/src/company.ts +13 -0
  75. package/src/consentPurpose.ts +95 -0
  76. package/src/customField.ts +211 -0
  77. package/src/deal.ts +13 -0
  78. package/src/enquiry.ts +94 -0
  79. package/src/form.ts +230 -2
  80. package/src/formSubmission.ts +63 -0
  81. package/src/importExport.ts +44 -5
  82. package/src/index.ts +70 -2
  83. package/src/list.ts +16 -0
  84. package/src/names.ts +49 -0
  85. package/src/opportunity.ts +18 -0
  86. package/src/partnership.ts +13 -0
  87. package/src/person.ts +69 -1
  88. package/src/personConsent.ts +60 -0
  89. package/src/raise.ts +13 -0
  90. package/src/sampleData.ts +3 -0
  91. package/src/values.ts +169 -3
package/src/index.ts CHANGED
@@ -24,8 +24,20 @@ export {
24
24
  CANDIDATE_STATUS_LABELS,
25
25
  CANDIDATE_STATUSES,
26
26
  COMPANY_STAGES,
27
+ CONSENT_PURPOSE_STATUS_LABELS,
28
+ CONSENT_PURPOSE_STATUSES,
29
+ CONSENT_SOURCE_KINDS,
30
+ CONSENT_STATUS_LABELS,
31
+ CONSENT_STATUSES,
32
+ CUSTOM_FIELD_OBJECT_TYPE_LABELS,
33
+ CUSTOM_FIELD_OBJECT_TYPES,
34
+ CUSTOM_FIELD_TYPE_LABELS,
35
+ CUSTOM_FIELD_TYPES,
36
+ CUSTOM_FIELD_TYPES_WITH_OPTIONS,
27
37
  EXTENSIBLE_RECORD_TYPES,
28
38
  FIRST_INTERVIEW_STAGE,
39
+ FORM_ACTION_STATUSES,
40
+ FORM_BOOLEAN_FIELD_TYPES,
29
41
  FORM_FIELD_MAP_TARGET_LABELS,
30
42
  FORM_FIELD_MAP_TARGETS,
31
43
  FORM_FIELD_TYPES,
@@ -40,6 +52,7 @@ export {
40
52
  INVITE_STATUSES,
41
53
  MEMBER_ROLES,
42
54
  OPEN_PLAN_ITEM_STATUSES,
55
+ PERSON_CONSENT_TARGET,
43
56
  PERSON_EMAIL_TARGET,
44
57
  PIPELINE_KIND_LABELS,
45
58
  PIPELINE_KINDS,
@@ -71,7 +84,15 @@ export type {
71
84
  ApiKeyKind,
72
85
  CandidateStatus,
73
86
  CompanyStage,
87
+ ConsentPurposeStatus,
88
+ ConsentSourceKind,
89
+ ConsentStatus,
90
+ CustomFieldObjectType,
91
+ CustomFieldType,
92
+ CustomFieldTypeWithOptions,
74
93
  ExtensibleRecordType,
94
+ FormActionStatus,
95
+ FormBooleanFieldType,
75
96
  FormFieldMapTarget,
76
97
  FormFieldType,
77
98
  FormOptionValueType,
@@ -101,6 +122,38 @@ export type {
101
122
  export { definedFields, idSchema, nullableTimestampSchema, timestampSchema } from './wire.ts'
102
123
  export type { RecordTimestamps } from './wire.ts'
103
124
 
125
+ export {
126
+ consentPurposeBody,
127
+ consentPurposeSchema,
128
+ createConsentPurposeBody,
129
+ } from './consentPurpose.ts'
130
+ export type {
131
+ ConsentPurpose,
132
+ ConsentPurposeInput,
133
+ CreateConsentPurposeInput,
134
+ } from './consentPurpose.ts'
135
+
136
+ export { personConsentSchema, personConsentWriteBody } from './personConsent.ts'
137
+ export type { PersonConsent, PersonConsentInput } from './personConsent.ts'
138
+
139
+ export {
140
+ createCustomFieldDefinitionBody,
141
+ customFieldDefinitionBody,
142
+ customFieldDefinitionSchema,
143
+ customFieldValuesBody,
144
+ customFieldValuesSchema,
145
+ customFieldWireValue,
146
+ customFieldsPatchShape,
147
+ } from './customField.ts'
148
+ export type {
149
+ CreateCustomFieldDefinitionInput,
150
+ CustomFieldDefinition,
151
+ CustomFieldDefinitionInput,
152
+ CustomFieldValue,
153
+ CustomFieldValues,
154
+ CustomFieldWireValue,
155
+ } from './customField.ts'
156
+
104
157
  export {
105
158
  eventActorSchema,
106
159
  eventEnvelopeSchema,
@@ -109,6 +162,8 @@ export {
109
162
  } from './events.ts'
110
163
  export type { EventActor, EventTarget, KelpieEvent, UpdateEventData } from './events.ts'
111
164
 
165
+ export { composeName } from './names.ts'
166
+ export type { NameParts } from './names.ts'
112
167
  export { personBody, personSchema } from './person.ts'
113
168
  export type { Person, PersonInput, SocialProfile } from './person.ts'
114
169
 
@@ -124,6 +179,9 @@ export type { Deal, DealInput } from './deal.ts'
124
179
  export { opportunityBody, opportunitySchema } from './opportunity.ts'
125
180
  export type { Opportunity, OpportunityInput } from './opportunity.ts'
126
181
 
182
+ export { enquiryBody, enquirySchema } from './enquiry.ts'
183
+ export type { Enquiry, EnquiryInput } from './enquiry.ts'
184
+
127
185
  export { partnershipBody, partnershipSchema } from './partnership.ts'
128
186
  export type { Partnership, PartnershipInput } from './partnership.ts'
129
187
 
@@ -181,6 +239,7 @@ export { createFormBody, formBody, formSchema } from './form.ts'
181
239
  export type {
182
240
  CreateFormInput,
183
241
  Form,
242
+ FormAttachTarget,
184
243
  FormField,
185
244
  FormFieldInput,
186
245
  FormFieldOption,
@@ -188,8 +247,17 @@ export type {
188
247
  FormInput,
189
248
  } from './form.ts'
190
249
 
191
- export { formSubmissionSchema, formSubmitResultSchema } from './formSubmission.ts'
192
- export type { FormSubmission, FormSubmitResult } from './formSubmission.ts'
250
+ export {
251
+ FORM_SUBMISSION_LINK_TARGETS,
252
+ formSubmissionSchema,
253
+ formSubmitResultSchema,
254
+ } from './formSubmission.ts'
255
+ export type {
256
+ FormSubmission,
257
+ FormSubmissionActionEntry,
258
+ FormSubmissionLinkTarget,
259
+ FormSubmitResult,
260
+ } from './formSubmission.ts'
193
261
 
194
262
  export {
195
263
  CONFLICT_MODE_LABELS,
package/src/list.ts CHANGED
@@ -22,6 +22,13 @@ export interface List extends RecordTimestamps {
22
22
  readonly description: string | null
23
23
  readonly targetType: RecordTargetType
24
24
  readonly memberCount: number
25
+ /**
26
+ * The consent purpose captured when a form submit adds a person to this
27
+ * list. Person lists only. Manual and API list adds capture nothing.
28
+ */
29
+ readonly consentPurposeId: string | null
30
+ /** The named purpose's slug, denormalised so a reader needs no second call. */
31
+ readonly consentPurposeSlug: string | null
25
32
  }
26
33
 
27
34
  export const listSchema: z.ZodType<List, unknown> = z
@@ -31,6 +38,8 @@ export const listSchema: z.ZodType<List, unknown> = z
31
38
  description: z.string().nullable(),
32
39
  target_type: z.enum(RECORD_TARGET_TYPES),
33
40
  member_count: z.number().int().nonnegative(),
41
+ consent_purpose_id: idSchema.nullable(),
42
+ consent_purpose_slug: z.string().nullable(),
34
43
  ...recordTimestamps,
35
44
  })
36
45
  .transform(
@@ -40,6 +49,8 @@ export const listSchema: z.ZodType<List, unknown> = z
40
49
  description: wire.description,
41
50
  targetType: wire.target_type,
42
51
  memberCount: wire.member_count,
52
+ consentPurposeId: wire.consent_purpose_id,
53
+ consentPurposeSlug: wire.consent_purpose_slug,
43
54
  createdAt: wire.created_at,
44
55
  updatedAt: wire.updated_at,
45
56
  }),
@@ -49,6 +60,8 @@ export interface CreateListInput {
49
60
  readonly name: string
50
61
  readonly targetType: RecordTargetType
51
62
  readonly description?: string | null
63
+ /** Person lists only. `null` on a company list is fine; a non-null value is `422`. */
64
+ readonly consentPurposeId?: string | null
52
65
  }
53
66
 
54
67
  export function createListBody(input: CreateListInput): Record<string, unknown> {
@@ -56,6 +69,7 @@ export function createListBody(input: CreateListInput): Record<string, unknown>
56
69
  name: input.name,
57
70
  target_type: input.targetType,
58
71
  description: input.description,
72
+ consent_purpose_id: input.consentPurposeId,
59
73
  })
60
74
  }
61
75
 
@@ -63,11 +77,13 @@ export function createListBody(input: CreateListInput): Record<string, unknown>
63
77
  export interface ListInput {
64
78
  readonly name?: string
65
79
  readonly description?: string | null
80
+ readonly consentPurposeId?: string | null
66
81
  }
67
82
 
68
83
  export function listBody(input: ListInput): Record<string, unknown> {
69
84
  return definedFields({
70
85
  name: input.name,
71
86
  description: input.description,
87
+ consent_purpose_id: input.consentPurposeId,
72
88
  })
73
89
  }
package/src/names.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Building a person's display name out of its parts.
3
+ *
4
+ * One direction only. A name is composed when a caller supplied parts and no
5
+ * name; parts are never derived by splitting a name. Splitting is a guess, and a
6
+ * stored guess is indistinguishable from something a person typed: "van der
7
+ * Berg" is one surname written as three words, "Ursula K. Le Guin" is four
8
+ * tokens over two names, a mononym is neither a first nor a last name, and half
9
+ * the world writes the family name first. `name` stays the record's canonical
10
+ * display string, which is why every consumer that renders a person keeps
11
+ * reading it and nothing else.
12
+ */
13
+
14
+ /**
15
+ * The parts a display name is built from, in the order they are written.
16
+ *
17
+ * Each accepts `undefined` as well as `null` so that a caller holding either
18
+ * shape — a nullable column, an absent CSV cell, an omitted request field — can
19
+ * pass what it has without first flattening the two. Both mean the same thing
20
+ * here: nothing to contribute.
21
+ */
22
+ export interface NameParts {
23
+ readonly firstName?: string | null | undefined
24
+ readonly lastName?: string | null | undefined
25
+ readonly suffix?: string | null | undefined
26
+ }
27
+
28
+ /**
29
+ * The parts joined, single-spaced.
30
+ *
31
+ * `salutation` is deliberately not a parameter. "Mr" is a form of address rather
32
+ * than part of the name, and a list of people reading "Mr John Smith" is a list
33
+ * nobody asked for. vCard draws the same line: honorific prefixes sit in `N`,
34
+ * away from the `FN` a client displays.
35
+ *
36
+ * The order is fixed rather than per-locale, because this composes a *fallback*
37
+ * for a record that arrived without a display name. A workspace that writes the
38
+ * family name first sets `name` itself, which is the whole reason that field is
39
+ * the canonical one.
40
+ *
41
+ * @returns The composed name, or `''` when no part carried anything. An empty
42
+ * answer is the caller's signal that there is nothing here to fall back to.
43
+ */
44
+ export function composeName(parts: NameParts): string {
45
+ return [parts.firstName, parts.lastName, parts.suffix]
46
+ .map((part) => (part ?? '').trim())
47
+ .filter((part) => part.length > 0)
48
+ .join(' ')
49
+ }
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod'
2
2
 
3
+ import { customFieldValuesBody, customFieldValuesSchema } from './customField.ts'
4
+ import type { CustomFieldValue, CustomFieldValues } from './customField.ts'
3
5
  import { definedFields, idSchema, recordTimestamps } from './wire.ts'
4
6
  import type { RecordTimestamps } from './wire.ts'
5
7
 
@@ -16,8 +18,11 @@ export interface Opportunity extends RecordTimestamps {
16
18
  readonly ownerId: string | null
17
19
  /** `YYYY-MM-DD`, per `api.md` date-only fields. */
18
20
  readonly expectedClose: string | null
21
+ readonly personIds: readonly string[]
19
22
  readonly summary: string
20
23
  readonly tags: readonly string[]
24
+ /** Workspace-defined fields, keyed by definition key. Always present (default `{}`). */
25
+ readonly customFields: CustomFieldValues
21
26
  }
22
27
 
23
28
  export const opportunitySchema: z.ZodType<Opportunity, unknown> = z
@@ -29,8 +34,10 @@ export const opportunitySchema: z.ZodType<Opportunity, unknown> = z
29
34
  company_id: idSchema.nullable(),
30
35
  owner_id: idSchema.nullable(),
31
36
  expected_close: z.string().nullable(),
37
+ person_ids: z.array(idSchema),
32
38
  summary: z.string(),
33
39
  tags: z.array(z.string()),
40
+ custom_fields: customFieldValuesSchema,
34
41
  ...recordTimestamps,
35
42
  })
36
43
  .transform(
@@ -42,8 +49,10 @@ export const opportunitySchema: z.ZodType<Opportunity, unknown> = z
42
49
  companyId: wire.company_id,
43
50
  ownerId: wire.owner_id,
44
51
  expectedClose: wire.expected_close,
52
+ personIds: wire.person_ids,
45
53
  summary: wire.summary,
46
54
  tags: wire.tags,
55
+ customFields: wire.custom_fields,
47
56
  createdAt: wire.created_at,
48
57
  updatedAt: wire.updated_at,
49
58
  }),
@@ -56,8 +65,14 @@ export interface OpportunityInput {
56
65
  readonly companyId?: string | null
57
66
  readonly ownerId?: string | null
58
67
  readonly expectedClose?: string | null
68
+ readonly personIds?: readonly string[]
59
69
  readonly summary?: string
60
70
  readonly tags?: readonly string[]
71
+ /**
72
+ * Partial merge patch: sent keys change, `null` clears a key, absent keys are
73
+ * left alone. Unknown keys are rejected at `422`.
74
+ */
75
+ readonly customFields?: Readonly<Record<string, CustomFieldValue | null>>
61
76
  }
62
77
 
63
78
  export function opportunityBody(input: OpportunityInput): Record<string, unknown> {
@@ -68,7 +83,10 @@ export function opportunityBody(input: OpportunityInput): Record<string, unknown
68
83
  company_id: input.companyId,
69
84
  owner_id: input.ownerId,
70
85
  expected_close: input.expectedClose,
86
+ person_ids: input.personIds,
71
87
  summary: input.summary,
72
88
  tags: input.tags,
89
+ custom_fields:
90
+ input.customFields === undefined ? undefined : customFieldValuesBody(input.customFields),
73
91
  })
74
92
  }
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod'
2
2
 
3
+ import { customFieldValuesBody, customFieldValuesSchema } from './customField.ts'
4
+ import type { CustomFieldValue, CustomFieldValues } from './customField.ts'
3
5
  import { definedFields, idSchema, recordTimestamps } from './wire.ts'
4
6
  import type { RecordTimestamps } from './wire.ts'
5
7
 
@@ -21,6 +23,8 @@ export interface Partnership extends RecordTimestamps {
21
23
  readonly personIds: readonly string[]
22
24
  readonly summary: string
23
25
  readonly tags: readonly string[]
26
+ /** Workspace-defined fields, keyed by definition key. Always present (default `{}`). */
27
+ readonly customFields: CustomFieldValues
24
28
  }
25
29
 
26
30
  export const partnershipSchema: z.ZodType<Partnership, unknown> = z
@@ -37,6 +41,7 @@ export const partnershipSchema: z.ZodType<Partnership, unknown> = z
37
41
  person_ids: z.array(idSchema),
38
42
  summary: z.string(),
39
43
  tags: z.array(z.string()),
44
+ custom_fields: customFieldValuesSchema,
40
45
  ...recordTimestamps,
41
46
  })
42
47
  .transform(
@@ -53,6 +58,7 @@ export const partnershipSchema: z.ZodType<Partnership, unknown> = z
53
58
  personIds: wire.person_ids,
54
59
  summary: wire.summary,
55
60
  tags: wire.tags,
61
+ customFields: wire.custom_fields,
56
62
  createdAt: wire.created_at,
57
63
  updatedAt: wire.updated_at,
58
64
  }),
@@ -70,6 +76,11 @@ export interface PartnershipInput {
70
76
  readonly personIds?: readonly string[]
71
77
  readonly summary?: string
72
78
  readonly tags?: readonly string[]
79
+ /**
80
+ * Partial merge patch: sent keys change, `null` clears a key, absent keys are
81
+ * left alone. Unknown keys are rejected at `422`.
82
+ */
83
+ readonly customFields?: Readonly<Record<string, CustomFieldValue | null>>
73
84
  }
74
85
 
75
86
  export function partnershipBody(input: PartnershipInput): Record<string, unknown> {
@@ -85,5 +96,7 @@ export function partnershipBody(input: PartnershipInput): Record<string, unknown
85
96
  person_ids: input.personIds,
86
97
  summary: input.summary,
87
98
  tags: input.tags,
99
+ custom_fields:
100
+ input.customFields === undefined ? undefined : customFieldValuesBody(input.customFields),
88
101
  })
89
102
  }
package/src/person.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { z } from 'zod'
2
2
 
3
+ import {
4
+ customFieldValuesBody,
5
+ customFieldValuesSchema,
6
+ } from './customField.ts'
7
+ import type { CustomFieldValue, CustomFieldValues } from './customField.ts'
8
+ import { personConsentSchema, personConsentWriteBody } from './personConsent.ts'
9
+ import type { PersonConsent, PersonConsentInput } from './personConsent.ts'
3
10
  import {
4
11
  INFLUENCE_LEVELS,
5
12
  PREFERRED_CHANNELS,
@@ -19,7 +26,19 @@ export interface SocialProfile {
19
26
 
20
27
  export interface Person extends RecordTimestamps {
21
28
  readonly id: string
29
+ /**
30
+ * What to call this person: the canonical display string, and the only name
31
+ * field every consumer reads. The parts below are optional detail beside it,
32
+ * never a replacement for it, so a mononym and a four-part name are both just
33
+ * a `name`.
34
+ */
22
35
  readonly name: string
36
+ /** A form of address — "Dr", "Ms". Not a job title: that is on Position. */
37
+ readonly salutation: string | null
38
+ readonly firstName: string | null
39
+ readonly lastName: string | null
40
+ /** A generational or post-nominal suffix — "Jr", "III", "PhD". */
41
+ readonly suffix: string | null
23
42
  readonly email: string | null
24
43
  readonly phones: readonly string[]
25
44
  readonly socialProfiles: readonly SocialProfile[]
@@ -31,6 +50,16 @@ export interface Person extends RecordTimestamps {
31
50
  readonly summary: string
32
51
  readonly tags: readonly string[]
33
52
  readonly lastContactedAt: Date | null
53
+ /** The Article 21 objection. Global, independent of consent purposes. */
54
+ readonly doNotContact: boolean
55
+ /**
56
+ * One entry per workspace consent purpose, with the effective status
57
+ * (an explicit `person_consents` row if one exists, else the purpose's
58
+ * `default_status`). Ordered by the purpose's `sort_order`.
59
+ */
60
+ readonly consents: readonly PersonConsent[]
61
+ /** Workspace-defined fields, keyed by definition key. Always present (default `{}`). */
62
+ readonly customFields: CustomFieldValues
34
63
  }
35
64
 
36
65
  const socialProfileSchema = z.object({
@@ -42,6 +71,10 @@ export const personSchema: z.ZodType<Person, unknown> = z
42
71
  .object({
43
72
  id: idSchema,
44
73
  name: z.string(),
74
+ salutation: z.string().nullable(),
75
+ first_name: z.string().nullable(),
76
+ last_name: z.string().nullable(),
77
+ suffix: z.string().nullable(),
45
78
  email: z.string().nullable(),
46
79
  phones: z.array(z.string()),
47
80
  social_profiles: z.array(socialProfileSchema),
@@ -53,12 +86,19 @@ export const personSchema: z.ZodType<Person, unknown> = z
53
86
  summary: z.string(),
54
87
  tags: z.array(z.string()),
55
88
  last_contacted_at: nullableTimestampSchema,
89
+ do_not_contact: z.boolean(),
90
+ consents: z.array(personConsentSchema),
91
+ custom_fields: customFieldValuesSchema,
56
92
  ...recordTimestamps,
57
93
  })
58
94
  .transform(
59
95
  (wire): Person => ({
60
96
  id: wire.id,
61
97
  name: wire.name,
98
+ salutation: wire.salutation,
99
+ firstName: wire.first_name,
100
+ lastName: wire.last_name,
101
+ suffix: wire.suffix,
62
102
  email: wire.email,
63
103
  phones: wire.phones,
64
104
  socialProfiles: wire.social_profiles,
@@ -70,6 +110,9 @@ export const personSchema: z.ZodType<Person, unknown> = z
70
110
  summary: wire.summary,
71
111
  tags: wire.tags,
72
112
  lastContactedAt: wire.last_contacted_at,
113
+ doNotContact: wire.do_not_contact,
114
+ consents: wire.consents,
115
+ customFields: wire.custom_fields,
73
116
  createdAt: wire.created_at,
74
117
  updatedAt: wire.updated_at,
75
118
  }),
@@ -78,10 +121,15 @@ export const personSchema: z.ZodType<Person, unknown> = z
78
121
  /**
79
122
  * A create or update body. Every field is optional because `POST` defaults all
80
123
  * but `name` server-side and `PATCH` sends only what changed; `name` is required
81
- * on create by the route, not by this type.
124
+ * on create by the route, not by this type — and a create may send `first_name`
125
+ * and `last_name` instead, which the route composes a `name` from.
82
126
  */
83
127
  export interface PersonInput {
84
128
  readonly name?: string
129
+ readonly salutation?: string | null
130
+ readonly firstName?: string | null
131
+ readonly lastName?: string | null
132
+ readonly suffix?: string | null
85
133
  readonly email?: string | null
86
134
  readonly phones?: readonly string[]
87
135
  readonly socialProfiles?: readonly SocialProfile[]
@@ -93,11 +141,27 @@ export interface PersonInput {
93
141
  readonly summary?: string
94
142
  readonly tags?: readonly string[]
95
143
  readonly lastContactedAt?: Date | null
144
+ readonly doNotContact?: boolean
145
+ /**
146
+ * Manual override of one or more consent purposes. Each entry upserts a
147
+ * `person_consents` row (`source: manual`); `status: null` clears the row
148
+ * and inherits the purpose's default. Absent purposes are left alone.
149
+ */
150
+ readonly consents?: readonly PersonConsentInput[]
151
+ /**
152
+ * Partial merge patch: sent keys change, `null` clears a key, absent keys are
153
+ * left alone. Unknown keys are rejected at `422`.
154
+ */
155
+ readonly customFields?: Readonly<Record<string, CustomFieldValue | null>>
96
156
  }
97
157
 
98
158
  export function personBody(input: PersonInput): Record<string, unknown> {
99
159
  return definedFields({
100
160
  name: input.name,
161
+ salutation: input.salutation,
162
+ first_name: input.firstName,
163
+ last_name: input.lastName,
164
+ suffix: input.suffix,
101
165
  email: input.email,
102
166
  phones: input.phones,
103
167
  social_profiles: input.socialProfiles,
@@ -112,5 +176,9 @@ export function personBody(input: PersonInput): Record<string, unknown> {
112
176
  input.lastContactedAt === undefined || input.lastContactedAt === null
113
177
  ? input.lastContactedAt
114
178
  : input.lastContactedAt.toISOString(),
179
+ do_not_contact: input.doNotContact,
180
+ consents: input.consents?.map(personConsentWriteBody),
181
+ custom_fields:
182
+ input.customFields === undefined ? undefined : customFieldValuesBody(input.customFields),
115
183
  })
116
184
  }
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod'
2
+
3
+ import { CONSENT_PURPOSE_STATUSES } from './values.ts'
4
+ import type { ConsentPurposeStatus, ConsentStatus } from './values.ts'
5
+ import { definedFields, nullableTimestampSchema } from './wire.ts'
6
+
7
+ /**
8
+ * The effective consent status for a (person, purpose) pair.
9
+ *
10
+ * `inherited: true` means the person has no explicit `person_consents` row for
11
+ * the purpose and the reader is looking at the purpose's `default_status`. In
12
+ * that case `source` and `notedAt` are null. `inherited: false` means the row
13
+ * exists and carries its own `granted | withdrawn`, `source`, and `notedAt`.
14
+ */
15
+ export interface PersonConsent {
16
+ /** The purpose's slug — the identity that survives a rename of its label. */
17
+ readonly purposeSlug: string
18
+ readonly purposeLabel: string
19
+ readonly status: ConsentPurposeStatus
20
+ readonly source: string | null
21
+ readonly notedAt: Date | null
22
+ readonly inherited: boolean
23
+ }
24
+
25
+ export const personConsentSchema: z.ZodType<PersonConsent, unknown> = z
26
+ .object({
27
+ purpose_slug: z.string(),
28
+ purpose_label: z.string(),
29
+ status: z.enum(CONSENT_PURPOSE_STATUSES),
30
+ source: z.string().nullable(),
31
+ noted_at: nullableTimestampSchema,
32
+ inherited: z.boolean(),
33
+ })
34
+ .transform(
35
+ (wire): PersonConsent => ({
36
+ purposeSlug: wire.purpose_slug,
37
+ purposeLabel: wire.purpose_label,
38
+ status: wire.status,
39
+ source: wire.source,
40
+ notedAt: wire.noted_at,
41
+ inherited: wire.inherited,
42
+ }),
43
+ )
44
+
45
+ /**
46
+ * What a manual override writes for one (person, purpose) pair. A `status` of
47
+ * `granted` or `withdrawn` upserts the row (`source: manual`); a `null` status
48
+ * clears the row and falls the pair back to the purpose's default.
49
+ */
50
+ export interface PersonConsentInput {
51
+ readonly purposeSlug: string
52
+ readonly status: ConsentStatus | null
53
+ }
54
+
55
+ export function personConsentWriteBody(input: PersonConsentInput): Record<string, unknown> {
56
+ return definedFields({
57
+ purpose_slug: input.purposeSlug,
58
+ status: input.status,
59
+ })
60
+ }
package/src/raise.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod'
2
2
 
3
+ import { customFieldValuesBody, customFieldValuesSchema } from './customField.ts'
4
+ import type { CustomFieldValue, CustomFieldValues } from './customField.ts'
3
5
  import { definedFields, idSchema, recordTimestamps } from './wire.ts'
4
6
  import type { RecordTimestamps } from './wire.ts'
5
7
 
@@ -22,6 +24,8 @@ export interface Raise extends RecordTimestamps {
22
24
  readonly personIds: readonly string[]
23
25
  readonly summary: string
24
26
  readonly tags: readonly string[]
27
+ /** Workspace-defined fields, keyed by definition key. Always present (default `{}`). */
28
+ readonly customFields: CustomFieldValues
25
29
  }
26
30
 
27
31
  export const raiseSchema: z.ZodType<Raise, unknown> = z
@@ -39,6 +43,7 @@ export const raiseSchema: z.ZodType<Raise, unknown> = z
39
43
  person_ids: z.array(idSchema),
40
44
  summary: z.string(),
41
45
  tags: z.array(z.string()),
46
+ custom_fields: customFieldValuesSchema,
42
47
  ...recordTimestamps,
43
48
  })
44
49
  .transform(
@@ -56,6 +61,7 @@ export const raiseSchema: z.ZodType<Raise, unknown> = z
56
61
  personIds: wire.person_ids,
57
62
  summary: wire.summary,
58
63
  tags: wire.tags,
64
+ customFields: wire.custom_fields,
59
65
  createdAt: wire.created_at,
60
66
  updatedAt: wire.updated_at,
61
67
  }),
@@ -74,6 +80,11 @@ export interface RaiseInput {
74
80
  readonly personIds?: readonly string[]
75
81
  readonly summary?: string
76
82
  readonly tags?: readonly string[]
83
+ /**
84
+ * Partial merge patch: sent keys change, `null` clears a key, absent keys are
85
+ * left alone. Unknown keys are rejected at `422`.
86
+ */
87
+ readonly customFields?: Readonly<Record<string, CustomFieldValue | null>>
77
88
  }
78
89
 
79
90
  export function raiseBody(input: RaiseInput): Record<string, unknown> {
@@ -90,5 +101,7 @@ export function raiseBody(input: RaiseInput): Record<string, unknown> {
90
101
  person_ids: input.personIds,
91
102
  summary: input.summary,
92
103
  tags: input.tags,
104
+ custom_fields:
105
+ input.customFields === undefined ? undefined : customFieldValuesBody(input.customFields),
93
106
  })
94
107
  }
package/src/sampleData.ts CHANGED
@@ -17,6 +17,7 @@ export interface SampleDataCounts {
17
17
  readonly opportunities: number
18
18
  readonly raises: number
19
19
  readonly partnerships: number
20
+ readonly enquiries: number
20
21
  readonly roles: number
21
22
  readonly candidates: number
22
23
  }
@@ -32,6 +33,7 @@ export const sampleDataCountsSchema: z.ZodType<SampleDataCounts, unknown> = z
32
33
  opportunities: z.number().int().nonnegative(),
33
34
  raises: z.number().int().nonnegative(),
34
35
  partnerships: z.number().int().nonnegative(),
36
+ enquiries: z.number().int().nonnegative(),
35
37
  roles: z.number().int().nonnegative(),
36
38
  candidates: z.number().int().nonnegative(),
37
39
  })
@@ -46,6 +48,7 @@ export const sampleDataCountsSchema: z.ZodType<SampleDataCounts, unknown> = z
46
48
  opportunities: wire.opportunities,
47
49
  raises: wire.raises,
48
50
  partnerships: wire.partnerships,
51
+ enquiries: wire.enquiries,
49
52
  roles: wire.roles,
50
53
  candidates: wire.candidates,
51
54
  }),