founder-schema 0.15.0 → 0.17.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 +197 -5
- package/dist/core/common.d.ts +103 -20
- package/dist/core/common.d.ts.map +1 -1
- package/dist/core/common.js +104 -16
- package/dist/core/common.js.map +1 -1
- package/dist/core/limits.d.ts +7 -0
- package/dist/core/limits.d.ts.map +1 -1
- package/dist/core/limits.js +7 -0
- package/dist/core/limits.js.map +1 -1
- package/dist/core/validation.d.ts +26 -0
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +26 -0
- package/dist/core/validation.js.map +1 -1
- package/dist/domains/communities/communities.d.ts +9 -9
- package/dist/domains/discover/discover.d.ts +7 -0
- package/dist/domains/discover/discover.d.ts.map +1 -1
- package/dist/domains/feed/feed.d.ts +11 -11
- package/dist/domains/location/india.d.ts +205 -0
- package/dist/domains/location/india.d.ts.map +1 -0
- package/dist/domains/location/india.js +1175 -0
- package/dist/domains/location/india.js.map +1 -0
- package/dist/domains/onboarding/onboarding-answers.d.ts +81 -0
- package/dist/domains/onboarding/onboarding-answers.d.ts.map +1 -0
- package/dist/domains/onboarding/onboarding-answers.js +141 -0
- package/dist/domains/onboarding/onboarding-answers.js.map +1 -0
- package/dist/domains/onboarding/onboarding.d.ts +422 -34
- package/dist/domains/onboarding/onboarding.d.ts.map +1 -1
- package/dist/domains/onboarding/onboarding.js +314 -58
- package/dist/domains/onboarding/onboarding.js.map +1 -1
- package/dist/domains/products/products.d.ts +3 -3
- package/dist/domains/profile/founder-profile.d.ts +122 -6
- package/dist/domains/profile/founder-profile.d.ts.map +1 -1
- package/dist/domains/profile/founder-profile.js +29 -9
- package/dist/domains/profile/founder-profile.js.map +1 -1
- package/dist/domains/profile/my-profile.d.ts +386 -0
- package/dist/domains/profile/my-profile.d.ts.map +1 -0
- package/dist/domains/profile/my-profile.js +52 -0
- package/dist/domains/profile/my-profile.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,8 +86,12 @@ SAME schema object, so the two can never disagree about what is allowed.
|
|
|
86
86
|
(`ONBOARDING_STEP_SCHEMAS`), so a step the wizard lets through is a step the
|
|
87
87
|
finish accepts. The step list (`FOUNDER_ONBOARDING_STEPS`) is built from those
|
|
88
88
|
slices, including whether a step may be skipped.
|
|
89
|
-
- **No copy.**
|
|
90
|
-
(`
|
|
89
|
+
- **No copy.** A step's label and title, a topic category and a goal category
|
|
90
|
+
are translation keys or plain keys (`labelKey`, `titleKey`, `key`); the words
|
|
91
|
+
live in each client. The web keys its category words by `key`, type-checked
|
|
92
|
+
against it, and its step words by the step's own key (`OnboardingStepKey`);
|
|
93
|
+
`labelKey` / `titleKey` are there for a translated client (i18n), and
|
|
94
|
+
nothing reads them yet.
|
|
91
95
|
- **Bodies are typed JSON; only queries coerce.** `teamSize: "5"` is refused in a
|
|
92
96
|
body, and `?limit=5` is read as 5.
|
|
93
97
|
- **Shapes are derived, never copied.** One `locationSchema`, one
|
|
@@ -99,6 +103,189 @@ instance; this package must not bring its own.
|
|
|
99
103
|
|
|
100
104
|
---
|
|
101
105
|
|
|
106
|
+
## Breaking in 0.17.0: the onboarding photo is saved, and onboarding's single source of truth (2026-10-19)
|
|
107
|
+
|
|
108
|
+
Two pieces of the user's work on onboarding, 2026-10-19.
|
|
109
|
+
|
|
110
|
+
**The photo.** Going back to the photo step showed no photo, though the member
|
|
111
|
+
had pressed Save and continue on it. Every other answer came back from the
|
|
112
|
+
draft; the photo was held for the visit only, so any reload or a resume the
|
|
113
|
+
next day lost it.
|
|
114
|
+
|
|
115
|
+
- **New:** `onboardingStatusSchema.photo` (optional, a `mediaImageSchema`): the
|
|
116
|
+
photo the member saved, as it will appear. Absent when there is none.
|
|
117
|
+
- **Changed meaning:** a step save carrying `draft.avatarMediaId` (an upload id,
|
|
118
|
+
or `null`) is the photo step SAVING its photo. The server holds that upload
|
|
119
|
+
for the draft, so the 24 hour unattached sweep never deletes it, and lets go
|
|
120
|
+
of the one it replaces; `null` lets go of it. It is not kept in the draft:
|
|
121
|
+
`readOnboardingDraft` still gives `null` for it, and the photo comes back only
|
|
122
|
+
as `photo`, from what the server holds.
|
|
123
|
+
- **Stricter:** `onboardingProgressUpdateSchema` refuses a `draft.avatarMediaId`
|
|
124
|
+
that is neither an upload id nor `null` (`invalidId`, at
|
|
125
|
+
`draft.avatarMediaId`). No client sent the key before 0.17.0.
|
|
126
|
+
- **Backend:** a held photo is a media row attached to the member's onboarding
|
|
127
|
+
progress row (attachment kind `onboarding`, backend only). Finish moves it to
|
|
128
|
+
the new profile in the same transaction; a scheduled job lets go of a held
|
|
129
|
+
photo whose progress row the 90 day TTL deleted (Privacy: sign-up answers go
|
|
130
|
+
90 days after they were last changed). New partial index on `media` for that
|
|
131
|
+
job; run `sync-indexes --apply` after deploy.
|
|
132
|
+
- **Deploy order: the backend, then the web.** The web reads the status with
|
|
133
|
+
`z.object`, which drops a key it does not know, so a 0.16 web on a 0.17
|
|
134
|
+
backend simply shows no photo on Back, as today.
|
|
135
|
+
- **Rollback floor:** a backend below 0.17 does not know the `onboarding`
|
|
136
|
+
attachment, so photos held by then stay attached and are never swept until
|
|
137
|
+
0.17 runs again, and a member who finishes on the old build is told the photo
|
|
138
|
+
expired and asked to add it again (the old finish takes only an unattached
|
|
139
|
+
upload). A rollback that is KEPT therefore needs a one-off release of every
|
|
140
|
+
`media` row attached as kind `onboarding`, or those photos outlive the
|
|
141
|
+
Privacy's 90 days (account deletion still erases them).
|
|
142
|
+
|
|
143
|
+
**One source of truth** (the user: "all the data for all the onboarding steps
|
|
144
|
+
and all the errors and validation types ... should come from the founder
|
|
145
|
+
circle schema"). What a client or the backend kept its own copy of now lives
|
|
146
|
+
here, and each consumer reads it:
|
|
147
|
+
|
|
148
|
+
- **New, data:** `GOAL_CATEGORIES` / `GOAL_CATEGORY_KEYS` (the goals step's
|
|
149
|
+
three groups, from the web); `domains/location/india`: `INDIA`,
|
|
150
|
+
`INDIA_DIALING_CODE`, `INDIA_STATES` (each state's cities and other names,
|
|
151
|
+
from the web) with names typed against `GST_STATES` (ONE list of state
|
|
152
|
+
names), `INDIA_STATE_NAMES`, `STATE_ALIASES`, `CITY_ALIASES`,
|
|
153
|
+
`findIndiaState`, `listedCityName`, `canonicalIndiaLocation`;
|
|
154
|
+
`ONBOARDING_DRAFT_RETENTION_DAYS` (90, from the backend's TTL);
|
|
155
|
+
`PHONE_DIGITS`.
|
|
156
|
+
- **New, rules every side applies the same way:** `normalizePhone`,
|
|
157
|
+
`normalizeLinkUrl`, `tidyFullName`, `teamSizeDigits` (from the web);
|
|
158
|
+
`isRealIsoDate`, `todayInIndia`, `dateOfBirthBounds` (a picker's window,
|
|
159
|
+
exact at both ends), `isUnderAgeDateOfBirth` (the draft's age rule, from the
|
|
160
|
+
backend, now with the real-date check), `isoDateToDate` / `dateToIsoDate`;
|
|
161
|
+
`goalListSchema` / `skillListSchema` (one list rule for the finish and Me).
|
|
162
|
+
- **New, from the answers to what is sent** (`domains/onboarding/
|
|
163
|
+
onboarding-answers`, from the web): `emptyOnboardingForm`, `ventureStarted`,
|
|
164
|
+
`onboardingDraftForStep` (exactly a step's own fields), and
|
|
165
|
+
`completeOnboardingBodyFrom` (the finish body: every unanswered optional
|
|
166
|
+
answer absent, each answer cleaned as above), with the
|
|
167
|
+
`CompleteOnboardingBodyCandidate` type; `locationStarted` (a place opened
|
|
168
|
+
but nothing chosen is "no location").
|
|
169
|
+
- **New, the keys of shapes** (so no consumer lists them by hand):
|
|
170
|
+
`FOUNDER_LINK_KEYS` / `FounderLinkKey`, `VENTURE_KEYS` /
|
|
171
|
+
`VENTURE_OPTIONAL_KEYS` / `VentureKey`; types `IndiaLocation`, `IndiaState`,
|
|
172
|
+
`IndiaStateName`, `GoalCategory`.
|
|
173
|
+
- **New message keys:** `guidelinesRequired`, `photoUnavailable`,
|
|
174
|
+
`ventureRequired`, `stateRequired`, `stateUnknown`, `goalsDuplicate`,
|
|
175
|
+
`skillsDuplicate`. Each client words them in the same change (a client
|
|
176
|
+
holding its words as `Record<ValidationMessageKey, string>` fails to build
|
|
177
|
+
until it does).
|
|
178
|
+
- **Stricter:**
|
|
179
|
+
- every location WRITE (`completeOnboardingBodySchema`,
|
|
180
|
+
`updateFounderProfileBodySchema`, `updateMyPersonalDetailsBodySchema`, and
|
|
181
|
+
the server's `suggestedLocationSchema`) is `indiaLocationSchema`: country
|
|
182
|
+
`India`, a state from the list (required: `stateRequired`; not on it:
|
|
183
|
+
`stateUnknown`; read in any common spelling, "KARNATAKA", "Orissa",
|
|
184
|
+
"Jammu & Kashmir", and kept as the list's name), a city. READS keep
|
|
185
|
+
`locationSchema`, so a place stored before this is read as it is;
|
|
186
|
+
- `businessType` / `legalStructure` without a `venture` are each refused ON
|
|
187
|
+
THEIR OWN FIELD (`ventureRequired`), on the finish body and the business
|
|
188
|
+
step; the backend used to drop them without a word, and the web dropped a
|
|
189
|
+
lone registration (hary's ruling: no silent drop). `ventureStarted` no
|
|
190
|
+
longer counts the type: it describes a business, it does not start one;
|
|
191
|
+
- `displayNameSchema` stores the name tidied (`tidyFullName`): every body
|
|
192
|
+
that writes a name, the Me patches included;
|
|
193
|
+
- goals and skills each once (`goalsDuplicate`, `skillsDuplicate`), on the
|
|
194
|
+
finish and the Me patch;
|
|
195
|
+
- the guidelines: a box left unticked is `guidelinesRequired` (it was
|
|
196
|
+
`guidelinesOutdated`, the words for an older text).
|
|
197
|
+
- **Breaking:** `completeOnboardingBodySchema` and
|
|
198
|
+
`ONBOARDING_STEP_SCHEMAS.business` carry the venture rule (refinements), so
|
|
199
|
+
Zod 4.6 throws on `.pick`, `.omit`, `.partial` and `.merge` of them, and on
|
|
200
|
+
an `.extend` that overwrites a key. Read a field with `.shape.<field>`, and a
|
|
201
|
+
step's fields with `ONBOARDING_STEP_SCHEMAS.<step>` (picked before the rule;
|
|
202
|
+
the business slice carries it again). The web picked from the finish body in
|
|
203
|
+
three places (the `/dummy` showcase, at module scope, and two tests); they
|
|
204
|
+
read `.shape` now.
|
|
205
|
+
- **Changed type:** `displayNameSchema` is a pipe (`.transform(tidyFullName)`),
|
|
206
|
+
no longer a `ZodString`, so `.min` / `.max` / `.regex` cannot be chained on
|
|
207
|
+
it. Nothing did; a length is read through the pipe.
|
|
208
|
+
- **Breaking (no consumer reads it):** `FOUNDER_ONBOARDING_STEPS[i].helpKey`
|
|
209
|
+
is gone (there is no help line any more; the user, 2026-10-19), and
|
|
210
|
+
`labelKey` names the step's short label.
|
|
211
|
+
- **Moved, same export name:** `suggestedLocationSchema` / `SuggestedLocation`
|
|
212
|
+
now come from `domains/location/india`.
|
|
213
|
+
- **Deploy order: still the backend first.** A 0.16 web is refused a little
|
|
214
|
+
that it used to send, on its field: a place with no state (an old draft's,
|
|
215
|
+
or the 0.16 Me editor saving an OLD stateless place, which it sends without
|
|
216
|
+
one) at `location.state`. (It never sends a lone business type: its
|
|
217
|
+
`ventureStarted` counted the type, so a type on its own went with an empty
|
|
218
|
+
venture, refused at `venture.name` / `industry` / `stage` as on 0.16.) A
|
|
219
|
+
0.17 web needs the 0.17 backend for the photo. It reads the location
|
|
220
|
+
suggestion with the strict write shape, which takes a state in any common
|
|
221
|
+
spelling ("KARNATAKA"), so a 0.16 backend's answer is read unless it has no
|
|
222
|
+
state, or one the list does not know: then it is no suggestion.
|
|
223
|
+
- **Rollback floor (also):** the 0.17 web on a backend rolled below 0.17 saves
|
|
224
|
+
no photo, and shows no suggestion where the old backend answered without a
|
|
225
|
+
state the list knows.
|
|
226
|
+
- **Known, follow-up (hary, 2026-10-19), not in 0.17.0:**
|
|
227
|
+
- a step save racing a finish when the member has NO progress row (the TTL
|
|
228
|
+
deleted it while a tab stayed open) can leave a photo held for a finished
|
|
229
|
+
member, until the TTL takes that new row: the finish's update of a row
|
|
230
|
+
that is not there writes nothing, so the two transactions never conflict.
|
|
231
|
+
Fix: the finish upserts its completion stamp;
|
|
232
|
+
- finish's retry loop sets its result inside the transaction callback, so a
|
|
233
|
+
commit that fails and a retry that then meets a username clash could
|
|
234
|
+
answer success for a profile never committed. Fix: reset the result on
|
|
235
|
+
every attempt. Do both together: an 11000 on that upsert would otherwise
|
|
236
|
+
read as a username clash.
|
|
237
|
+
|
|
238
|
+
## Breaking in 0.16.0: onboarding in nine steps (2026-10-18)
|
|
239
|
+
|
|
240
|
+
The user's rework of onboarding. The finish body takes the same answers plus one.
|
|
241
|
+
|
|
242
|
+
- **Breaking:** `ONBOARDING_STEP_KEYS` is nine: `basics` (full name, gender,
|
|
243
|
+
date of birth, nothing else), `photo`, `professional` (title, about, skills),
|
|
244
|
+
then `contact`, `location`, `goals`, `interests`, `business`, `guidelines` as
|
|
245
|
+
before. `identity` and `ONBOARDING_STEP_SCHEMAS.identity` are gone;
|
|
246
|
+
`ONBOARDING_TOTAL_STEPS` is 9. The resume cursor is an index into this list:
|
|
247
|
+
the backend translates a cursor saved under the seven steps once, by step
|
|
248
|
+
name (its progress rows carry `stepsVersion`), so nobody resumes on the wrong
|
|
249
|
+
step or loses an answer.
|
|
250
|
+
- **Breaking:** `onboardingFormSchema` gains `legalStructure` (nullable), so a
|
|
251
|
+
form built from it must hold the key.
|
|
252
|
+
- **New:** `LEGAL_STRUCTURE_VALUES` / `legalStructureSchema` / `LegalStructure`:
|
|
253
|
+
how a venture is registered in India (sole proprietorship, partnership, LLP,
|
|
254
|
+
OPC, private and public limited, Section 8, trust or society, co-operative),
|
|
255
|
+
or not yet, or outside India. Optional on the finish body and the business
|
|
256
|
+
step; stored on the profile's venture beside `businessType`, and shown only
|
|
257
|
+
to the member (below).
|
|
258
|
+
- **New: the member's private answers in Me** (`domains/profile/my-profile.ts`,
|
|
259
|
+
the user, 2026-10-18): `GET`/`PUT /profiles/me/business` (`myBusinessSchema`,
|
|
260
|
+
`putMyBusinessBodySchema`: the WHOLE venture with its `businessType` and
|
|
261
|
+
`legalStructure`, which are `myVentureSchema`, or `null` to remove it) and
|
|
262
|
+
`GET`/`PUT /profiles/me/interests` (`myInterestsSchema`,
|
|
263
|
+
`putMyInterestsBodySchema`). Owner only: none of the three is on
|
|
264
|
+
`founderProfileSchema`, a card, a byline or a member's page.
|
|
265
|
+
- **Stricter:** `interestsSchema` is the ONE interests rule, used by the finish
|
|
266
|
+
body and the Me editor, and it now refuses a topic twice (`topicsDuplicate`):
|
|
267
|
+
three copies of one topic met the minimum of three. Neither client sends one.
|
|
268
|
+
- **New:** `splitFullName(fullName)`: the first and last name a full name
|
|
269
|
+
deduces to, at the first space; a one-word name is whole. Derived, never
|
|
270
|
+
stored: `displayName` stays the one owner of a name.
|
|
271
|
+
- **Appended:** `GOAL_VALUES` +7 (`get_feedback`, `find_testers`,
|
|
272
|
+
`mentor_others`, `invest_in_startups`, `join_startup`,
|
|
273
|
+
`find_service_providers`, `attend_events`); `BUSINESS_TYPE_VALUES` +5
|
|
274
|
+
(`freelancer`, `agency`, `social_enterprise`, `family_business`, `franchise`).
|
|
275
|
+
Nothing renamed or removed.
|
|
276
|
+
- **Deploy order: the backend, then the web straight after.** A 0.15 backend
|
|
277
|
+
refuses the `legalStructure` draft key and steps above 7, so a 0.16 web on it
|
|
278
|
+
is stuck at the business step; a 0.15 web cannot parse a 9-step status or a
|
|
279
|
+
card holding a new goal. Open tabs and an installed app's cached copy keep the
|
|
280
|
+
old bundle until they update.
|
|
281
|
+
- **Rollback floor:** once a member saves a new goal, a build below 0.16
|
|
282
|
+
cannot parse their card, member page or own profile; roll back only after a
|
|
283
|
+
backfill. A new business type or registration travels on none of those (only
|
|
284
|
+
on the owner's own `/profiles/me/business`, new in 0.16), so it does not
|
|
285
|
+
move the floor.
|
|
286
|
+
- **Backend, at deploy:** no new index and no migration (old cursors are
|
|
287
|
+
translated on the member's next visit).
|
|
288
|
+
|
|
102
289
|
## Breaking in 0.15.0: the launch waitlist (2026-10-16)
|
|
103
290
|
|
|
104
291
|
Until the launch instant `FC_LAUNCH_AT` (a server-only env on BOTH the backend
|
|
@@ -574,8 +761,13 @@ is imported, so a server process still up on 1 January rejects the new year unti
|
|
|
574
761
|
somebody restarts it.
|
|
575
762
|
|
|
576
763
|
**Changing a member of an enum array is a data change, not a type change.** The
|
|
577
|
-
|
|
578
|
-
|
|
764
|
+
`*_VALUES` arrays the backend's profile model uses (stage, industry, skill, goal,
|
|
765
|
+
gender, topic, business type, legal structure) are Mongoose `enum` constraints.
|
|
766
|
+
Removing or renaming a value orphans documents already saved with it. Appending
|
|
767
|
+
is safe for the database, but not for an OLDER CLIENT, which parses strictly and
|
|
768
|
+
refuses a value it does not know: deploy the readers (backend, then web) before
|
|
769
|
+
anything can write the new value, and a rollback below the version that added it
|
|
770
|
+
needs a backfill first.
|
|
579
771
|
`STARTUP_STAGE_VALUES` is ordered earliest to latest and that order is load
|
|
580
772
|
bearing, so append at the end and never insert.
|
|
581
773
|
|
|
@@ -584,7 +776,7 @@ bearing, so append at the end and never insert.
|
|
|
584
776
|
## Settled since the scaffold
|
|
585
777
|
|
|
586
778
|
- **The npm name and the first publish:** `founder-schema`, 0.1.0, by the user.
|
|
587
|
-
- **The onboarding steps:**
|
|
779
|
+
- **The onboarding steps:** nine since 0.16.0 (the user, 2026-10-18), seven before (ruled 2026-09-22); see `src/domains/onboarding/onboarding.ts`.
|
|
588
780
|
|
|
589
781
|
## Contract decisions, 2026-09-21
|
|
590
782
|
|
package/dist/core/common.d.ts
CHANGED
|
@@ -78,6 +78,14 @@ export type SafeUrl = z.infer<typeof safeUrlSchema>;
|
|
|
78
78
|
*/
|
|
79
79
|
export declare const httpUrlSchema: z.ZodURL;
|
|
80
80
|
export type HttpUrl = z.infer<typeof httpUrlSchema>;
|
|
81
|
+
/**
|
|
82
|
+
* A link as people paste it, made into what `httpUrlSchema` checks (0.17.0,
|
|
83
|
+
* moved here from the web so every client cleans a link the same way):
|
|
84
|
+
* `linkedin.com/in/asha` gains `https://`; blank stays blank. Something that
|
|
85
|
+
* already names a scheme keeps it, so `javascript:` is never quietly "fixed"
|
|
86
|
+
* into a link: the schema then refuses it, as it only takes http and https.
|
|
87
|
+
*/
|
|
88
|
+
export declare function normalizeLinkUrl(raw: string): string;
|
|
81
89
|
/**
|
|
82
90
|
* True when `url` parses and its hostname is EXACTLY one of `hosts`
|
|
83
91
|
* (lowercase). The one host check in this package, written the way the warning
|
|
@@ -238,11 +246,12 @@ export type Skill = z.infer<typeof skillSchema>;
|
|
|
238
246
|
* venture works only for a member who ALREADY HAS a venture row, and somebody
|
|
239
247
|
* who wants to start a company is precisely the member with no venture at all,
|
|
240
248
|
* so there is nothing to infer from. It also cannot express a founder with a
|
|
241
|
-
* running company who wants to start another.
|
|
242
|
-
*
|
|
243
|
-
*
|
|
249
|
+
* running company who wants to start another. The others were all asks of other
|
|
250
|
+
* people and this one an ask of the product, which is the whole of the oddness.
|
|
251
|
+
* (hary, 2026-09-21.) Since 0.16.0 the list also holds offers (`mentor_others`,
|
|
252
|
+
* `invest_in_startups`) and a second ask of the product (`attend_events`).
|
|
244
253
|
*/
|
|
245
|
-
export declare const GOAL_VALUES: readonly ["find_cofounder", "find_investors", "find_customers", "hiring", "partnerships", "mentorship", "grow_network", "business_ideas", "market_insights", "learn_startup", "start_company"];
|
|
254
|
+
export declare const GOAL_VALUES: readonly ["find_cofounder", "find_investors", "find_customers", "hiring", "partnerships", "mentorship", "grow_network", "business_ideas", "market_insights", "learn_startup", "start_company", "get_feedback", "find_testers", "mentor_others", "invest_in_startups", "join_startup", "find_service_providers", "attend_events"];
|
|
246
255
|
export declare const goalSchema: z.ZodEnum<{
|
|
247
256
|
find_cofounder: "find_cofounder";
|
|
248
257
|
find_investors: "find_investors";
|
|
@@ -255,8 +264,59 @@ export declare const goalSchema: z.ZodEnum<{
|
|
|
255
264
|
market_insights: "market_insights";
|
|
256
265
|
learn_startup: "learn_startup";
|
|
257
266
|
start_company: "start_company";
|
|
267
|
+
get_feedback: "get_feedback";
|
|
268
|
+
find_testers: "find_testers";
|
|
269
|
+
mentor_others: "mentor_others";
|
|
270
|
+
invest_in_startups: "invest_in_startups";
|
|
271
|
+
join_startup: "join_startup";
|
|
272
|
+
find_service_providers: "find_service_providers";
|
|
273
|
+
attend_events: "attend_events";
|
|
258
274
|
}>;
|
|
259
275
|
export type Goal = z.infer<typeof goalSchema>;
|
|
276
|
+
/**
|
|
277
|
+
* A member's goals as a list (0.17.0): each goal once, at most every goal.
|
|
278
|
+
* ONE rule for onboarding's finish (which adds its minimum, `GOALS_MIN`) and
|
|
279
|
+
* the Me tab's editor: two copies of one goal met the minimum while meaning
|
|
280
|
+
* one, and the profile would show it twice.
|
|
281
|
+
*/
|
|
282
|
+
export declare const goalListSchema: z.ZodArray<z.ZodEnum<{
|
|
283
|
+
find_cofounder: "find_cofounder";
|
|
284
|
+
find_investors: "find_investors";
|
|
285
|
+
find_customers: "find_customers";
|
|
286
|
+
hiring: "hiring";
|
|
287
|
+
partnerships: "partnerships";
|
|
288
|
+
mentorship: "mentorship";
|
|
289
|
+
grow_network: "grow_network";
|
|
290
|
+
business_ideas: "business_ideas";
|
|
291
|
+
market_insights: "market_insights";
|
|
292
|
+
learn_startup: "learn_startup";
|
|
293
|
+
start_company: "start_company";
|
|
294
|
+
get_feedback: "get_feedback";
|
|
295
|
+
find_testers: "find_testers";
|
|
296
|
+
mentor_others: "mentor_others";
|
|
297
|
+
invest_in_startups: "invest_in_startups";
|
|
298
|
+
join_startup: "join_startup";
|
|
299
|
+
find_service_providers: "find_service_providers";
|
|
300
|
+
attend_events: "attend_events";
|
|
301
|
+
}>>;
|
|
302
|
+
/** A member's skills (0.17.0): each skill once, at most `SKILLS_MAX`. ONE rule for onboarding and Me. */
|
|
303
|
+
export declare const skillListSchema: z.ZodArray<z.ZodEnum<{
|
|
304
|
+
engineering: "engineering";
|
|
305
|
+
product: "product";
|
|
306
|
+
design: "design";
|
|
307
|
+
data: "data";
|
|
308
|
+
growth: "growth";
|
|
309
|
+
sales: "sales";
|
|
310
|
+
marketing: "marketing";
|
|
311
|
+
operations: "operations";
|
|
312
|
+
finance: "finance";
|
|
313
|
+
legal: "legal";
|
|
314
|
+
hr_people: "hr_people";
|
|
315
|
+
supply_chain: "supply_chain";
|
|
316
|
+
research: "research";
|
|
317
|
+
fundraising: "fundraising";
|
|
318
|
+
community: "community";
|
|
319
|
+
}>>;
|
|
260
320
|
/**
|
|
261
321
|
* Demographic only. Nothing here computes anything from it, unlike the sibling
|
|
262
322
|
* gym product where gender feeds body-metric maths.
|
|
@@ -273,14 +333,42 @@ export declare const genderSchema: z.ZodEnum<{
|
|
|
273
333
|
prefer_not_to_say: "prefer_not_to_say";
|
|
274
334
|
}>;
|
|
275
335
|
export type Gender = z.infer<typeof genderSchema>;
|
|
336
|
+
/**
|
|
337
|
+
* A full name as it is sent and stored (0.17.0, moved here from the web):
|
|
338
|
+
* trimmed, and every run of spaces made one. `displayNameSchema` applies it,
|
|
339
|
+
* so the server stores it from every client; a client applies it to show the
|
|
340
|
+
* member what will be kept. What `splitFullName` reads.
|
|
341
|
+
*/
|
|
342
|
+
export declare const tidyFullName: (fullName: string) => string;
|
|
276
343
|
/**
|
|
277
344
|
* The name a member is shown by. ONE rule for every body that writes it:
|
|
278
345
|
* onboarding's finish, the profile patch and the personal-details patch, so a
|
|
279
|
-
* name one accepts the others can never refuse
|
|
280
|
-
*
|
|
281
|
-
*
|
|
346
|
+
* name one accepts the others can never refuse, and each stores it TIDIED
|
|
347
|
+
* (`tidyFullName`, 0.17.0: "Asha Rao" from the Me editor was stored as
|
|
348
|
+
* typed, while onboarding sent it tidy). Plain bounds, no custom message:
|
|
349
|
+
* `too_small` with minimum 1 already says "required", and the client words it.
|
|
282
350
|
*/
|
|
283
|
-
export declare const displayNameSchema: z.ZodString
|
|
351
|
+
export declare const displayNameSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
352
|
+
/**
|
|
353
|
+
* A full name's first and last name, DEDUCED, never asked (the user,
|
|
354
|
+
* 2026-10-18: onboarding asks one "Full name" and works the two out from the
|
|
355
|
+
* space). The first word is the first name and everything after the first
|
|
356
|
+
* space is the last name, kept as written ("Asha Rao Kulkarni" is Asha and
|
|
357
|
+
* Rao Kulkarni).
|
|
358
|
+
*
|
|
359
|
+
* ⚠ A ONE-WORD NAME IS A WHOLE NAME. Many people in India go by a single name,
|
|
360
|
+
* so `lastName` is then `''`, and nothing may refuse it for that.
|
|
361
|
+
*
|
|
362
|
+
* Derived wherever it is needed, never stored as a second copy of the name:
|
|
363
|
+
* `displayName` stays the one owner of a member's name, so a rename can never
|
|
364
|
+
* leave a stale first name behind.
|
|
365
|
+
*/
|
|
366
|
+
export declare function splitFullName(fullName: string): {
|
|
367
|
+
firstName: string;
|
|
368
|
+
lastName: string;
|
|
369
|
+
};
|
|
370
|
+
/** The digits an Indian mobile number has: what `phoneSchema` takes and the field keeps. */
|
|
371
|
+
export declare const PHONE_DIGITS = 10;
|
|
284
372
|
/**
|
|
285
373
|
* Indian mobile number: ten raw digits, no country code, matching the standard
|
|
286
374
|
* used across this family. Display adds the `+91`.
|
|
@@ -292,6 +380,13 @@ export declare const displayNameSchema: z.ZodString;
|
|
|
292
380
|
*/
|
|
293
381
|
export declare const phoneSchema: z.ZodString;
|
|
294
382
|
export type Phone = z.infer<typeof phoneSchema>;
|
|
383
|
+
/**
|
|
384
|
+
* An Indian mobile number as `phoneSchema`'s ten digits, from however it was
|
|
385
|
+
* typed (0.17.0, moved here from the web): `98765 43210`, `+91 98765-43210`,
|
|
386
|
+
* `091 9876543210`. Anything else comes back digits only and fails the schema
|
|
387
|
+
* with `phoneInvalid`, rather than being guessed at.
|
|
388
|
+
*/
|
|
389
|
+
export declare function normalizePhone(raw: string): string;
|
|
295
390
|
/**
|
|
296
391
|
* Where the founder is based. City and country are what Discover filters on.
|
|
297
392
|
*
|
|
@@ -328,18 +423,6 @@ export declare const coordinatesQuerySchema: z.ZodObject<{
|
|
|
328
423
|
}, z.core.$strip>;
|
|
329
424
|
export type CoordinatesQuery = z.infer<typeof coordinatesQuerySchema>;
|
|
330
425
|
export type CoordinatesQueryInput = z.input<typeof coordinatesQuerySchema>;
|
|
331
|
-
/**
|
|
332
|
-
* The city these coordinates look like, for the member to CONFIRM, or `null`
|
|
333
|
-
* when the lookup could not place them confidently (or they are outside India,
|
|
334
|
-
* whose area data is all there is). A suggestion, never a write: only what the
|
|
335
|
-
* member then submits becomes their location.
|
|
336
|
-
*/
|
|
337
|
-
export declare const suggestedLocationSchema: z.ZodNullable<z.ZodObject<{
|
|
338
|
-
city: z.ZodString;
|
|
339
|
-
state: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
340
|
-
country: z.ZodString;
|
|
341
|
-
}, z.core.$strip>>;
|
|
342
|
-
export type SuggestedLocation = z.infer<typeof suggestedLocationSchema>;
|
|
343
426
|
/**
|
|
344
427
|
* Cursor pagination query. Feed and Discover both use it.
|
|
345
428
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/core/common.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/core/common.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAexB;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,OAGvD,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,GAAI,kBAAe,yDAC8B,CAAC;AAEpF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,KAAK,MAAM,2DAKlC,CAAC;AAEzC,4EAA4E;AAC5E,eAAO,MAAM,cAAc,wDAGS,CAAC;AACrC,yFAAyF;AACzF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAI5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,aAAa,UAIxB,CAAC;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,UAAkC,CAAC;AAC7D,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAM1E;AAED,2EAA2E;AAC3E,eAAO,MAAM,cAAc,GAAI,OAAO,SAAS,MAAM,EAAE,EAAE,SAAS,MAAM,aACI,CAAC;AAE7E;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,4CAA6C,CAAC;AAC7F,eAAO,MAAM,+BAA+B;;;;EAA6C,CAAC;AAC1F,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAExF;;;;GAIG;AACH,eAAO,MAAM,aAAa,8HAQhB,CAAC;AAEX;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgBzD;AAID;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B,eAAO,MAAM,cAAc,aAOqD,CAAC;AACjF,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,uNAqBrB,CAAC;AAIX,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,aAE/B,CAAC;AAIH;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,qGASvB,CAAC;AACX,eAAO,MAAM,kBAAkB;;;;;;;;;EAA+B,CAAC;AAC/D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;GAGG;AACH,eAAO,MAAM,eAAe,2PAsBlB,CAAC;AACX,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;EAA0B,CAAC;AACtD,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,YAAY,8LAgBf,CAAC;AACX,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;EAAuB,CAAC;AAChD,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,WAAW,kUAwBd,CAAC;AACX,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;EAAsB,CAAC;AAC9C,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAK9C;;;;;GAKG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;GAGsC,CAAC;AAElE,yGAAyG;AACzG,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;GAGsC,CAAC;AAInE;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,2DAA4D,CAAC;AACvF,eAAO,MAAM,YAAY;;;;;EAAwB,CAAC;AAClD,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAIlD;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,UAAU,MAAM,KAAG,MAA8C,CAAC;AAE/F;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,wDAKJ,CAAC;AAE3B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAMvF;AAID,4FAA4F;AAC5F,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,aAGgE,CAAC;AACzF,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMlD;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,cAAc;;;;iBAIzB,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB;;;iBAGjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACtE,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAI3E;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;iBAYhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACpE,kFAAkF;AAClF,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
|
package/dist/core/common.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module founder-schema/core/common
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
-
import { CITY_MAX, COUNTRY_MAX, DISPLAY_NAME_MAX, LINK_URL_MAX, PAGE_SIZE_DEFAULT, PAGE_SIZE_MAX, STATE_MAX, } from './limits.js';
|
|
10
|
+
import { CITY_MAX, COUNTRY_MAX, DISPLAY_NAME_MAX, LINK_URL_MAX, PAGE_SIZE_DEFAULT, PAGE_SIZE_MAX, SKILLS_MAX, STATE_MAX, } from './limits.js';
|
|
11
11
|
import { VALIDATION_MESSAGE } from './validation.js';
|
|
12
12
|
// ─── String helpers ──────────────────────────────────────────────────────────
|
|
13
13
|
/**
|
|
@@ -94,6 +94,19 @@ export const safeUrlSchema = z.url({
|
|
|
94
94
|
* `imageUrl` would have failed a whole feed page.
|
|
95
95
|
*/
|
|
96
96
|
export const httpUrlSchema = safeUrlSchema.max(LINK_URL_MAX);
|
|
97
|
+
/**
|
|
98
|
+
* A link as people paste it, made into what `httpUrlSchema` checks (0.17.0,
|
|
99
|
+
* moved here from the web so every client cleans a link the same way):
|
|
100
|
+
* `linkedin.com/in/asha` gains `https://`; blank stays blank. Something that
|
|
101
|
+
* already names a scheme keeps it, so `javascript:` is never quietly "fixed"
|
|
102
|
+
* into a link: the schema then refuses it, as it only takes http and https.
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeLinkUrl(raw) {
|
|
105
|
+
const trimmed = raw.trim();
|
|
106
|
+
if (!trimmed)
|
|
107
|
+
return '';
|
|
108
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
109
|
+
}
|
|
97
110
|
/**
|
|
98
111
|
* True when `url` parses and its hostname is EXACTLY one of `hosts`
|
|
99
112
|
* (lowercase). The one host check in this package, written the way the warning
|
|
@@ -312,9 +325,10 @@ export const skillSchema = z.enum(SKILL_VALUES);
|
|
|
312
325
|
* venture works only for a member who ALREADY HAS a venture row, and somebody
|
|
313
326
|
* who wants to start a company is precisely the member with no venture at all,
|
|
314
327
|
* so there is nothing to infer from. It also cannot express a founder with a
|
|
315
|
-
* running company who wants to start another.
|
|
316
|
-
*
|
|
317
|
-
*
|
|
328
|
+
* running company who wants to start another. The others were all asks of other
|
|
329
|
+
* people and this one an ask of the product, which is the whole of the oddness.
|
|
330
|
+
* (hary, 2026-09-21.) Since 0.16.0 the list also holds offers (`mentor_others`,
|
|
331
|
+
* `invest_in_startups`) and a second ask of the product (`attend_events`).
|
|
318
332
|
*/
|
|
319
333
|
export const GOAL_VALUES = [
|
|
320
334
|
'find_cofounder',
|
|
@@ -328,8 +342,37 @@ export const GOAL_VALUES = [
|
|
|
328
342
|
'market_insights',
|
|
329
343
|
'learn_startup',
|
|
330
344
|
'start_company',
|
|
345
|
+
// 0.16.0 (the user, 2026-10-18: "a few more ... cover most of the goals"):
|
|
346
|
+
// the journey's own asks (feedback on an idea, early testers), the other
|
|
347
|
+
// side of mentorship and of fundraising (mentoring, investing), and three
|
|
348
|
+
// more a founder network is asked for: joining a startup, finding
|
|
349
|
+
// service providers, and events. Appended, per the rule above.
|
|
350
|
+
'get_feedback',
|
|
351
|
+
'find_testers',
|
|
352
|
+
'mentor_others',
|
|
353
|
+
'invest_in_startups',
|
|
354
|
+
'join_startup',
|
|
355
|
+
'find_service_providers',
|
|
356
|
+
'attend_events',
|
|
331
357
|
];
|
|
332
358
|
export const goalSchema = z.enum(GOAL_VALUES);
|
|
359
|
+
/** True when no value is in the list twice. */
|
|
360
|
+
const eachOnce = (items) => new Set(items).size === items.length;
|
|
361
|
+
/**
|
|
362
|
+
* A member's goals as a list (0.17.0): each goal once, at most every goal.
|
|
363
|
+
* ONE rule for onboarding's finish (which adds its minimum, `GOALS_MIN`) and
|
|
364
|
+
* the Me tab's editor: two copies of one goal met the minimum while meaning
|
|
365
|
+
* one, and the profile would show it twice.
|
|
366
|
+
*/
|
|
367
|
+
export const goalListSchema = z
|
|
368
|
+
.array(goalSchema)
|
|
369
|
+
.max(GOAL_VALUES.length)
|
|
370
|
+
.refine(eachOnce, { error: VALIDATION_MESSAGE.goalsDuplicate });
|
|
371
|
+
/** A member's skills (0.17.0): each skill once, at most `SKILLS_MAX`. ONE rule for onboarding and Me. */
|
|
372
|
+
export const skillListSchema = z
|
|
373
|
+
.array(skillSchema)
|
|
374
|
+
.max(SKILLS_MAX)
|
|
375
|
+
.refine(eachOnce, { error: VALIDATION_MESSAGE.skillsDuplicate });
|
|
333
376
|
// ─── Gender ──────────────────────────────────────────────────────────────────
|
|
334
377
|
/**
|
|
335
378
|
* Demographic only. Nothing here computes anything from it, unlike the sibling
|
|
@@ -342,15 +385,51 @@ export const goalSchema = z.enum(GOAL_VALUES);
|
|
|
342
385
|
export const GENDER_VALUES = ['male', 'female', 'other', 'prefer_not_to_say'];
|
|
343
386
|
export const genderSchema = z.enum(GENDER_VALUES);
|
|
344
387
|
// ─── Display name ────────────────────────────────────────────────────────────
|
|
388
|
+
/**
|
|
389
|
+
* A full name as it is sent and stored (0.17.0, moved here from the web):
|
|
390
|
+
* trimmed, and every run of spaces made one. `displayNameSchema` applies it,
|
|
391
|
+
* so the server stores it from every client; a client applies it to show the
|
|
392
|
+
* member what will be kept. What `splitFullName` reads.
|
|
393
|
+
*/
|
|
394
|
+
export const tidyFullName = (fullName) => fullName.trim().replace(/\s+/g, ' ');
|
|
345
395
|
/**
|
|
346
396
|
* The name a member is shown by. ONE rule for every body that writes it:
|
|
347
397
|
* onboarding's finish, the profile patch and the personal-details patch, so a
|
|
348
|
-
* name one accepts the others can never refuse
|
|
349
|
-
*
|
|
350
|
-
*
|
|
398
|
+
* name one accepts the others can never refuse, and each stores it TIDIED
|
|
399
|
+
* (`tidyFullName`, 0.17.0: "Asha Rao" from the Me editor was stored as
|
|
400
|
+
* typed, while onboarding sent it tidy). Plain bounds, no custom message:
|
|
401
|
+
* `too_small` with minimum 1 already says "required", and the client words it.
|
|
351
402
|
*/
|
|
352
|
-
export const displayNameSchema = z
|
|
403
|
+
export const displayNameSchema = z
|
|
404
|
+
.string()
|
|
405
|
+
.trim()
|
|
406
|
+
.min(1)
|
|
407
|
+
.max(DISPLAY_NAME_MAX)
|
|
408
|
+
.transform(tidyFullName);
|
|
409
|
+
/**
|
|
410
|
+
* A full name's first and last name, DEDUCED, never asked (the user,
|
|
411
|
+
* 2026-10-18: onboarding asks one "Full name" and works the two out from the
|
|
412
|
+
* space). The first word is the first name and everything after the first
|
|
413
|
+
* space is the last name, kept as written ("Asha Rao Kulkarni" is Asha and
|
|
414
|
+
* Rao Kulkarni).
|
|
415
|
+
*
|
|
416
|
+
* ⚠ A ONE-WORD NAME IS A WHOLE NAME. Many people in India go by a single name,
|
|
417
|
+
* so `lastName` is then `''`, and nothing may refuse it for that.
|
|
418
|
+
*
|
|
419
|
+
* Derived wherever it is needed, never stored as a second copy of the name:
|
|
420
|
+
* `displayName` stays the one owner of a member's name, so a rename can never
|
|
421
|
+
* leave a stale first name behind.
|
|
422
|
+
*/
|
|
423
|
+
export function splitFullName(fullName) {
|
|
424
|
+
const name = tidyFullName(fullName);
|
|
425
|
+
const space = name.indexOf(' ');
|
|
426
|
+
return space === -1
|
|
427
|
+
? { firstName: name, lastName: '' }
|
|
428
|
+
: { firstName: name.slice(0, space), lastName: name.slice(space + 1) };
|
|
429
|
+
}
|
|
353
430
|
// ─── Phone ───────────────────────────────────────────────────────────────────
|
|
431
|
+
/** The digits an Indian mobile number has: what `phoneSchema` takes and the field keeps. */
|
|
432
|
+
export const PHONE_DIGITS = 10;
|
|
354
433
|
/**
|
|
355
434
|
* Indian mobile number: ten raw digits, no country code, matching the standard
|
|
356
435
|
* used across this family. Display adds the `+91`.
|
|
@@ -363,7 +442,23 @@ export const displayNameSchema = z.string().trim().min(1).max(DISPLAY_NAME_MAX);
|
|
|
363
442
|
export const phoneSchema = z
|
|
364
443
|
.string()
|
|
365
444
|
.trim()
|
|
366
|
-
.regex(
|
|
445
|
+
.regex(new RegExp(`^[6-9]\\d{${PHONE_DIGITS - 1}}$`), VALIDATION_MESSAGE.phoneInvalid);
|
|
446
|
+
/**
|
|
447
|
+
* An Indian mobile number as `phoneSchema`'s ten digits, from however it was
|
|
448
|
+
* typed (0.17.0, moved here from the web): `98765 43210`, `+91 98765-43210`,
|
|
449
|
+
* `091 9876543210`. Anything else comes back digits only and fails the schema
|
|
450
|
+
* with `phoneInvalid`, rather than being guessed at.
|
|
451
|
+
*/
|
|
452
|
+
export function normalizePhone(raw) {
|
|
453
|
+
let digits = raw.replace(/\D/g, '');
|
|
454
|
+
if (digits.length === PHONE_DIGITS + 2 && digits.startsWith('91'))
|
|
455
|
+
digits = digits.slice(2);
|
|
456
|
+
else if (digits.length === PHONE_DIGITS + 1 && digits.startsWith('0'))
|
|
457
|
+
digits = digits.slice(1);
|
|
458
|
+
else if (digits.length === PHONE_DIGITS + 3 && digits.startsWith('091'))
|
|
459
|
+
digits = digits.slice(3);
|
|
460
|
+
return digits;
|
|
461
|
+
}
|
|
367
462
|
// ─── Location ────────────────────────────────────────────────────────────────
|
|
368
463
|
/**
|
|
369
464
|
* Where the founder is based. City and country are what Discover filters on.
|
|
@@ -398,13 +493,6 @@ export const coordinatesQuerySchema = z.object({
|
|
|
398
493
|
lat: z.preprocess(emptyStringToUndefined, z.coerce.number().min(-90).max(90)),
|
|
399
494
|
lng: z.preprocess(emptyStringToUndefined, z.coerce.number().min(-180).max(180)),
|
|
400
495
|
});
|
|
401
|
-
/**
|
|
402
|
-
* The city these coordinates look like, for the member to CONFIRM, or `null`
|
|
403
|
-
* when the lookup could not place them confidently (or they are outside India,
|
|
404
|
-
* whose area data is all there is). A suggestion, never a write: only what the
|
|
405
|
-
* member then submits becomes their location.
|
|
406
|
-
*/
|
|
407
|
-
export const suggestedLocationSchema = locationSchema.nullable();
|
|
408
496
|
// ─── Pagination ──────────────────────────────────────────────────────────────
|
|
409
497
|
/**
|
|
410
498
|
* Cursor pagination query. Feed and Discover both use it.
|