@kelpie/schemas 0.7.0 → 0.9.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 (111) hide show
  1. package/dist/account.d.ts +35 -0
  2. package/dist/account.d.ts.map +1 -1
  3. package/dist/account.js +18 -0
  4. package/dist/account.js.map +1 -1
  5. package/dist/company.d.ts +11 -0
  6. package/dist/company.d.ts.map +1 -1
  7. package/dist/company.js +7 -0
  8. package/dist/company.js.map +1 -1
  9. package/dist/consentPurpose.d.ts +49 -0
  10. package/dist/consentPurpose.d.ts.map +1 -0
  11. package/dist/consentPurpose.js +40 -0
  12. package/dist/consentPurpose.js.map +1 -0
  13. package/dist/customField.d.ts +99 -0
  14. package/dist/customField.d.ts.map +1 -0
  15. package/dist/customField.js +133 -0
  16. package/dist/customField.js.map +1 -0
  17. package/dist/deal.d.ts +8 -0
  18. package/dist/deal.d.ts.map +1 -1
  19. package/dist/deal.js +4 -0
  20. package/dist/deal.js.map +1 -1
  21. package/dist/emailDomainLinker.d.ts +16 -0
  22. package/dist/emailDomainLinker.d.ts.map +1 -0
  23. package/dist/emailDomainLinker.js +11 -0
  24. package/dist/emailDomainLinker.js.map +1 -0
  25. package/dist/enquiry.d.ts +43 -0
  26. package/dist/enquiry.d.ts.map +1 -0
  27. package/dist/enquiry.js +47 -0
  28. package/dist/enquiry.js.map +1 -0
  29. package/dist/form.d.ts +117 -2
  30. package/dist/form.d.ts.map +1 -1
  31. package/dist/form.js +102 -1
  32. package/dist/form.js.map +1 -1
  33. package/dist/formSubmission.d.ts +29 -0
  34. package/dist/formSubmission.d.ts.map +1 -1
  35. package/dist/formSubmission.js +34 -0
  36. package/dist/formSubmission.js.map +1 -1
  37. package/dist/importExport.d.ts +28 -4
  38. package/dist/importExport.d.ts.map +1 -1
  39. package/dist/importExport.js +25 -2
  40. package/dist/importExport.js.map +1 -1
  41. package/dist/index.d.ts +27 -7
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +13 -3
  44. package/dist/index.js.map +1 -1
  45. package/dist/list.d.ts +44 -0
  46. package/dist/list.d.ts.map +1 -0
  47. package/dist/list.js +41 -0
  48. package/dist/list.js.map +1 -0
  49. package/dist/listMember.d.ts +24 -0
  50. package/dist/listMember.d.ts.map +1 -0
  51. package/dist/listMember.js +27 -0
  52. package/dist/listMember.js.map +1 -0
  53. package/dist/listMembership.d.ts +20 -0
  54. package/dist/listMembership.d.ts.map +1 -0
  55. package/dist/listMembership.js +23 -0
  56. package/dist/listMembership.js.map +1 -0
  57. package/dist/names.d.ts +43 -0
  58. package/dist/names.d.ts.map +1 -0
  59. package/dist/names.js +35 -0
  60. package/dist/names.js.map +1 -0
  61. package/dist/opportunity.d.ts +10 -0
  62. package/dist/opportunity.d.ts.map +1 -1
  63. package/dist/opportunity.js +7 -0
  64. package/dist/opportunity.js.map +1 -1
  65. package/dist/partnership.d.ts +8 -0
  66. package/dist/partnership.d.ts.map +1 -1
  67. package/dist/partnership.js +4 -0
  68. package/dist/partnership.js.map +1 -1
  69. package/dist/person.d.ts +42 -1
  70. package/dist/person.d.ts.map +1 -1
  71. package/dist/person.js +23 -0
  72. package/dist/person.js.map +1 -1
  73. package/dist/personConsent.d.ts +31 -0
  74. package/dist/personConsent.d.ts.map +1 -0
  75. package/dist/personConsent.js +27 -0
  76. package/dist/personConsent.js.map +1 -0
  77. package/dist/raise.d.ts +8 -0
  78. package/dist/raise.d.ts.map +1 -1
  79. package/dist/raise.js +4 -0
  80. package/dist/raise.js.map +1 -1
  81. package/dist/sampleData.d.ts +23 -0
  82. package/dist/sampleData.d.ts.map +1 -0
  83. package/dist/sampleData.js +31 -0
  84. package/dist/sampleData.js.map +1 -0
  85. package/dist/values.d.ts +88 -8
  86. package/dist/values.d.ts.map +1 -1
  87. package/dist/values.js +150 -3
  88. package/dist/values.js.map +1 -1
  89. package/package.json +1 -1
  90. package/src/account.ts +56 -0
  91. package/src/company.ts +19 -0
  92. package/src/consentPurpose.ts +95 -0
  93. package/src/customField.ts +211 -0
  94. package/src/deal.ts +13 -0
  95. package/src/emailDomainLinker.ts +28 -0
  96. package/src/enquiry.ts +94 -0
  97. package/src/form.ts +230 -2
  98. package/src/formSubmission.ts +63 -0
  99. package/src/importExport.ts +45 -6
  100. package/src/index.ts +86 -2
  101. package/src/list.ts +89 -0
  102. package/src/listMember.ts +54 -0
  103. package/src/listMembership.ts +45 -0
  104. package/src/names.ts +49 -0
  105. package/src/opportunity.ts +18 -0
  106. package/src/partnership.ts +13 -0
  107. package/src/person.ts +69 -1
  108. package/src/personConsent.ts +60 -0
  109. package/src/raise.ts +13 -0
  110. package/src/sampleData.ts +55 -0
  111. package/src/values.ts +180 -3
package/src/company.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 { ACCOUNT_TYPES, COMPANY_STAGES, ICP_FITS, SIZE_BANDS } from './values.ts'
4
6
  import type { AccountType, CompanyStage, IcpFit, SizeBand } from './values.ts'
5
7
  import { definedFields, idSchema, recordTimestamps } from './wire.ts'
@@ -22,6 +24,10 @@ export interface Company extends RecordTimestamps {
22
24
  readonly techStack: readonly string[]
23
25
  readonly summary: string
24
26
  readonly tags: readonly string[]
27
+ /** True when this Company represents the workspace itself ("us"). */
28
+ readonly isOwn: boolean
29
+ /** Workspace-defined fields, keyed by definition key. Always present (default `{}`). */
30
+ readonly customFields: CustomFieldValues
25
31
  }
26
32
 
27
33
  export const companySchema: z.ZodType<Company, unknown> = z
@@ -40,6 +46,8 @@ export const companySchema: z.ZodType<Company, unknown> = z
40
46
  tech_stack: z.array(z.string()),
41
47
  summary: z.string(),
42
48
  tags: z.array(z.string()),
49
+ is_own: z.boolean(),
50
+ custom_fields: customFieldValuesSchema,
43
51
  ...recordTimestamps,
44
52
  })
45
53
  .transform(
@@ -58,6 +66,8 @@ export const companySchema: z.ZodType<Company, unknown> = z
58
66
  techStack: wire.tech_stack,
59
67
  summary: wire.summary,
60
68
  tags: wire.tags,
69
+ isOwn: wire.is_own,
70
+ customFields: wire.custom_fields,
61
71
  createdAt: wire.created_at,
62
72
  updatedAt: wire.updated_at,
63
73
  }),
@@ -77,6 +87,12 @@ export interface CompanyInput {
77
87
  readonly techStack?: readonly string[]
78
88
  readonly summary?: string
79
89
  readonly tags?: readonly string[]
90
+ readonly isOwn?: boolean
91
+ /**
92
+ * Partial merge patch: sent keys change, `null` clears a key, absent keys are
93
+ * left alone. Unknown keys are rejected at `422`.
94
+ */
95
+ readonly customFields?: Readonly<Record<string, CustomFieldValue | null>>
80
96
  }
81
97
 
82
98
  export function companyBody(input: CompanyInput): Record<string, unknown> {
@@ -94,5 +110,8 @@ export function companyBody(input: CompanyInput): Record<string, unknown> {
94
110
  tech_stack: input.techStack,
95
111
  summary: input.summary,
96
112
  tags: input.tags,
113
+ is_own: input.isOwn,
114
+ custom_fields:
115
+ input.customFields === undefined ? undefined : customFieldValuesBody(input.customFields),
97
116
  })
98
117
  }
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod'
2
+
3
+ import { CONSENT_PURPOSE_STATUSES } from './values.ts'
4
+ import type { ConsentPurposeStatus } from './values.ts'
5
+ import { definedFields, idSchema, recordTimestamps } from './wire.ts'
6
+ import type { RecordTimestamps } from './wire.ts'
7
+
8
+ /**
9
+ * Wire and write shapes for `/v1/consent_purposes`.
10
+ *
11
+ * A workspace defines its own purposes (`contact`, `marketing`, …). Every
12
+ * capture site — forms, lists, imports, the manual override on a Person —
13
+ * names one. Modelled on `custom_field_definitions`: `slug` is immutable and
14
+ * behaves as the key, `default_status` is the workspace default a person
15
+ * without an explicit `person_consents` row inherits, and `sort_order` runs
16
+ * 0..N-1 with the service renumbering on reorder and delete.
17
+ */
18
+
19
+ /** A workspace's definition of one consent purpose. */
20
+ export interface ConsentPurpose extends RecordTimestamps {
21
+ readonly id: string
22
+ /** Immutable after create. Lowercase snake_case, matches `/^[a-z][a-z0-9_]*$/`. */
23
+ readonly slug: string
24
+ readonly label: string
25
+ readonly description: string
26
+ /** The workspace default a person without an explicit row inherits. */
27
+ readonly defaultStatus: ConsentPurposeStatus
28
+ readonly sortOrder: number
29
+ }
30
+
31
+ export const consentPurposeSchema: z.ZodType<ConsentPurpose, unknown> = z
32
+ .object({
33
+ id: idSchema,
34
+ slug: z.string(),
35
+ label: z.string(),
36
+ description: z.string(),
37
+ default_status: z.enum(CONSENT_PURPOSE_STATUSES),
38
+ sort_order: z.number().int(),
39
+ ...recordTimestamps,
40
+ })
41
+ .transform(
42
+ (wire): ConsentPurpose => ({
43
+ id: wire.id,
44
+ slug: wire.slug,
45
+ label: wire.label,
46
+ description: wire.description,
47
+ defaultStatus: wire.default_status,
48
+ sortOrder: wire.sort_order,
49
+ createdAt: wire.created_at,
50
+ updatedAt: wire.updated_at,
51
+ }),
52
+ )
53
+
54
+ /**
55
+ * The body a create takes. `slug` is only written here; a `PATCH` cannot
56
+ * change it — a rename would strand every stored `person_consents` row and
57
+ * every form/list/import that names the purpose.
58
+ */
59
+ export interface CreateConsentPurposeInput {
60
+ readonly slug: string
61
+ readonly label: string
62
+ readonly description?: string
63
+ readonly defaultStatus?: ConsentPurposeStatus
64
+ }
65
+
66
+ export function createConsentPurposeBody(
67
+ input: CreateConsentPurposeInput,
68
+ ): Record<string, unknown> {
69
+ return definedFields({
70
+ slug: input.slug,
71
+ label: input.label,
72
+ description: input.description,
73
+ default_status: input.defaultStatus,
74
+ })
75
+ }
76
+
77
+ /**
78
+ * What a `PATCH` may change. `slug` is fixed for the purpose's lifetime and
79
+ * the strict body naturally makes it a `422`.
80
+ */
81
+ export interface ConsentPurposeInput {
82
+ readonly label?: string
83
+ readonly description?: string
84
+ readonly defaultStatus?: ConsentPurposeStatus
85
+ readonly sortOrder?: number
86
+ }
87
+
88
+ export function consentPurposeBody(input: ConsentPurposeInput): Record<string, unknown> {
89
+ return definedFields({
90
+ label: input.label,
91
+ description: input.description,
92
+ default_status: input.defaultStatus,
93
+ sort_order: input.sortOrder,
94
+ })
95
+ }
@@ -0,0 +1,211 @@
1
+ import { z } from 'zod'
2
+
3
+ import {
4
+ CUSTOM_FIELD_OBJECT_TYPES,
5
+ CUSTOM_FIELD_TYPES,
6
+ } from './values.ts'
7
+ import type { CustomFieldObjectType, CustomFieldType } from './values.ts'
8
+ import { definedFields, idSchema, recordTimestamps } from './wire.ts'
9
+ import type { RecordTimestamps } from './wire.ts'
10
+
11
+ /**
12
+ * Wire and write shapes for `/v1/custom_fields`.
13
+ *
14
+ * The definition is workspace-scoped. Every record on the six object types
15
+ * carries a `custom_fields` object keyed by definition key; the values are the
16
+ * shape this file describes. The Zod layer here only checks structure — that a
17
+ * value looks like text / number / boolean / a string list / a money object —
18
+ * because the workspace's definitions are read from the database, not the
19
+ * request. Definition-aware checks (this key exists on this object type, this
20
+ * select value is one of the options) run in the service.
21
+ *
22
+ * Merge semantics on the record's `custom_fields` (documented in `api.md`): a
23
+ * `PATCH` sends the keys that change; `null` clears a key; an unknown key is
24
+ * `422` naming `custom_fields.<key>`.
25
+ */
26
+
27
+ /**
28
+ * The value union a custom field can carry on the wire. Structural only — the
29
+ * validator in `@kelpie/server` reads the workspace's definitions and refuses a
30
+ * `number` written into a `text` field, an unknown `select` option, and so on.
31
+ *
32
+ * `text`, `long_text`, `url`, `date` and `select` all serialise as strings; a
33
+ * `multi_select` is a `string[]`; a `currency` is a small object; the rest are
34
+ * primitives. Nothing here reads a definition, so the caps below are the
35
+ * absolute ceilings meant to reject a runaway payload before it reaches the
36
+ * database, not the per-type limits (also enforced by the validator).
37
+ */
38
+ export const customFieldWireValue = z.union([
39
+ z.string().max(65536),
40
+ z.number(),
41
+ z.boolean(),
42
+ z.array(z.string().min(1).max(120)).max(100),
43
+ z.strictObject({
44
+ amount_cents: z.number().int(),
45
+ currency: z.string().regex(/^[A-Z]{3}$/u),
46
+ }),
47
+ ])
48
+
49
+ export type CustomFieldWireValue = z.infer<typeof customFieldWireValue>
50
+
51
+ /** A value already parsed and rebuilt as the record the UI holds. */
52
+ export type CustomFieldValue =
53
+ | string
54
+ | number
55
+ | boolean
56
+ | readonly string[]
57
+ | { readonly amountCents: number; readonly currency: string }
58
+
59
+ /** The `custom_fields` object every record on the six object types carries. */
60
+ export type CustomFieldValues = Readonly<Record<string, CustomFieldValue>>
61
+
62
+ /**
63
+ * Parses the wire object into the record shape the UI works with. Currency is
64
+ * the one type that changes shape (`amount_cents` → `amountCents`); everything
65
+ * else is the same value.
66
+ */
67
+ export const customFieldValuesSchema: z.ZodType<CustomFieldValues, unknown> = z
68
+ .record(z.string(), customFieldWireValue)
69
+ .transform((wire): CustomFieldValues => {
70
+ const out: Record<string, CustomFieldValue> = {}
71
+ for (const [key, value] of Object.entries(wire)) {
72
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
73
+ out[key] = { amountCents: value.amount_cents, currency: value.currency }
74
+ } else {
75
+ out[key] = value
76
+ }
77
+ }
78
+ return out
79
+ })
80
+
81
+ /**
82
+ * The shape a record's `PATCH` accepts for `custom_fields`. A key's value is
83
+ * the wire value, or `null` to clear that key; unknown keys fail at the service
84
+ * layer with `422 custom_fields.<key>`.
85
+ */
86
+ export const customFieldsPatchShape: z.ZodType<
87
+ Readonly<Record<string, CustomFieldWireValue | null>>,
88
+ unknown
89
+ > = z
90
+ .record(z.string().min(1).max(64), customFieldWireValue.nullable())
91
+ .describe(
92
+ 'Workspace-defined fields keyed by definition key. List the workspace\'s definitions with the custom_fields_list tool before writing. A key set to null clears the value; an unknown key is refused.',
93
+ )
94
+
95
+ /**
96
+ * Turns the UI's record shape back into the wire object for a `PATCH` body.
97
+ * The inverse of `customFieldValuesSchema`.
98
+ */
99
+ export function customFieldValuesBody(
100
+ values: Readonly<Record<string, CustomFieldValue | null>>,
101
+ ): Record<string, CustomFieldWireValue | null> {
102
+ const out: Record<string, CustomFieldWireValue | null> = {}
103
+ for (const [key, value] of Object.entries(values)) {
104
+ out[key] = wireCustomFieldValue(value)
105
+ }
106
+ return out
107
+ }
108
+
109
+ function wireCustomFieldValue(value: CustomFieldValue | null): CustomFieldWireValue | null {
110
+ if (value === null) return null
111
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
112
+ return value
113
+ }
114
+ // Array.isArray does not narrow a `readonly` array literal, so branch on the
115
+ // currency object's shape and treat everything else as the string list.
116
+ if ('amountCents' in value && 'currency' in value) {
117
+ return { amount_cents: value.amountCents, currency: value.currency }
118
+ }
119
+ return [...value]
120
+ }
121
+
122
+ /** A workspace's definition of one custom field, for one object type. */
123
+ export interface CustomFieldDefinition extends RecordTimestamps {
124
+ readonly id: string
125
+ readonly objectType: CustomFieldObjectType
126
+ /** Immutable after create. Lowercase snake_case, matches `/^[a-z][a-z0-9_]*$/`. */
127
+ readonly key: string
128
+ readonly label: string
129
+ /** Immutable after create. */
130
+ readonly type: CustomFieldType
131
+ /** Non-empty only for `select` and `multi_select`. */
132
+ readonly options: readonly string[]
133
+ readonly description: string
134
+ readonly sortOrder: number
135
+ }
136
+
137
+ export const customFieldDefinitionSchema: z.ZodType<CustomFieldDefinition, unknown> = z
138
+ .object({
139
+ id: idSchema,
140
+ object_type: z.enum(CUSTOM_FIELD_OBJECT_TYPES),
141
+ key: z.string(),
142
+ label: z.string(),
143
+ type: z.enum(CUSTOM_FIELD_TYPES),
144
+ options: z.array(z.string()),
145
+ description: z.string(),
146
+ sort_order: z.number().int(),
147
+ ...recordTimestamps,
148
+ })
149
+ .transform(
150
+ (wire): CustomFieldDefinition => ({
151
+ id: wire.id,
152
+ objectType: wire.object_type,
153
+ key: wire.key,
154
+ label: wire.label,
155
+ type: wire.type,
156
+ options: wire.options,
157
+ description: wire.description,
158
+ sortOrder: wire.sort_order,
159
+ createdAt: wire.created_at,
160
+ updatedAt: wire.updated_at,
161
+ }),
162
+ )
163
+
164
+ /**
165
+ * The body a create takes. `object_type`, `key`, and `type` are only written
166
+ * here; a `PATCH` cannot change any of them (see `CustomFieldDefinitionInput`).
167
+ */
168
+ export interface CreateCustomFieldDefinitionInput {
169
+ readonly objectType: CustomFieldObjectType
170
+ readonly key: string
171
+ readonly label: string
172
+ readonly type: CustomFieldType
173
+ readonly options?: readonly string[]
174
+ readonly description?: string
175
+ }
176
+
177
+ export function createCustomFieldDefinitionBody(
178
+ input: CreateCustomFieldDefinitionInput,
179
+ ): Record<string, unknown> {
180
+ return definedFields({
181
+ object_type: input.objectType,
182
+ key: input.key,
183
+ label: input.label,
184
+ type: input.type,
185
+ options: input.options,
186
+ description: input.description,
187
+ })
188
+ }
189
+
190
+ /**
191
+ * What a `PATCH` may change. `key` and `type` are fixed for the definition's
192
+ * lifetime — a change would strand records with the wrong value shape — and
193
+ * the strict body naturally makes either a `422`.
194
+ */
195
+ export interface CustomFieldDefinitionInput {
196
+ readonly label?: string
197
+ readonly options?: readonly string[]
198
+ readonly description?: string
199
+ readonly sortOrder?: number
200
+ }
201
+
202
+ export function customFieldDefinitionBody(
203
+ input: CustomFieldDefinitionInput,
204
+ ): Record<string, unknown> {
205
+ return definedFields({
206
+ label: input.label,
207
+ options: input.options,
208
+ description: input.description,
209
+ sort_order: input.sortOrder,
210
+ })
211
+ }
package/src/deal.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 Deal extends RecordTimestamps {
22
24
  readonly summary: string
23
25
  readonly tags: readonly string[]
24
26
  readonly externalId: string | null
27
+ /** Workspace-defined fields, keyed by definition key. Always present (default `{}`). */
28
+ readonly customFields: CustomFieldValues
25
29
  }
26
30
 
27
31
  export const dealSchema: z.ZodType<Deal, unknown> = z
@@ -41,6 +45,7 @@ export const dealSchema: z.ZodType<Deal, unknown> = z
41
45
  summary: z.string(),
42
46
  tags: z.array(z.string()),
43
47
  external_id: z.string().nullable(),
48
+ custom_fields: customFieldValuesSchema,
44
49
  ...recordTimestamps,
45
50
  })
46
51
  .transform(
@@ -60,6 +65,7 @@ export const dealSchema: z.ZodType<Deal, unknown> = z
60
65
  summary: wire.summary,
61
66
  tags: wire.tags,
62
67
  externalId: wire.external_id,
68
+ customFields: wire.custom_fields,
63
69
  createdAt: wire.created_at,
64
70
  updatedAt: wire.updated_at,
65
71
  }),
@@ -80,6 +86,11 @@ export interface DealInput {
80
86
  readonly summary?: string
81
87
  readonly tags?: readonly string[]
82
88
  readonly externalId?: string | null
89
+ /**
90
+ * Partial merge patch: sent keys change, `null` clears a key, absent keys are
91
+ * left alone. Unknown keys are rejected at `422`.
92
+ */
93
+ readonly customFields?: Readonly<Record<string, CustomFieldValue | null>>
83
94
  }
84
95
 
85
96
  export function dealBody(input: DealInput): Record<string, unknown> {
@@ -98,5 +109,7 @@ export function dealBody(input: DealInput): Record<string, unknown> {
98
109
  summary: input.summary,
99
110
  tags: input.tags,
100
111
  external_id: input.externalId,
112
+ custom_fields:
113
+ input.customFields === undefined ? undefined : customFieldValuesBody(input.customFields),
101
114
  })
102
115
  }
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Wire shape for `POST /v1/workspaces/:id/relink-email-domains`.
5
+ *
6
+ * The endpoint takes no body. The response counts what the sweep did: how
7
+ * many Companies were scanned and how many titleless Positions were newly
8
+ * created. A follow-up run on the same workspace returns the same count for
9
+ * `companies_scanned` and zero for `positions_created` — the sweep is
10
+ * idempotent (a person already linked to that company is skipped).
11
+ */
12
+
13
+ export interface RelinkEmailDomainsCounts {
14
+ readonly companiesScanned: number
15
+ readonly positionsCreated: number
16
+ }
17
+
18
+ export const relinkEmailDomainsCountsSchema: z.ZodType<RelinkEmailDomainsCounts, unknown> = z
19
+ .object({
20
+ companies_scanned: z.number().int().nonnegative(),
21
+ positions_created: z.number().int().nonnegative(),
22
+ })
23
+ .transform(
24
+ (wire): RelinkEmailDomainsCounts => ({
25
+ companiesScanned: wire.companies_scanned,
26
+ positionsCreated: wire.positions_created,
27
+ }),
28
+ )
package/src/enquiry.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { z } from 'zod'
2
+
3
+ import { customFieldValuesBody, customFieldValuesSchema } from './customField.ts'
4
+ import type { CustomFieldValue, CustomFieldValues } from './customField.ts'
5
+ import { definedFields, idSchema, recordTimestamps } from './wire.ts'
6
+ import type { RecordTimestamps } from './wire.ts'
7
+
8
+ /** Wire and write shapes for `/v1/enquiries`. */
9
+
10
+ export interface Enquiry extends RecordTimestamps {
11
+ readonly id: string
12
+ readonly name: string
13
+ /** Free text ("Website", "Email", "Referral", …), like Opportunity's `kind`. */
14
+ readonly source: string
15
+ readonly stageId: string
16
+ /** Nullable: an early enquiry may arrive before a company is on file. */
17
+ readonly companyId: string | null
18
+ readonly ownerId: string | null
19
+ /** Non-null once the enquiry has been converted to a Deal; nulled if that deal is deleted. */
20
+ readonly convertedDealId: string | null
21
+ readonly personIds: readonly string[]
22
+ readonly summary: string
23
+ readonly tags: readonly string[]
24
+ /** Workspace-defined fields, keyed by definition key. Always present (default `{}`). */
25
+ readonly customFields: CustomFieldValues
26
+ }
27
+
28
+ export const enquirySchema: z.ZodType<Enquiry, unknown> = z
29
+ .object({
30
+ id: idSchema,
31
+ name: z.string(),
32
+ source: z.string(),
33
+ stage_id: idSchema,
34
+ company_id: idSchema.nullable(),
35
+ owner_id: idSchema.nullable(),
36
+ converted_deal_id: idSchema.nullable(),
37
+ person_ids: z.array(idSchema),
38
+ summary: z.string(),
39
+ tags: z.array(z.string()),
40
+ custom_fields: customFieldValuesSchema,
41
+ ...recordTimestamps,
42
+ })
43
+ .transform(
44
+ (wire): Enquiry => ({
45
+ id: wire.id,
46
+ name: wire.name,
47
+ source: wire.source,
48
+ stageId: wire.stage_id,
49
+ companyId: wire.company_id,
50
+ ownerId: wire.owner_id,
51
+ convertedDealId: wire.converted_deal_id,
52
+ personIds: wire.person_ids,
53
+ summary: wire.summary,
54
+ tags: wire.tags,
55
+ customFields: wire.custom_fields,
56
+ createdAt: wire.created_at,
57
+ updatedAt: wire.updated_at,
58
+ }),
59
+ )
60
+
61
+ /**
62
+ * `convertedDealId` is deliberately absent from the input: it is set only by
63
+ * `POST /v1/enquiries/:id/convert`, and a `PATCH` that carried it is `422`.
64
+ */
65
+ export interface EnquiryInput {
66
+ readonly name?: string
67
+ readonly source?: string
68
+ readonly stageId?: string
69
+ readonly companyId?: string | null
70
+ readonly ownerId?: string | null
71
+ readonly personIds?: readonly string[]
72
+ readonly summary?: string
73
+ readonly tags?: readonly string[]
74
+ /**
75
+ * Partial merge patch: sent keys change, `null` clears a key, absent keys are
76
+ * left alone. Unknown keys are rejected at `422`.
77
+ */
78
+ readonly customFields?: Readonly<Record<string, CustomFieldValue | null>>
79
+ }
80
+
81
+ export function enquiryBody(input: EnquiryInput): Record<string, unknown> {
82
+ return definedFields({
83
+ name: input.name,
84
+ source: input.source,
85
+ stage_id: input.stageId,
86
+ company_id: input.companyId,
87
+ owner_id: input.ownerId,
88
+ person_ids: input.personIds,
89
+ summary: input.summary,
90
+ tags: input.tags,
91
+ custom_fields:
92
+ input.customFields === undefined ? undefined : customFieldValuesBody(input.customFields),
93
+ })
94
+ }