@oxyhq/contracts 0.22.0 → 0.24.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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/accountGraph.js +257 -23
- package/dist/cjs/deviceSession.js +5 -4
- package/dist/cjs/followGraph.js +28 -0
- package/dist/cjs/index.js +15 -7
- package/dist/cjs/userResponse.js +17 -3
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/accountGraph.js +253 -22
- package/dist/esm/deviceSession.js +5 -4
- package/dist/esm/followGraph.js +27 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/userResponse.js +18 -4
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/accountGraph.d.ts +124 -40
- package/dist/types/deviceSession.d.ts +5 -4
- package/dist/types/followGraph.d.ts +144 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/recommendations.d.ts +14 -14
- package/dist/types/updates.d.ts +2 -2
- package/dist/types/userResponse.d.ts +286 -62
- package/package.json +1 -1
package/dist/esm/accountGraph.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Account graph wire contracts — the account-kind vocabulary,
|
|
3
|
-
* taxonomy, and create-account input.
|
|
2
|
+
* Account graph wire contracts — the account-kind vocabulary, the account
|
|
3
|
+
* category taxonomy, and the create-account input.
|
|
4
4
|
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* `accountCategories` classifies a NON-PERSONAL account — what it is about, what
|
|
6
|
+
* it does — without polluting `User.kind`. See the block above
|
|
7
|
+
* {@link ACCOUNT_CATEGORY_IDS} for the four rules that govern it.
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
/**
|
|
11
11
|
* The union is spelled out above and the array proves coverage BOTH ways
|
|
12
12
|
* (`satisfies` here, the `Gap` alias below) — the same shape this package's
|
|
13
|
-
* `
|
|
13
|
+
* `ACCOUNT_CATEGORY_IDS` / `TRUST_TIERS` pairs use, and the one
|
|
14
14
|
* `db/schema/users.ts` mirrors to keep the `users_kind_check` CHECK honest.
|
|
15
15
|
*
|
|
16
16
|
* Deriving the union from the array instead would cost nothing here and be paid
|
|
@@ -66,13 +66,246 @@ export function isActAsEligibleKind(kind) {
|
|
|
66
66
|
export function isAccountKind(value) {
|
|
67
67
|
return typeof value === 'string' && ACCOUNT_KINDS.includes(value);
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
// ===========================================================================
|
|
70
|
+
// Account categories
|
|
71
|
+
//
|
|
72
|
+
// Four rules govern this taxonomy. Each one is here because the obvious
|
|
73
|
+
// alternative fails silently rather than loudly.
|
|
74
|
+
//
|
|
75
|
+
// 1. AN ID IS AN OPAQUE, IMMUTABLE SLUG — the LABEL is not stored anywhere.
|
|
76
|
+
// Every value below is an identifier that no rename may ever change. The
|
|
77
|
+
// human-readable label lives in each client's own translation catalogue,
|
|
78
|
+
// keyed by the id. Re-labelling `agency` from "Real estate agency" to
|
|
79
|
+
// "Agency" is therefore a one-line locale edit that touches no row; moving
|
|
80
|
+
// the label into the column or the DTO would make the same edit a data
|
|
81
|
+
// migration, and would pin every reader to the language of whoever chose it.
|
|
82
|
+
//
|
|
83
|
+
// 2. THE PRIMARY CATEGORY IS THE FIRST ELEMENT of an ordered list. Not a
|
|
84
|
+
// separate flag, and not a second field: a flag admits two primaries and
|
|
85
|
+
// admits none, and ordering makes both unrepresentable. The cost is that
|
|
86
|
+
// ORDER IS DATA — anything that re-serializes, sorts or de-duplicates this
|
|
87
|
+
// list can change which category is primary without erroring, so no layer
|
|
88
|
+
// between the column and the client may reorder it.
|
|
89
|
+
//
|
|
90
|
+
// 3. THE VOCABULARY IS APPEND-ONLY. `ACCOUNT_CATEGORY_IDS` may gain ids and may
|
|
91
|
+
// never lose one, because rows already carry the ids it holds. Withdrawing a
|
|
92
|
+
// category means listing it in {@link RETIRED_ACCOUNT_CATEGORY_IDS}, which
|
|
93
|
+
// removes it from the picker while leaving it readable and re-writable. The
|
|
94
|
+
// database enforces the same asymmetry: its CHECK is re-evaluated on EVERY
|
|
95
|
+
// update to a row, so narrowing the allowed set makes an unrelated write —
|
|
96
|
+
// saving a bio — fail on any account that had picked the withdrawn value.
|
|
97
|
+
// Measured on a real Postgres, `NOT VALID` included; it does not help.
|
|
98
|
+
//
|
|
99
|
+
// 4. THE SET IS CLOSED. An id nobody can validate is an id no client can render:
|
|
100
|
+
// the label comes from a translation key derived from the id, so an invented
|
|
101
|
+
// value paints as a blank or a raw slug in every app at once. Closing it also
|
|
102
|
+
// costs nothing that rule 3 does not already charge — adding a category needs
|
|
103
|
+
// a migration to widen the CHECK whether or not the enum exists, so an open
|
|
104
|
+
// list would buy back no work, only the validation.
|
|
105
|
+
// ===========================================================================
|
|
106
|
+
/**
|
|
107
|
+
* Every account category, by stable id.
|
|
108
|
+
*
|
|
109
|
+
* Grouped by comment for readability only; the storage, the wire and the picker
|
|
110
|
+
* all treat this as one flat list. `other` is the escape hatch for an account
|
|
111
|
+
* that fits nothing here.
|
|
112
|
+
*
|
|
113
|
+
* TO ADD ONE: append an id (lowercase ASCII, `snake_case`) here, publish
|
|
114
|
+
* `@oxyhq/contracts`, then ship a migration that widens
|
|
115
|
+
* `users_account_categories_check` — never edit an existing migration — and add
|
|
116
|
+
* an `accounts.accountCategory.<id>` label to each client's locales.
|
|
117
|
+
*
|
|
118
|
+
* TO WITHDRAW ONE: leave the id here and add it to
|
|
119
|
+
* {@link RETIRED_ACCOUNT_CATEGORY_IDS}. See rule 3 above.
|
|
120
|
+
*/
|
|
121
|
+
export const ACCOUNT_CATEGORY_IDS = [
|
|
122
|
+
// ---- media & public information -----------------------------------------
|
|
123
|
+
'news',
|
|
124
|
+
'politics',
|
|
125
|
+
// ---- business & economy --------------------------------------------------
|
|
126
|
+
'business',
|
|
127
|
+
'startup',
|
|
128
|
+
'finance',
|
|
129
|
+
'crypto',
|
|
130
|
+
'marketplace',
|
|
131
|
+
'retail',
|
|
132
|
+
// The four ids the single-valued `organizationCategory` field used to hold,
|
|
133
|
+
// carried forward VERBATIM. Their labels may be rewritten freely; their ids
|
|
134
|
+
// may not, because live rows hold them.
|
|
135
|
+
'real_estate',
|
|
70
136
|
'agency',
|
|
71
|
-
'cooperative',
|
|
72
137
|
'landlord',
|
|
138
|
+
'cooperative',
|
|
139
|
+
'architecture',
|
|
140
|
+
// ---- technology ----------------------------------------------------------
|
|
141
|
+
'technology',
|
|
142
|
+
'software',
|
|
143
|
+
'ai',
|
|
144
|
+
'security',
|
|
145
|
+
'automation',
|
|
146
|
+
// ---- knowledge -----------------------------------------------------------
|
|
147
|
+
'science',
|
|
148
|
+
'education',
|
|
149
|
+
'books',
|
|
150
|
+
// ---- health --------------------------------------------------------------
|
|
151
|
+
'health',
|
|
152
|
+
'fitness',
|
|
153
|
+
// ---- sport & play --------------------------------------------------------
|
|
154
|
+
'sports',
|
|
155
|
+
'gaming',
|
|
156
|
+
// ---- culture & entertainment ---------------------------------------------
|
|
157
|
+
//
|
|
158
|
+
// There is deliberately NO generic `entertainment` here, and re-adding one is
|
|
159
|
+
// a regression rather than a gap. It is the only id this list ever carried
|
|
160
|
+
// that was dominated by its own specifics — `film`, `music`, `gaming` and
|
|
161
|
+
// `comedy` all exist — and a generic drawer sitting beside its four
|
|
162
|
+
// concretions collects the lazy pick, which degrades the data for all four at
|
|
163
|
+
// once: the accounts that would have said `film` say `entertainment` instead,
|
|
164
|
+
// and `film` stops meaning what it meant.
|
|
165
|
+
//
|
|
166
|
+
// The other overlaps in this vocabulary are NOT the same case and must not be
|
|
167
|
+
// merged on this reasoning: `sports`/`fitness`, `art`/`photography`,
|
|
168
|
+
// `business`/`startup`, `finance`/`crypto`, `home_garden`/`diy` and
|
|
169
|
+
// `technology`/`software`/`security` are genuinely different audiences, and
|
|
170
|
+
// with a cap of four the granularity is cheap.
|
|
171
|
+
'music',
|
|
172
|
+
'film',
|
|
173
|
+
'podcast',
|
|
174
|
+
'art',
|
|
175
|
+
'photography',
|
|
176
|
+
'comedy',
|
|
177
|
+
// ---- everyday life -------------------------------------------------------
|
|
178
|
+
'food',
|
|
179
|
+
'travel',
|
|
180
|
+
'fashion',
|
|
181
|
+
'home_garden',
|
|
182
|
+
'diy',
|
|
183
|
+
'automotive',
|
|
184
|
+
'animals',
|
|
185
|
+
'family',
|
|
186
|
+
// ---- society -------------------------------------------------------------
|
|
187
|
+
'nonprofit',
|
|
188
|
+
'government',
|
|
189
|
+
'community',
|
|
190
|
+
'activism',
|
|
191
|
+
'environment',
|
|
192
|
+
'religion',
|
|
193
|
+
// ---- fallback ------------------------------------------------------------
|
|
73
194
|
'other',
|
|
74
195
|
];
|
|
75
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Accepts EVERY id, withdrawn ones included — see rule 3.
|
|
198
|
+
*
|
|
199
|
+
* A schema that rejected a withdrawn id would 400 the whole request whenever a
|
|
200
|
+
* client round-trips the categories it was served, so an account that had
|
|
201
|
+
* picked one could no longer save its bio either. That is the same failure the
|
|
202
|
+
* nullable `bio` / `avatar` fix addressed, wearing a different hat.
|
|
203
|
+
*/
|
|
204
|
+
export const accountCategoryIdSchema = z.enum(ACCOUNT_CATEGORY_IDS);
|
|
205
|
+
/**
|
|
206
|
+
* Ids withdrawn from the picker. Empty today.
|
|
207
|
+
*
|
|
208
|
+
* A withdrawn id keeps working everywhere it is already stored: it validates,
|
|
209
|
+
* it survives a round-trip save, it still renders from its label key, and it
|
|
210
|
+
* stays PRIMARY if it was primary. Nothing rewrites a stored list — a read-time
|
|
211
|
+
* or migration-time demotion would silently replace a choice its owner made,
|
|
212
|
+
* which is precisely what stable ids exist to prevent. The owner drops it on
|
|
213
|
+
* their next edit; until then it is honoured.
|
|
214
|
+
*
|
|
215
|
+
* What withdrawal changes is only this: the id leaves
|
|
216
|
+
* {@link SELECTABLE_ACCOUNT_CATEGORY_IDS}, so no picker offers it, and
|
|
217
|
+
* {@link newlyAddedRetiredCategories} refuses to let a write ADD it to an
|
|
218
|
+
* account that did not already have it.
|
|
219
|
+
*/
|
|
220
|
+
export const RETIRED_ACCOUNT_CATEGORY_IDS = [];
|
|
221
|
+
/** Whether a category may still be OFFERED. A stored one is readable either way. */
|
|
222
|
+
export function isSelectableAccountCategoryId(id) {
|
|
223
|
+
return !RETIRED_ACCOUNT_CATEGORY_IDS.includes(id);
|
|
224
|
+
}
|
|
225
|
+
/** The ids a picker may offer, in declaration order. */
|
|
226
|
+
export const SELECTABLE_ACCOUNT_CATEGORY_IDS = ACCOUNT_CATEGORY_IDS.filter(isSelectableAccountCategoryId);
|
|
227
|
+
/**
|
|
228
|
+
* Which of `next` are withdrawn ids the account did not already carry — i.e.
|
|
229
|
+
* the ones a write must be refused for.
|
|
230
|
+
*
|
|
231
|
+
* `retired` is a parameter rather than a module read so the rule can be
|
|
232
|
+
* exercised against a non-empty set while the production one is empty; a test
|
|
233
|
+
* over `RETIRED_ACCOUNT_CATEGORY_IDS` alone would pass vacuously today and stay
|
|
234
|
+
* passing if the rule were deleted.
|
|
235
|
+
*/
|
|
236
|
+
export function newlyAddedRetiredCategories(next, previous, retired) {
|
|
237
|
+
return next.filter((id) => retired.includes(id) && !previous.includes(id));
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* How many categories one account may carry.
|
|
241
|
+
*
|
|
242
|
+
* Four, not "as many as you like". Three reasons, in the order they bind:
|
|
243
|
+
*
|
|
244
|
+
* - The primary has to MEAN something. At ten categories the first element
|
|
245
|
+
* reads as a sort artifact rather than a choice, and rule 2 above is the
|
|
246
|
+
* entire mechanism by which a primary exists.
|
|
247
|
+
* - The profile RENDERS them as a row of chips; four labels of this length is
|
|
248
|
+
* what fits a phone-width profile header before the row wraps or truncates.
|
|
249
|
+
* - Four is enough to place a genuinely compound account without a tag cloud:
|
|
250
|
+
* a housing cooperative that is also a non-profit serving a local community
|
|
251
|
+
* spends `cooperative`, `nonprofit`, `community`, `real_estate` — and is the
|
|
252
|
+
* most compound real example in the ecosystem.
|
|
253
|
+
*
|
|
254
|
+
* One constant, read by the wire schema, the database CHECK and the picker, so
|
|
255
|
+
* changing it is one edit plus a migration.
|
|
256
|
+
*/
|
|
257
|
+
export const MAX_ACCOUNT_CATEGORIES = 4;
|
|
258
|
+
/**
|
|
259
|
+
* An account's categories on the wire. ORDER IS MEANINGFUL — index 0 is the
|
|
260
|
+
* primary (rule 2).
|
|
261
|
+
*
|
|
262
|
+
* A duplicate is REJECTED rather than silently collapsed. De-duplicating would
|
|
263
|
+
* rewrite the caller's list, and any rewrite of this list can move which id sits
|
|
264
|
+
* at index 0 — so the one repair available here is the one that would break the
|
|
265
|
+
* property the list exists to carry. A duplicate only ever comes from a client
|
|
266
|
+
* bug, and a 400 naming the index is how that bug gets found.
|
|
267
|
+
*/
|
|
268
|
+
export const accountCategoriesSchema = z
|
|
269
|
+
.array(accountCategoryIdSchema)
|
|
270
|
+
.max(MAX_ACCOUNT_CATEGORIES)
|
|
271
|
+
.superRefine((ids, ctx) => {
|
|
272
|
+
const seen = new Set();
|
|
273
|
+
ids.forEach((id, index) => {
|
|
274
|
+
if (seen.has(id)) {
|
|
275
|
+
ctx.addIssue({
|
|
276
|
+
code: z.ZodIssueCode.custom,
|
|
277
|
+
message: `Duplicate account category "${id}"`,
|
|
278
|
+
path: [index],
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
seen.add(id);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
/**
|
|
285
|
+
* Kinds that may carry categories: every kind EXCEPT `personal`.
|
|
286
|
+
*
|
|
287
|
+
* A person has interests, not a sector — and their interests are not a
|
|
288
|
+
* classification anybody else gets to read off their profile. Spelled out
|
|
289
|
+
* positively, like {@link isActAsEligibleKind} and for the same reason: a `kind
|
|
290
|
+
* !== 'personal'` test silently admits every kind invented after it was
|
|
291
|
+
* written, whereas this list forces whoever adds one to decide.
|
|
292
|
+
*/
|
|
293
|
+
export const ACCOUNT_CATEGORY_KINDS = [
|
|
294
|
+
'organization',
|
|
295
|
+
'project',
|
|
296
|
+
'bot',
|
|
297
|
+
'channel',
|
|
298
|
+
];
|
|
299
|
+
/**
|
|
300
|
+
* Whether an account of this kind may carry categories.
|
|
301
|
+
*
|
|
302
|
+
* The API refuses the write and the `users_account_categories_kind_check`
|
|
303
|
+
* constraint makes it unrepresentable; both derive from
|
|
304
|
+
* {@link ACCOUNT_CATEGORY_KINDS}, so they cannot disagree.
|
|
305
|
+
*/
|
|
306
|
+
export function kindAcceptsAccountCategories(kind) {
|
|
307
|
+
return ACCOUNT_CATEGORY_KINDS.includes(kind ?? '');
|
|
308
|
+
}
|
|
76
309
|
/**
|
|
77
310
|
* An account's name on the create/update wire.
|
|
78
311
|
*
|
|
@@ -96,10 +329,16 @@ const accountNameSchema = z
|
|
|
96
329
|
.optional();
|
|
97
330
|
/**
|
|
98
331
|
* POST /accounts — create a non-personal account under the caller's tree.
|
|
99
|
-
*
|
|
332
|
+
*
|
|
333
|
+
* No cross-field refinement guards `accountCategories`, and that is not an
|
|
334
|
+
* omission: `kind` here is a CHILD kind, and every child kind is in
|
|
335
|
+
* {@link ACCOUNT_CATEGORY_KINDS}, so `personal` is already unrepresentable on
|
|
336
|
+
* this route. The refinement the single-valued predecessor needed disappeared
|
|
337
|
+
* along with the restriction that made it necessary. A child kind that does NOT
|
|
338
|
+
* accept categories would break that reasoning silently, so
|
|
339
|
+
* `__tests__/accountGraph.test.ts` asserts the two lists agree.
|
|
100
340
|
*/
|
|
101
|
-
export const createAccountRequestSchema = z
|
|
102
|
-
.object({
|
|
341
|
+
export const createAccountRequestSchema = z.object({
|
|
103
342
|
parentAccountId: z.string().trim().min(1).optional(),
|
|
104
343
|
kind: childAccountKindSchema,
|
|
105
344
|
username: z.string().trim().min(1).max(100),
|
|
@@ -107,14 +346,6 @@ export const createAccountRequestSchema = z
|
|
|
107
346
|
bio: z.string().trim().max(500).optional(),
|
|
108
347
|
avatar: z.string().optional(),
|
|
109
348
|
description: z.string().trim().max(1000).optional(),
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.superRefine((data, ctx) => {
|
|
113
|
-
if (data.organizationCategory !== undefined && data.kind !== 'organization') {
|
|
114
|
-
ctx.addIssue({
|
|
115
|
-
code: z.ZodIssueCode.custom,
|
|
116
|
-
message: 'organizationCategory applies only when kind is organization',
|
|
117
|
-
path: ['organizationCategory'],
|
|
118
|
-
});
|
|
119
|
-
}
|
|
349
|
+
/** Ordered, PRIMARY FIRST — see rule 2 above {@link ACCOUNT_CATEGORY_IDS}. */
|
|
350
|
+
accountCategories: accountCategoriesSchema.optional(),
|
|
120
351
|
});
|
|
@@ -45,10 +45,11 @@ export const deviceTokenMintRequestSchema = z.object({
|
|
|
45
45
|
});
|
|
46
46
|
/**
|
|
47
47
|
* Wire shape of a successful `POST /session/device/token`: the freshly-minted
|
|
48
|
-
* short access token for the active account, its expiry, the
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* projected device-session state.
|
|
48
|
+
* short access token for the active account, its expiry, the device secret the
|
|
49
|
+
* client must persist (`nextDeviceSecret` — on mint this echoes the presented
|
|
50
|
+
* secret unchanged so concurrent refreshes from multiple origins do not race),
|
|
51
|
+
* and the projected device-session state. Sign-in rotates the secret via
|
|
52
|
+
* `issueDeviceSecret`; mint does not.
|
|
52
53
|
*/
|
|
53
54
|
export const deviceTokenMintResponseSchema = z.object({
|
|
54
55
|
accessToken: z.string(),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The follow graph wire contract (`/v2/follows`).
|
|
3
|
+
*
|
|
4
|
+
* These types are the boundary between the API that owns the graph and every
|
|
5
|
+
* application that reads it. They live here — not in the API and not in the
|
|
6
|
+
* SDK — because both ends have to agree, and a shape defined on one side is a
|
|
7
|
+
* shape the other side re-declares slightly differently within a release or two.
|
|
8
|
+
*
|
|
9
|
+
* ## Why the state is three fields and not a boolean
|
|
10
|
+
*
|
|
11
|
+
* A user can follow something globally and turn it off in ONE application. That
|
|
12
|
+
* is a state the user themselves created, so the client has to be able to see
|
|
13
|
+
* it and say so — "following, but not shown here" is a sentence a boolean
|
|
14
|
+
* cannot express. `globalState`, `applicationMode` and `effectiveState` are
|
|
15
|
+
* therefore reported separately, and only the last one answers "does this
|
|
16
|
+
* appear in my feed right now".
|
|
17
|
+
*
|
|
18
|
+
* ## Why kinds are strings
|
|
19
|
+
*
|
|
20
|
+
* `FollowTargetKind` is a plain `string`, not a union. Applications register
|
|
21
|
+
* their own kinds at runtime (`mercaria.store`, `syra.artist`), so a union here
|
|
22
|
+
* would mean every new application in the ecosystem needs a release of this
|
|
23
|
+
* package before it can follow anything. The namespace rule is enforced by the
|
|
24
|
+
* database, which is the one place that can enforce it for applications this
|
|
25
|
+
* package has never heard of.
|
|
26
|
+
*/
|
|
27
|
+
export {};
|
package/dist/esm/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Platform-agnostic — zod is the only runtime dependency. No react/react-native/
|
|
10
10
|
* expo, no `require()` in the ESM build.
|
|
11
11
|
*/
|
|
12
|
-
export { ACCOUNT_KINDS, accountKindSchema, CHILD_ACCOUNT_KINDS, childAccountKindSchema, isAccountKind, isActAsEligibleKind,
|
|
12
|
+
export { ACCOUNT_KINDS, accountKindSchema, CHILD_ACCOUNT_KINDS, childAccountKindSchema, isAccountKind, isActAsEligibleKind, ACCOUNT_CATEGORY_IDS, ACCOUNT_CATEGORY_KINDS, accountCategoriesSchema, accountCategoryIdSchema, isSelectableAccountCategoryId, kindAcceptsAccountCategories, MAX_ACCOUNT_CATEGORIES, newlyAddedRetiredCategories, RETIRED_ACCOUNT_CATEGORY_IDS, SELECTABLE_ACCOUNT_CATEGORY_IDS, createAccountRequestSchema, } from './accountGraph.js';
|
|
13
13
|
export {
|
|
14
14
|
// Schemas
|
|
15
15
|
userNameSchema, userRelationshipSchema, themePreferenceSchema, userResponseSchema, userProfileUpdateSchema, currentUserResponseSchema, deviceLinkedSessionSchema, deviceLinkedSessionsResponseSchema,
|
package/dist/esm/userResponse.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { z } from 'zod';
|
|
32
32
|
import { verifiedDomainSchema } from './identity.js';
|
|
33
|
-
import {
|
|
33
|
+
import { accountCategoriesSchema, accountKindSchema } from './accountGraph.js';
|
|
34
34
|
export const userNameSchema = z
|
|
35
35
|
.object({
|
|
36
36
|
first: z.string().optional(),
|
|
@@ -115,10 +115,24 @@ export const userResponseSchema = z
|
|
|
115
115
|
*/
|
|
116
116
|
kind: accountKindSchema.optional(),
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
118
|
+
* What this account is about — the field a profile screen RENDERS.
|
|
119
|
+
*
|
|
120
|
+
* **Ordered, primary first.** `accountCategories[0]` is the primary
|
|
121
|
+
* category; there is deliberately no sibling `primaryCategory` field,
|
|
122
|
+
* because two representations of one fact can disagree (see rule 2 in
|
|
123
|
+
* `accountGraph.ts`). Nothing downstream may sort, de-duplicate or
|
|
124
|
+
* otherwise reorder this array.
|
|
125
|
+
*
|
|
126
|
+
* **Ids, never labels.** Each element is a stable slug; the visible text
|
|
127
|
+
* comes from the reader's own translation catalogue, keyed
|
|
128
|
+
* `accounts.accountCategory.<id>`. A label on the wire would paint every
|
|
129
|
+
* profile in the language of whoever picked it.
|
|
130
|
+
*
|
|
131
|
+
* Absent when the account has none — which is every `personal` account,
|
|
132
|
+
* and any non-personal one that has not chosen. A renderer reads
|
|
133
|
+
* `user.accountCategories ?? []`.
|
|
120
134
|
*/
|
|
121
|
-
|
|
135
|
+
accountCategories: accountCategoriesSchema.optional(),
|
|
122
136
|
/**
|
|
123
137
|
* The authenticated viewer's relationship to this profile. Present ONLY
|
|
124
138
|
* on single-profile fetches (`GET /profiles/username/:username`,
|