@company-semantics/contracts 6.6.0 → 7.0.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/package.json +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +2 -0
- package/src/identity/__tests__/avatar.test.ts +12 -12
- package/src/identity/avatar.ts +17 -17
- package/src/identity/people-org-chart.ts +1 -1
- package/src/identity/schemas.ts +1 -1
- package/src/identity/types.ts +29 -9
- package/src/org/schemas.ts +1 -1
package/package.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = 'e20bec799e8f' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = 'e20bec799e8f8bd1a51fccbafa626fad0f4e001789ff8b13cda65847c32972f2' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -4078,6 +4078,7 @@ export interface components {
|
|
|
4078
4078
|
/** Format: uuid */
|
|
4079
4079
|
derivedFromUnitId: string;
|
|
4080
4080
|
derivedFromUnitName?: string;
|
|
4081
|
+
derivedFromRoot?: boolean;
|
|
4081
4082
|
derivedFromUserId?: string | null;
|
|
4082
4083
|
explanation: string;
|
|
4083
4084
|
derivationMetadata?: {
|
|
@@ -4104,6 +4105,7 @@ export interface components {
|
|
|
4104
4105
|
/** Format: uuid */
|
|
4105
4106
|
derivedFromUnitId: string;
|
|
4106
4107
|
derivedFromUnitName?: string;
|
|
4108
|
+
derivedFromRoot?: boolean;
|
|
4107
4109
|
derivedFromUserId?: string | null;
|
|
4108
4110
|
explanation: string;
|
|
4109
4111
|
derivationMetadata?: {
|
|
@@ -36,39 +36,39 @@ describe('generateInitials', () => {
|
|
|
36
36
|
})
|
|
37
37
|
|
|
38
38
|
describe('resolveAvatar', () => {
|
|
39
|
-
it('initials is always populated when source is
|
|
40
|
-
const result = resolveAvatar({
|
|
41
|
-
expect(result.source).toBe('
|
|
39
|
+
it('initials is always populated when source is photo (critical invariant)', () => {
|
|
40
|
+
const result = resolveAvatar({ avatarUrl: 'https://example.com/img.jpg', fullName: 'Ian Heidt' })
|
|
41
|
+
expect(result.source).toBe('photo')
|
|
42
42
|
expect(result.url).toBe('https://example.com/img.jpg')
|
|
43
43
|
// INVARIANT: initials is ALWAYS populated, regardless of source.
|
|
44
|
-
// This prevents UI regressions when
|
|
44
|
+
// This prevents UI regressions when the photo fails to load.
|
|
45
45
|
expect(typeof result.initials).toBe('string')
|
|
46
46
|
expect(result.initials).toBe('IH')
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
it('returns initials source when no
|
|
49
|
+
it('returns initials source when no avatarUrl', () => {
|
|
50
50
|
const result = resolveAvatar({ fullName: 'Ian Heidt' } as any)
|
|
51
51
|
expect(result.source).toBe('initials')
|
|
52
52
|
expect(result.initials).toBe('IH')
|
|
53
53
|
expect(result.url).toBeUndefined()
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
it('returns
|
|
57
|
-
const result = resolveAvatar({
|
|
58
|
-
expect(result.source).toBe('
|
|
56
|
+
it('returns photo source with empty initials when fullName is empty', () => {
|
|
57
|
+
const result = resolveAvatar({ avatarUrl: 'https://example.com/img.jpg', fullName: '' })
|
|
58
|
+
expect(result.source).toBe('photo')
|
|
59
59
|
expect(result.url).toBe('https://example.com/img.jpg')
|
|
60
60
|
// Empty is valid — just not undefined
|
|
61
61
|
expect(result.initials).toBe('')
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
it('returns initials source when
|
|
65
|
-
const result = resolveAvatar({
|
|
64
|
+
it('returns initials source when avatarUrl is explicitly undefined', () => {
|
|
65
|
+
const result = resolveAvatar({ avatarUrl: undefined, fullName: 'Ian Heidt' })
|
|
66
66
|
expect(result.source).toBe('initials')
|
|
67
67
|
expect(result.initials).toBe('IH')
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
-
it('returns initials source when
|
|
71
|
-
const result = resolveAvatar({
|
|
70
|
+
it('returns initials source when avatarUrl is empty string (falsy)', () => {
|
|
71
|
+
const result = resolveAvatar({ avatarUrl: '', fullName: 'Ian Heidt' })
|
|
72
72
|
expect(result.source).toBe('initials')
|
|
73
73
|
expect(result.initials).toBe('IH')
|
|
74
74
|
})
|
package/src/identity/avatar.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Avatar Resolution Functions
|
|
3
3
|
*
|
|
4
4
|
* Canonical functions for resolving user avatars.
|
|
5
|
-
* Determines whether to use
|
|
5
|
+
* Determines whether to use a resolved profile photo or initials fallback.
|
|
6
6
|
*
|
|
7
7
|
* @see ADR-BE-063 for design rationale
|
|
8
8
|
*/
|
|
@@ -15,22 +15,22 @@ import type { UserIdentity } from './types';
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Source of the resolved avatar.
|
|
18
|
-
* - '
|
|
18
|
+
* - 'photo': Using a resolved profile photo
|
|
19
19
|
* - 'initials': Using generated initials (fallback)
|
|
20
20
|
*/
|
|
21
|
-
export type AvatarSource = '
|
|
21
|
+
export type AvatarSource = 'photo' | 'initials';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Resolved avatar for UI rendering.
|
|
25
25
|
*
|
|
26
26
|
* INVARIANT: initials is ALWAYS populated, regardless of source.
|
|
27
|
-
* This prevents UI regressions if
|
|
27
|
+
* This prevents UI regressions if the photo fails to load.
|
|
28
28
|
*/
|
|
29
29
|
export interface ResolvedAvatar {
|
|
30
|
-
/** Source of the avatar (
|
|
30
|
+
/** Source of the avatar (photo or initials) */
|
|
31
31
|
source: AvatarSource;
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/** resolved photo URL (present only if source === photo) */
|
|
34
34
|
url?: string;
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -85,29 +85,29 @@ export function generateInitials(fullName: string): string {
|
|
|
85
85
|
* Returns a ResolvedAvatar with source, optional URL, and required initials.
|
|
86
86
|
*
|
|
87
87
|
* INVARIANT: initials is ALWAYS populated, regardless of source.
|
|
88
|
-
* This ensures the UI always has a fallback if the
|
|
88
|
+
* This ensures the UI always has a fallback if the photo fails to load.
|
|
89
89
|
*
|
|
90
|
-
* @param identity - Object with
|
|
91
|
-
* @returns ResolvedAvatar with source, url (if
|
|
90
|
+
* @param identity - Object with avatarUrl and fullName fields
|
|
91
|
+
* @returns ResolvedAvatar with source, url (if photo), and initials
|
|
92
92
|
*
|
|
93
93
|
* @example
|
|
94
|
-
* //
|
|
95
|
-
* resolveAvatar({
|
|
96
|
-
* // { source: '
|
|
94
|
+
* // Photo avatar available
|
|
95
|
+
* resolveAvatar({ avatarUrl: "https://...", fullName: "Ian Heidt" })
|
|
96
|
+
* // { source: 'photo', url: 'https://...', initials: 'IH' }
|
|
97
97
|
*
|
|
98
|
-
* // No
|
|
98
|
+
* // No photo avatar (initials fallback)
|
|
99
99
|
* resolveAvatar({ fullName: "Ian Heidt" })
|
|
100
100
|
* // { source: 'initials', initials: 'IH' }
|
|
101
101
|
*/
|
|
102
102
|
export function resolveAvatar(
|
|
103
|
-
identity: Pick<UserIdentity, '
|
|
103
|
+
identity: Pick<UserIdentity, 'avatarUrl' | 'fullName'>
|
|
104
104
|
): ResolvedAvatar {
|
|
105
105
|
const initials = generateInitials(identity.fullName);
|
|
106
106
|
|
|
107
|
-
if (identity.
|
|
107
|
+
if (identity.avatarUrl) {
|
|
108
108
|
return {
|
|
109
|
-
source: '
|
|
110
|
-
url: identity.
|
|
109
|
+
source: 'photo',
|
|
110
|
+
url: identity.avatarUrl,
|
|
111
111
|
initials,
|
|
112
112
|
};
|
|
113
113
|
}
|
|
@@ -28,7 +28,7 @@ export const PeopleOrgChartNodeSchema = z.object({
|
|
|
28
28
|
id: z.string().uuid(),
|
|
29
29
|
fullName: z.string(),
|
|
30
30
|
jobTitle: z.string().nullable(),
|
|
31
|
-
|
|
31
|
+
avatarUrl: z.string().nullable(),
|
|
32
32
|
primaryUnitId: z.string().uuid().nullable(),
|
|
33
33
|
});
|
|
34
34
|
|
package/src/identity/schemas.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { z } from 'zod';
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
|
|
17
17
|
const ResolvedAvatarSchema = z.object({
|
|
18
|
-
source: z.enum(['
|
|
18
|
+
source: z.enum(['photo', 'initials']),
|
|
19
19
|
url: z.string().optional(),
|
|
20
20
|
initials: z.string(),
|
|
21
21
|
});
|
package/src/identity/types.ts
CHANGED
|
@@ -79,26 +79,46 @@ export type UserIdentity = {
|
|
|
79
79
|
updatedAt: ISODateString;
|
|
80
80
|
|
|
81
81
|
// ==========================================================================
|
|
82
|
-
//
|
|
82
|
+
// Avatar Fields (source-agnostic; see ADR-BE-063 for original Slack origin)
|
|
83
83
|
// ==========================================================================
|
|
84
|
+
//
|
|
85
|
+
// The avatar model is source-agnostic: avatarUrl is the resolved profile
|
|
86
|
+
// image regardless of where it came from, avatarSource records the stored
|
|
87
|
+
// origin (if any), and avatarUpdatedAt records when it last changed. Avatar
|
|
88
|
+
// rendering depends only on avatarUrl (falling back to generated initials);
|
|
89
|
+
// avatarSource/avatarUpdatedAt are metadata for sync and audit.
|
|
84
90
|
|
|
85
91
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
92
|
+
* Source-agnostic resolved profile image URL.
|
|
93
|
+
* When present, avatar resolution uses this photo (source === 'photo');
|
|
94
|
+
* otherwise it falls back to generated initials.
|
|
88
95
|
*/
|
|
89
|
-
|
|
96
|
+
avatarUrl?: string;
|
|
90
97
|
|
|
91
98
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
99
|
+
* Stored origin of the avatar image.
|
|
100
|
+
* Absent when there is no stored avatar (resolution falls back to initials).
|
|
101
|
+
* - 'slack': synced from a connected Slack workspace
|
|
102
|
+
* - 'upload': uploaded directly by the user
|
|
94
103
|
*/
|
|
95
|
-
|
|
104
|
+
avatarSource?: 'slack' | 'upload';
|
|
96
105
|
|
|
97
106
|
/**
|
|
98
|
-
* When the
|
|
107
|
+
* When the avatar was last updated (synced or uploaded).
|
|
99
108
|
* Used for debugging and audit purposes.
|
|
100
109
|
*/
|
|
101
|
-
|
|
110
|
+
avatarUpdatedAt?: ISODateString;
|
|
111
|
+
|
|
112
|
+
// ==========================================================================
|
|
113
|
+
// Slack Identity Fields (ADR-BE-063)
|
|
114
|
+
// ==========================================================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Slack user ID (set when Slack is connected and email matches).
|
|
118
|
+
* Used for efficient lookups in user_change event handling.
|
|
119
|
+
* This is Slack identity, not avatar data — retained independently of avatar source.
|
|
120
|
+
*/
|
|
121
|
+
slackUserId?: string;
|
|
102
122
|
};
|
|
103
123
|
|
|
104
124
|
// =============================================================================
|
package/src/org/schemas.ts
CHANGED
|
@@ -867,7 +867,7 @@ export const OrgUnitOwnerSchema = z.object({
|
|
|
867
867
|
userId: z.string().uuid(),
|
|
868
868
|
fullName: z.string().min(1),
|
|
869
869
|
jobTitle: z.string().nullable(),
|
|
870
|
-
|
|
870
|
+
avatarUrl: z.string().nullable(),
|
|
871
871
|
/** All authority grants this user holds for the unit. At least one entry. */
|
|
872
872
|
authorities: z.array(OwnerAuthoritySchema).min(1),
|
|
873
873
|
});
|