@kelpie/schemas 0.10.0 → 0.11.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/README.md +16 -0
- package/dist/apiKey.d.ts +5 -0
- package/dist/apiKey.d.ts.map +1 -1
- package/dist/apiKey.js +8 -1
- package/dist/apiKey.js.map +1 -1
- package/dist/apiKeyScopes.d.ts +36 -0
- package/dist/apiKeyScopes.d.ts.map +1 -0
- package/dist/apiKeyScopes.js +217 -0
- package/dist/apiKeyScopes.js.map +1 -0
- package/dist/conversion.d.ts +58 -0
- package/dist/conversion.d.ts.map +1 -0
- package/dist/conversion.js +63 -0
- package/dist/conversion.js.map +1 -0
- package/dist/deal.d.ts +3 -0
- package/dist/deal.d.ts.map +1 -1
- package/dist/deal.js +5 -0
- package/dist/deal.js.map +1 -1
- package/dist/enquiry.d.ts +3 -0
- package/dist/enquiry.d.ts.map +1 -1
- package/dist/enquiry.js +5 -0
- package/dist/enquiry.js.map +1 -1
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +2 -2
- package/dist/form.js.map +1 -1
- package/dist/formMapTargets.d.ts +72 -0
- package/dist/formMapTargets.d.ts.map +1 -0
- package/dist/formMapTargets.js +385 -0
- package/dist/formMapTargets.js.map +1 -0
- package/dist/importExport.d.ts +14 -1
- package/dist/importExport.d.ts.map +1 -1
- package/dist/importExport.js +161 -1
- package/dist/importExport.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/list.d.ts +0 -10
- package/dist/list.d.ts.map +1 -1
- package/dist/list.js +0 -6
- package/dist/list.js.map +1 -1
- package/dist/opportunity.d.ts +3 -0
- package/dist/opportunity.d.ts.map +1 -1
- package/dist/opportunity.js +5 -0
- package/dist/opportunity.js.map +1 -1
- package/dist/partnership.d.ts +3 -0
- package/dist/partnership.d.ts.map +1 -1
- package/dist/partnership.js +5 -0
- package/dist/partnership.js.map +1 -1
- package/dist/raise.d.ts +3 -0
- package/dist/raise.d.ts.map +1 -1
- package/dist/raise.js +5 -0
- package/dist/raise.js.map +1 -1
- package/dist/session.d.ts +1 -11
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +0 -6
- package/dist/session.js.map +1 -1
- package/dist/values.d.ts +4 -3
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +2 -0
- package/dist/values.js.map +1 -1
- package/package.json +1 -1
- package/src/apiKey.ts +13 -1
- package/src/apiKeyScopes.ts +264 -0
- package/src/conversion.ts +99 -0
- package/src/deal.ts +9 -0
- package/src/enquiry.ts +9 -0
- package/src/form.ts +1 -2
- package/src/formMapTargets.ts +548 -0
- package/src/importExport.ts +174 -1
- package/src/index.ts +61 -0
- package/src/list.ts +0 -16
- package/src/opportunity.ts +9 -0
- package/src/partnership.ts +9 -0
- package/src/raise.ts +9 -0
- package/src/session.ts +1 -17
- package/src/values.ts +7 -2
package/src/deal.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
+
import { convertedToWireSchema } from './conversion.ts'
|
|
4
|
+
import type { ConvertedTo } from './conversion.ts'
|
|
3
5
|
import { customFieldValuesBody, customFieldValuesSchema } from './customField.ts'
|
|
4
6
|
import type { CustomFieldValue, CustomFieldValues } from './customField.ts'
|
|
5
7
|
import { definedFields, idSchema, recordTimestamps } from './wire.ts'
|
|
@@ -24,6 +26,8 @@ export interface Deal extends RecordTimestamps {
|
|
|
24
26
|
readonly summary: string
|
|
25
27
|
readonly tags: readonly string[]
|
|
26
28
|
readonly externalId: string | null
|
|
29
|
+
/** Non-null once this record has been converted to another pipeline type. */
|
|
30
|
+
readonly convertedTo: ConvertedTo | null
|
|
27
31
|
/** Workspace-defined fields, keyed by definition key. Always present (default `{}`). */
|
|
28
32
|
readonly customFields: CustomFieldValues
|
|
29
33
|
}
|
|
@@ -45,6 +49,7 @@ export const dealSchema: z.ZodType<Deal, unknown> = z
|
|
|
45
49
|
summary: z.string(),
|
|
46
50
|
tags: z.array(z.string()),
|
|
47
51
|
external_id: z.string().nullable(),
|
|
52
|
+
converted_to: convertedToWireSchema,
|
|
48
53
|
custom_fields: customFieldValuesSchema,
|
|
49
54
|
...recordTimestamps,
|
|
50
55
|
})
|
|
@@ -65,6 +70,10 @@ export const dealSchema: z.ZodType<Deal, unknown> = z
|
|
|
65
70
|
summary: wire.summary,
|
|
66
71
|
tags: wire.tags,
|
|
67
72
|
externalId: wire.external_id,
|
|
73
|
+
convertedTo:
|
|
74
|
+
wire.converted_to === null
|
|
75
|
+
? null
|
|
76
|
+
: { targetType: wire.converted_to.target_type, targetId: wire.converted_to.target_id },
|
|
68
77
|
customFields: wire.custom_fields,
|
|
69
78
|
createdAt: wire.created_at,
|
|
70
79
|
updatedAt: wire.updated_at,
|
package/src/enquiry.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
+
import { convertedToWireSchema } from './conversion.ts'
|
|
4
|
+
import type { ConvertedTo } from './conversion.ts'
|
|
3
5
|
import { customFieldValuesBody, customFieldValuesSchema } from './customField.ts'
|
|
4
6
|
import type { CustomFieldValue, CustomFieldValues } from './customField.ts'
|
|
5
7
|
import { definedFields, idSchema, recordTimestamps } from './wire.ts'
|
|
@@ -18,6 +20,8 @@ export interface Enquiry extends RecordTimestamps {
|
|
|
18
20
|
readonly ownerId: string | null
|
|
19
21
|
/** Non-null once the enquiry has been converted to a Deal; nulled if that deal is deleted. */
|
|
20
22
|
readonly convertedDealId: string | null
|
|
23
|
+
/** Non-null once this record has been converted to another pipeline type. */
|
|
24
|
+
readonly convertedTo: ConvertedTo | null
|
|
21
25
|
readonly personIds: readonly string[]
|
|
22
26
|
readonly summary: string
|
|
23
27
|
readonly tags: readonly string[]
|
|
@@ -34,6 +38,7 @@ export const enquirySchema: z.ZodType<Enquiry, unknown> = z
|
|
|
34
38
|
company_id: idSchema.nullable(),
|
|
35
39
|
owner_id: idSchema.nullable(),
|
|
36
40
|
converted_deal_id: idSchema.nullable(),
|
|
41
|
+
converted_to: convertedToWireSchema,
|
|
37
42
|
person_ids: z.array(idSchema),
|
|
38
43
|
summary: z.string(),
|
|
39
44
|
tags: z.array(z.string()),
|
|
@@ -49,6 +54,10 @@ export const enquirySchema: z.ZodType<Enquiry, unknown> = z
|
|
|
49
54
|
companyId: wire.company_id,
|
|
50
55
|
ownerId: wire.owner_id,
|
|
51
56
|
convertedDealId: wire.converted_deal_id,
|
|
57
|
+
convertedTo:
|
|
58
|
+
wire.converted_to === null
|
|
59
|
+
? null
|
|
60
|
+
: { targetType: wire.converted_to.target_type, targetId: wire.converted_to.target_id },
|
|
52
61
|
personIds: wire.person_ids,
|
|
53
62
|
summary: wire.summary,
|
|
54
63
|
tags: wire.tags,
|
package/src/form.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
FORM_FIELD_MAP_TARGETS,
|
|
5
4
|
FORM_FIELD_TYPES,
|
|
6
5
|
FORM_OPTION_VALUE_TYPES,
|
|
7
6
|
FORM_STATUSES,
|
|
@@ -156,7 +155,7 @@ const formFieldSchema = z
|
|
|
156
155
|
label: z.string(),
|
|
157
156
|
type: z.enum(FORM_FIELD_TYPES),
|
|
158
157
|
required: z.boolean(),
|
|
159
|
-
map_to: z.
|
|
158
|
+
map_to: z.string().min(1),
|
|
160
159
|
options: z.array(formFieldOptionSchema),
|
|
161
160
|
placeholder: z.string().nullable(),
|
|
162
161
|
statement: z.string().nullable(),
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import type { CustomFieldObjectType, CustomFieldType, FormFieldType } from './values.ts'
|
|
2
|
+
import {
|
|
3
|
+
ACCOUNT_TYPES,
|
|
4
|
+
COMPANY_STAGES,
|
|
5
|
+
CUSTOM_FIELD_OBJECT_TYPE_LABELS,
|
|
6
|
+
CUSTOM_FIELD_TYPE_LABELS,
|
|
7
|
+
FORM_FIELD_MAP_TARGET_LABELS,
|
|
8
|
+
ICP_FITS,
|
|
9
|
+
INFLUENCE_LEVELS,
|
|
10
|
+
PERSON_CONSENT_TARGET,
|
|
11
|
+
PERSON_EMAIL_TARGET,
|
|
12
|
+
PREFERRED_CHANNELS,
|
|
13
|
+
RELATIONSHIP_LEVELS,
|
|
14
|
+
SIZE_BANDS,
|
|
15
|
+
} from './values.ts'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Where a form field's answer may land on submit.
|
|
19
|
+
*
|
|
20
|
+
* Any string matching the catalog (standard fields, workspace custom fields,
|
|
21
|
+
* `submission`, or `person.consent`). The legacy `FORM_FIELD_MAP_TARGETS` enum
|
|
22
|
+
* is a starter subset; validation uses this module instead.
|
|
23
|
+
*/
|
|
24
|
+
export type FormFieldMapTarget = string
|
|
25
|
+
|
|
26
|
+
/** Object types that carry standard writable fields in the catalog. */
|
|
27
|
+
export const FORM_MAP_OBJECT_TYPES = [
|
|
28
|
+
'person',
|
|
29
|
+
'company',
|
|
30
|
+
'position',
|
|
31
|
+
'deal',
|
|
32
|
+
'opportunity',
|
|
33
|
+
'partnership',
|
|
34
|
+
'enquiry',
|
|
35
|
+
'raise',
|
|
36
|
+
] as const
|
|
37
|
+
|
|
38
|
+
export type FormMapObjectType = (typeof FORM_MAP_OBJECT_TYPES)[number]
|
|
39
|
+
|
|
40
|
+
export const FORM_MAP_OBJECT_TYPE_LABELS: Readonly<Record<FormMapObjectType, string>> = {
|
|
41
|
+
person: 'Person',
|
|
42
|
+
company: 'Company',
|
|
43
|
+
position: 'Position',
|
|
44
|
+
deal: 'Deal',
|
|
45
|
+
opportunity: 'Opportunity',
|
|
46
|
+
partnership: 'Partnership',
|
|
47
|
+
enquiry: 'Enquiry',
|
|
48
|
+
raise: 'Raise',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The value shape a mapped target expects from a submit answer. */
|
|
52
|
+
export type FormMapValueType =
|
|
53
|
+
| 'text'
|
|
54
|
+
| 'email'
|
|
55
|
+
| 'phones'
|
|
56
|
+
| 'tags'
|
|
57
|
+
| 'string_array'
|
|
58
|
+
| 'enum'
|
|
59
|
+
| 'boolean'
|
|
60
|
+
| 'date'
|
|
61
|
+
| 'number'
|
|
62
|
+
| 'currency'
|
|
63
|
+
| 'url'
|
|
64
|
+
| 'long_text'
|
|
65
|
+
| 'consent'
|
|
66
|
+
|
|
67
|
+
export interface FormMapStandardField {
|
|
68
|
+
readonly field: string
|
|
69
|
+
readonly label: string
|
|
70
|
+
readonly valueType: FormMapValueType
|
|
71
|
+
/** Present for enum targets — the allowed wire values. */
|
|
72
|
+
readonly enumValues?: readonly string[]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface FormMapTargetEntry {
|
|
76
|
+
readonly target: string
|
|
77
|
+
readonly objectType: FormMapObjectType | 'submission' | 'consent'
|
|
78
|
+
readonly label: string
|
|
79
|
+
readonly fieldKind: 'standard' | 'custom' | 'special'
|
|
80
|
+
readonly valueType: FormMapValueType
|
|
81
|
+
readonly enumValues?: readonly string[] | undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Minimal custom-field definition shape for building and labelling targets. */
|
|
85
|
+
export interface CustomFieldDefinitionRef {
|
|
86
|
+
readonly objectType: CustomFieldObjectType
|
|
87
|
+
readonly key: string
|
|
88
|
+
readonly label: string
|
|
89
|
+
readonly type: CustomFieldType
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const PERSON_STANDARD_FIELDS: readonly FormMapStandardField[] = [
|
|
93
|
+
{ field: 'name', label: 'name', valueType: 'text' },
|
|
94
|
+
{ field: 'salutation', label: 'salutation', valueType: 'text' },
|
|
95
|
+
{ field: 'first_name', label: 'first name', valueType: 'text' },
|
|
96
|
+
{ field: 'last_name', label: 'last name', valueType: 'text' },
|
|
97
|
+
{ field: 'suffix', label: 'suffix', valueType: 'text' },
|
|
98
|
+
{ field: 'email', label: 'email', valueType: 'email' },
|
|
99
|
+
{ field: 'phones', label: 'phone', valueType: 'phones' },
|
|
100
|
+
{ field: 'timezone', label: 'timezone', valueType: 'text' },
|
|
101
|
+
{ field: 'location', label: 'location', valueType: 'text' },
|
|
102
|
+
{
|
|
103
|
+
field: 'preferred_channel',
|
|
104
|
+
label: 'preferred channel',
|
|
105
|
+
valueType: 'enum',
|
|
106
|
+
enumValues: PREFERRED_CHANNELS,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
field: 'influence',
|
|
110
|
+
label: 'influence',
|
|
111
|
+
valueType: 'enum',
|
|
112
|
+
enumValues: INFLUENCE_LEVELS,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
field: 'relationship',
|
|
116
|
+
label: 'relationship',
|
|
117
|
+
valueType: 'enum',
|
|
118
|
+
enumValues: RELATIONSHIP_LEVELS,
|
|
119
|
+
},
|
|
120
|
+
{ field: 'summary', label: 'summary', valueType: 'long_text' },
|
|
121
|
+
{ field: 'tags', label: 'tags', valueType: 'tags' },
|
|
122
|
+
{ field: 'do_not_contact', label: 'do not contact', valueType: 'boolean' },
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
const COMPANY_STANDARD_FIELDS: readonly FormMapStandardField[] = [
|
|
126
|
+
{ field: 'name', label: 'name', valueType: 'text' },
|
|
127
|
+
{ field: 'domain', label: 'domain', valueType: 'text' },
|
|
128
|
+
{ field: 'industry', label: 'industry', valueType: 'text' },
|
|
129
|
+
{ field: 'description', label: 'description', valueType: 'long_text' },
|
|
130
|
+
{
|
|
131
|
+
field: 'stage',
|
|
132
|
+
label: 'stage',
|
|
133
|
+
valueType: 'enum',
|
|
134
|
+
enumValues: COMPANY_STAGES,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
field: 'size_band',
|
|
138
|
+
label: 'size band',
|
|
139
|
+
valueType: 'enum',
|
|
140
|
+
enumValues: SIZE_BANDS,
|
|
141
|
+
},
|
|
142
|
+
{ field: 'hq', label: 'hq', valueType: 'text' },
|
|
143
|
+
{ field: 'website', label: 'website', valueType: 'url' },
|
|
144
|
+
{
|
|
145
|
+
field: 'account_type',
|
|
146
|
+
label: 'account type',
|
|
147
|
+
valueType: 'enum',
|
|
148
|
+
enumValues: ACCOUNT_TYPES,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
field: 'icp_fit',
|
|
152
|
+
label: 'ICP fit',
|
|
153
|
+
valueType: 'enum',
|
|
154
|
+
enumValues: ICP_FITS,
|
|
155
|
+
},
|
|
156
|
+
{ field: 'tech_stack', label: 'tech stack', valueType: 'string_array' },
|
|
157
|
+
{ field: 'summary', label: 'summary', valueType: 'long_text' },
|
|
158
|
+
{ field: 'tags', label: 'tags', valueType: 'tags' },
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
const POSITION_STANDARD_FIELDS: readonly FormMapStandardField[] = [
|
|
162
|
+
{ field: 'title', label: 'title', valueType: 'text' },
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
const DEAL_STANDARD_FIELDS: readonly FormMapStandardField[] = [
|
|
166
|
+
{ field: 'name', label: 'name', valueType: 'text' },
|
|
167
|
+
{ field: 'value_cents', label: 'value', valueType: 'number' },
|
|
168
|
+
{ field: 'currency', label: 'currency', valueType: 'text' },
|
|
169
|
+
{ field: 'expected_close', label: 'expected close', valueType: 'date' },
|
|
170
|
+
{ field: 'competitors', label: 'competitors', valueType: 'string_array' },
|
|
171
|
+
{ field: 'risks', label: 'risks', valueType: 'long_text' },
|
|
172
|
+
{ field: 'why_win', label: 'why win', valueType: 'long_text' },
|
|
173
|
+
{ field: 'summary', label: 'summary', valueType: 'long_text' },
|
|
174
|
+
{ field: 'tags', label: 'tags', valueType: 'tags' },
|
|
175
|
+
{ field: 'external_id', label: 'external id', valueType: 'text' },
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
const OPPORTUNITY_STANDARD_FIELDS: readonly FormMapStandardField[] = [
|
|
179
|
+
{ field: 'name', label: 'name', valueType: 'text' },
|
|
180
|
+
{ field: 'kind', label: 'kind', valueType: 'text' },
|
|
181
|
+
{ field: 'expected_close', label: 'expected close', valueType: 'date' },
|
|
182
|
+
{ field: 'summary', label: 'summary', valueType: 'long_text' },
|
|
183
|
+
{ field: 'tags', label: 'tags', valueType: 'tags' },
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
const PARTNERSHIP_STANDARD_FIELDS: readonly FormMapStandardField[] = [
|
|
187
|
+
{ field: 'name', label: 'name', valueType: 'text' },
|
|
188
|
+
{ field: 'kind', label: 'kind', valueType: 'text' },
|
|
189
|
+
{ field: 'next_touchpoint', label: 'next touchpoint', valueType: 'date' },
|
|
190
|
+
{ field: 'goals', label: 'goals', valueType: 'long_text' },
|
|
191
|
+
{ field: 'success_looks_like', label: 'success looks like', valueType: 'long_text' },
|
|
192
|
+
{ field: 'summary', label: 'summary', valueType: 'long_text' },
|
|
193
|
+
{ field: 'tags', label: 'tags', valueType: 'tags' },
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
const ENQUIRY_STANDARD_FIELDS: readonly FormMapStandardField[] = [
|
|
197
|
+
{ field: 'name', label: 'name', valueType: 'text' },
|
|
198
|
+
{ field: 'source', label: 'source', valueType: 'text' },
|
|
199
|
+
{ field: 'summary', label: 'summary', valueType: 'long_text' },
|
|
200
|
+
{ field: 'tags', label: 'tags', valueType: 'tags' },
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
const RAISE_STANDARD_FIELDS: readonly FormMapStandardField[] = [
|
|
204
|
+
{ field: 'name', label: 'name', valueType: 'text' },
|
|
205
|
+
{ field: 'check_size_cents', label: 'check size', valueType: 'number' },
|
|
206
|
+
{ field: 'currency', label: 'currency', valueType: 'text' },
|
|
207
|
+
{ field: 'thesis_fit', label: 'thesis fit', valueType: 'long_text' },
|
|
208
|
+
{ field: 'pass_reason', label: 'pass reason', valueType: 'long_text' },
|
|
209
|
+
{ field: 'expected_close', label: 'expected close', valueType: 'date' },
|
|
210
|
+
{ field: 'summary', label: 'summary', valueType: 'long_text' },
|
|
211
|
+
{ field: 'tags', label: 'tags', valueType: 'tags' },
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
/** Standard writable fields per object type, keyed by object. */
|
|
215
|
+
export const FORM_STANDARD_MAP_FIELDS: Readonly<
|
|
216
|
+
Record<FormMapObjectType, readonly FormMapStandardField[]>
|
|
217
|
+
> = {
|
|
218
|
+
person: PERSON_STANDARD_FIELDS,
|
|
219
|
+
company: COMPANY_STANDARD_FIELDS,
|
|
220
|
+
position: POSITION_STANDARD_FIELDS,
|
|
221
|
+
deal: DEAL_STANDARD_FIELDS,
|
|
222
|
+
opportunity: OPPORTUNITY_STANDARD_FIELDS,
|
|
223
|
+
partnership: PARTNERSHIP_STANDARD_FIELDS,
|
|
224
|
+
enquiry: ENQUIRY_STANDARD_FIELDS,
|
|
225
|
+
raise: RAISE_STANDARD_FIELDS,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function standardFieldTarget(objectType: FormMapObjectType, field: FormMapStandardField): string {
|
|
229
|
+
return `${objectType}.${field.field}`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function standardFieldLabel(objectType: FormMapObjectType, field: FormMapStandardField): string {
|
|
233
|
+
return `${FORM_MAP_OBJECT_TYPE_LABELS[objectType]} · ${field.label}`
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Every built-in map target entry, excluding workspace custom fields. */
|
|
237
|
+
export function listStandardMapTargetEntries(): readonly FormMapTargetEntry[] {
|
|
238
|
+
const entries: FormMapTargetEntry[] = [
|
|
239
|
+
{
|
|
240
|
+
target: 'submission',
|
|
241
|
+
objectType: 'submission',
|
|
242
|
+
label: 'Submission only',
|
|
243
|
+
fieldKind: 'special',
|
|
244
|
+
valueType: 'text',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
target: PERSON_CONSENT_TARGET,
|
|
248
|
+
objectType: 'consent',
|
|
249
|
+
label: 'Person · consent',
|
|
250
|
+
fieldKind: 'special',
|
|
251
|
+
valueType: 'consent',
|
|
252
|
+
},
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
for (const objectType of FORM_MAP_OBJECT_TYPES) {
|
|
256
|
+
for (const field of FORM_STANDARD_MAP_FIELDS[objectType]) {
|
|
257
|
+
const entry: FormMapTargetEntry = {
|
|
258
|
+
target: standardFieldTarget(objectType, field),
|
|
259
|
+
objectType,
|
|
260
|
+
label: standardFieldLabel(objectType, field),
|
|
261
|
+
fieldKind: 'standard',
|
|
262
|
+
valueType: field.valueType,
|
|
263
|
+
...(field.enumValues === undefined ? {} : { enumValues: field.enumValues }),
|
|
264
|
+
}
|
|
265
|
+
entries.push(entry)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return entries
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The wire target string for a workspace custom field definition. */
|
|
273
|
+
export function buildCustomMapTarget(objectType: CustomFieldObjectType, key: string): string {
|
|
274
|
+
return `${objectType}.custom_fields.${key}`
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** A catalog entry for one workspace custom field definition. */
|
|
278
|
+
export function buildCustomMapTargetEntry(definition: CustomFieldDefinitionRef): FormMapTargetEntry {
|
|
279
|
+
return {
|
|
280
|
+
target: buildCustomMapTarget(definition.objectType, definition.key),
|
|
281
|
+
objectType: definition.objectType,
|
|
282
|
+
label: `${CUSTOM_FIELD_OBJECT_TYPE_LABELS[definition.objectType]} · ${definition.label} (custom)`,
|
|
283
|
+
fieldKind: 'custom',
|
|
284
|
+
valueType: customFieldTypeToMapValueType(definition.type),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function customFieldTypeToMapValueType(type: CustomFieldType): FormMapValueType {
|
|
289
|
+
switch (type) {
|
|
290
|
+
case 'text':
|
|
291
|
+
return 'text'
|
|
292
|
+
case 'long_text':
|
|
293
|
+
return 'long_text'
|
|
294
|
+
case 'number':
|
|
295
|
+
return 'number'
|
|
296
|
+
case 'currency':
|
|
297
|
+
return 'currency'
|
|
298
|
+
case 'date':
|
|
299
|
+
return 'date'
|
|
300
|
+
case 'checkbox':
|
|
301
|
+
return 'boolean'
|
|
302
|
+
case 'select':
|
|
303
|
+
return 'enum'
|
|
304
|
+
case 'multi_select':
|
|
305
|
+
return 'string_array'
|
|
306
|
+
case 'url':
|
|
307
|
+
return 'url'
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface ParsedFormMapTarget {
|
|
312
|
+
readonly objectType: FormMapObjectType | 'submission' | 'consent'
|
|
313
|
+
readonly fieldPath: string
|
|
314
|
+
readonly isCustomField: boolean
|
|
315
|
+
readonly customFieldKey?: string | undefined
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Parses a map target string. Returns undefined when the shape is not recognised. */
|
|
319
|
+
export function parseFormMapTarget(target: string): ParsedFormMapTarget | undefined {
|
|
320
|
+
if (target === 'submission') {
|
|
321
|
+
return { objectType: 'submission', fieldPath: 'submission', isCustomField: false }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (target === PERSON_CONSENT_TARGET) {
|
|
325
|
+
return { objectType: 'consent', fieldPath: 'consent', isCustomField: false }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const customMatch = /^([a-z_]+)\.custom_fields\.([a-z][a-z0-9_]*)$/.exec(target)
|
|
329
|
+
|
|
330
|
+
if (customMatch !== null) {
|
|
331
|
+
const objectType = customMatch[1] as FormMapObjectType
|
|
332
|
+
const key = customMatch[2]
|
|
333
|
+
|
|
334
|
+
if (!FORM_MAP_OBJECT_TYPES.includes(objectType)) {
|
|
335
|
+
return undefined
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
objectType,
|
|
340
|
+
fieldPath: `custom_fields.${key}`,
|
|
341
|
+
isCustomField: true,
|
|
342
|
+
customFieldKey: key,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const standardMatch = /^([a-z_]+)\.([a-z_]+)$/.exec(target)
|
|
347
|
+
|
|
348
|
+
if (standardMatch === null) {
|
|
349
|
+
return undefined
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const objectType = standardMatch[1] as FormMapObjectType
|
|
353
|
+
const field = standardMatch[2]
|
|
354
|
+
|
|
355
|
+
if (field === undefined || !FORM_MAP_OBJECT_TYPES.includes(objectType)) {
|
|
356
|
+
return undefined
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const known = FORM_STANDARD_MAP_FIELDS[objectType].some((entry) => entry.field === field)
|
|
360
|
+
|
|
361
|
+
if (!known) {
|
|
362
|
+
return undefined
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { objectType, fieldPath: field, isCustomField: false }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** True when more than one form field may share this target. */
|
|
369
|
+
export function isRepeatableMapTarget(target: string): boolean {
|
|
370
|
+
return target === 'submission' || target === PERSON_CONSENT_TARGET
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function findStandardEntry(target: string): FormMapTargetEntry | undefined {
|
|
374
|
+
return listStandardMapTargetEntries().find((entry) => entry.target === target)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function findCustomEntry(
|
|
378
|
+
target: string,
|
|
379
|
+
definitions: readonly CustomFieldDefinitionRef[],
|
|
380
|
+
): FormMapTargetEntry | undefined {
|
|
381
|
+
const parsed = parseFormMapTarget(target)
|
|
382
|
+
|
|
383
|
+
if (parsed === undefined || !parsed.isCustomField || parsed.customFieldKey === undefined) {
|
|
384
|
+
return undefined
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const definition = definitions.find(
|
|
388
|
+
(row) => row.objectType === parsed.objectType && row.key === parsed.customFieldKey,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return definition === undefined ? undefined : buildCustomMapTargetEntry(definition)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Resolves a target to its catalog entry, checking custom definitions when needed. */
|
|
395
|
+
export function resolveMapTargetEntry(
|
|
396
|
+
target: string,
|
|
397
|
+
customDefinitions: readonly CustomFieldDefinitionRef[] = [],
|
|
398
|
+
): FormMapTargetEntry | undefined {
|
|
399
|
+
const standard = findStandardEntry(target)
|
|
400
|
+
|
|
401
|
+
if (standard !== undefined) {
|
|
402
|
+
return standard
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return findCustomEntry(target, customDefinitions)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Display label for a map target. Falls back to the raw target string. */
|
|
409
|
+
export function labelForMapTarget(
|
|
410
|
+
target: string,
|
|
411
|
+
customDefinitions: readonly CustomFieldDefinitionRef[] = [],
|
|
412
|
+
): string {
|
|
413
|
+
const legacy =
|
|
414
|
+
FORM_FIELD_MAP_TARGET_LABELS[target as keyof typeof FORM_FIELD_MAP_TARGET_LABELS]
|
|
415
|
+
|
|
416
|
+
if (legacy !== undefined) {
|
|
417
|
+
return legacy
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return resolveMapTargetEntry(target, customDefinitions)?.label ?? target
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Merges the static catalog with workspace custom field definitions. */
|
|
424
|
+
export function listMapTargetEntries(
|
|
425
|
+
customDefinitions: readonly CustomFieldDefinitionRef[] = [],
|
|
426
|
+
): readonly FormMapTargetEntry[] {
|
|
427
|
+
return [
|
|
428
|
+
...listStandardMapTargetEntries(),
|
|
429
|
+
...customDefinitions.map((definition) => buildCustomMapTargetEntry(definition)),
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** True when the target is known in the catalog (standard or custom). */
|
|
434
|
+
export function isKnownMapTarget(
|
|
435
|
+
target: string,
|
|
436
|
+
customDefinitions: readonly CustomFieldDefinitionRef[] = [],
|
|
437
|
+
): boolean {
|
|
438
|
+
return resolveMapTargetEntry(target, customDefinitions) !== undefined
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Suggested form field control type for a map target. */
|
|
442
|
+
export function suggestedFormFieldType(
|
|
443
|
+
target: string,
|
|
444
|
+
customDefinitions: readonly CustomFieldDefinitionRef[] = [],
|
|
445
|
+
current: FormFieldType = 'text',
|
|
446
|
+
): FormFieldType {
|
|
447
|
+
if (target === PERSON_EMAIL_TARGET) {
|
|
448
|
+
return 'email'
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (target === PERSON_CONSENT_TARGET) {
|
|
452
|
+
return current === 'notice' ? 'notice' : 'consent'
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const entry = resolveMapTargetEntry(target, customDefinitions)
|
|
456
|
+
|
|
457
|
+
if (entry === undefined) {
|
|
458
|
+
return current === 'consent' || current === 'notice' ? 'text' : current
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
switch (entry.valueType) {
|
|
462
|
+
case 'email':
|
|
463
|
+
return 'email'
|
|
464
|
+
case 'long_text':
|
|
465
|
+
case 'consent':
|
|
466
|
+
return entry.valueType === 'consent'
|
|
467
|
+
? current === 'notice'
|
|
468
|
+
? 'notice'
|
|
469
|
+
: 'consent'
|
|
470
|
+
: 'textarea'
|
|
471
|
+
case 'enum':
|
|
472
|
+
return 'select'
|
|
473
|
+
case 'boolean':
|
|
474
|
+
return 'select'
|
|
475
|
+
default:
|
|
476
|
+
return current === 'consent' || current === 'notice' ? 'text' : current
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** True when a form field type can reasonably collect an answer for this target. */
|
|
481
|
+
export function isCompatibleFormFieldType(
|
|
482
|
+
formType: FormFieldType,
|
|
483
|
+
target: string,
|
|
484
|
+
customDefinitions: readonly CustomFieldDefinitionRef[] = [],
|
|
485
|
+
): boolean {
|
|
486
|
+
if (target === PERSON_CONSENT_TARGET) {
|
|
487
|
+
return formType === 'consent' || formType === 'notice'
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (formType === 'consent' || formType === 'notice') {
|
|
491
|
+
return target === PERSON_CONSENT_TARGET
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const entry = resolveMapTargetEntry(target, customDefinitions)
|
|
495
|
+
|
|
496
|
+
if (entry === undefined) {
|
|
497
|
+
return false
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (entry.objectType === 'submission') {
|
|
501
|
+
return true
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
switch (entry.valueType) {
|
|
505
|
+
case 'email':
|
|
506
|
+
return formType === 'email' || formType === 'text'
|
|
507
|
+
case 'boolean':
|
|
508
|
+
return formType === 'select' || formType === 'text'
|
|
509
|
+
case 'enum':
|
|
510
|
+
return formType === 'select' || formType === 'text'
|
|
511
|
+
case 'number':
|
|
512
|
+
case 'currency':
|
|
513
|
+
case 'date':
|
|
514
|
+
return formType === 'text' || formType === 'select'
|
|
515
|
+
case 'tags':
|
|
516
|
+
case 'string_array':
|
|
517
|
+
case 'phones':
|
|
518
|
+
return formType === 'text' || formType === 'textarea' || formType === 'select'
|
|
519
|
+
case 'url':
|
|
520
|
+
return formType === 'text' || formType === 'email'
|
|
521
|
+
case 'long_text':
|
|
522
|
+
return formType === 'textarea' || formType === 'text'
|
|
523
|
+
case 'consent':
|
|
524
|
+
return false
|
|
525
|
+
default:
|
|
526
|
+
return formType === 'text' || formType === 'textarea' || formType === 'email' || formType === 'select'
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Meta line for the map-target search picker. */
|
|
531
|
+
export function metaForMapTargetEntry(
|
|
532
|
+
entry: FormMapTargetEntry,
|
|
533
|
+
customType?: CustomFieldType,
|
|
534
|
+
): string | undefined {
|
|
535
|
+
if (entry.fieldKind === 'custom') {
|
|
536
|
+
return customType === undefined
|
|
537
|
+
? 'custom'
|
|
538
|
+
: `custom · ${CUSTOM_FIELD_TYPE_LABELS[customType]}`
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (entry.fieldKind === 'special') {
|
|
542
|
+
return undefined
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return entry.valueType
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export { PERSON_CONSENT_TARGET, PERSON_EMAIL_TARGET }
|