Skip to content

Commit f8849e8

Browse files
CopilotStarefossen
andauthored
Standardize unique key generation using nanoid (#340)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Starefossen <968267+Starefossen@users.noreply.github.com> Co-authored-by: Hans Kristian Flaatten <hans.kristian.flaatten@nav.no>
1 parent 3d2fb48 commit f8849e8

7 files changed

Lines changed: 111 additions & 13 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {
2+
generateKey,
3+
ensureArrayKeys,
4+
createReference,
5+
createReferenceWithKey,
6+
} from '@/lib/sanity/helpers'
7+
8+
describe('generateKey', () => {
9+
it('should generate a unique key with default prefix', () => {
10+
const key1 = generateKey()
11+
const key2 = generateKey()
12+
13+
expect(key1).toMatch(/^item-/)
14+
expect(key2).toMatch(/^item-/)
15+
expect(key1).not.toBe(key2)
16+
})
17+
18+
it('should generate a unique key with custom prefix', () => {
19+
const key1 = generateKey('section')
20+
const key2 = generateKey('section')
21+
22+
expect(key1).toMatch(/^section-/)
23+
expect(key2).toMatch(/^section-/)
24+
expect(key1).not.toBe(key2)
25+
})
26+
27+
it('should generate unique keys on rapid successive calls', () => {
28+
const keys = Array.from({ length: 100 }, () => generateKey())
29+
const uniqueKeys = new Set(keys)
30+
31+
expect(uniqueKeys.size).toBe(keys.length)
32+
})
33+
})
34+
35+
describe('ensureArrayKeys', () => {
36+
it('should add _key to objects without one', () => {
37+
const input = [{ name: 'Item 1' }, { name: 'Item 2' }]
38+
const result = ensureArrayKeys(input)
39+
40+
expect(result[0]).toHaveProperty('_key')
41+
expect(result[1]).toHaveProperty('_key')
42+
expect(result[0]._key).toMatch(/^item-/)
43+
expect(result[1]._key).toMatch(/^item-/)
44+
expect(result[0]._key).not.toBe(result[1]._key)
45+
})
46+
47+
it('should preserve existing _key values', () => {
48+
const input = [{ name: 'Item 1', _key: 'existing-key' }, { name: 'Item 2' }]
49+
const result = ensureArrayKeys(input)
50+
51+
expect(result[0]._key).toBe('existing-key')
52+
expect(result[1]._key).toMatch(/^item-/)
53+
})
54+
55+
it('should use custom prefix', () => {
56+
const input = [{ name: 'Section 1' }]
57+
const result = ensureArrayKeys(input, 'section')
58+
59+
expect(result[0]._key).toMatch(/^section-/)
60+
})
61+
})
62+
63+
describe('createReference', () => {
64+
it('should create a reference object', () => {
65+
const ref = createReference('doc-id-123')
66+
67+
expect(ref).toEqual({
68+
_type: 'reference',
69+
_ref: 'doc-id-123',
70+
})
71+
})
72+
})
73+
74+
describe('createReferenceWithKey', () => {
75+
it('should create a reference with _key', () => {
76+
const ref = createReferenceWithKey('doc-id-123')
77+
78+
expect(ref._type).toBe('reference')
79+
expect(ref._ref).toBe('doc-id-123')
80+
expect(ref._key).toMatch(/^ref-/)
81+
})
82+
83+
it('should use custom prefix for _key', () => {
84+
const ref = createReferenceWithKey('doc-id-123', 'speaker')
85+
86+
expect(ref._type).toBe('reference')
87+
expect(ref._ref).toBe('doc-id-123')
88+
expect(ref._key).toMatch(/^speaker-/)
89+
})
90+
91+
it('should generate unique keys for different references', () => {
92+
const ref1 = createReferenceWithKey('doc-1')
93+
const ref2 = createReferenceWithKey('doc-2')
94+
95+
expect(ref1._key).not.toBe(ref2._key)
96+
})
97+
})

src/__mocks__/sponsor-data.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
ContractReadiness,
1616
MissingField,
1717
} from '@/lib/sponsor-crm/contract-readiness'
18+
import { nanoid } from 'nanoid'
1819

1920
/**
2021
* Mock data factories for sponsor-related components in Storybook
@@ -24,7 +25,7 @@ export function mockContactPerson(
2425
overrides: Partial<ContactPerson> = {},
2526
): ContactPerson {
2627
return {
27-
_key: `contact-${Math.random().toString(36).substr(2, 9)}`,
28+
_key: nanoid(),
2829
name: 'Jane Smith',
2930
email: 'jane.smith@example.com',
3031
phone: '+47 12 34 56 78',

src/components/admin/TicketPageContentEditor.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
TicketFaq,
1818
ConferenceVanityMetric,
1919
} from '@/lib/conference/types'
20+
import { generateKey } from '@/lib/sanity/helpers'
2021

2122
const ICON_OPTIONS = [
2223
{ value: 'MicrophoneIcon', label: 'Microphone' },
@@ -41,10 +42,6 @@ const ICON_OPTIONS = [
4142
{ value: 'CheckBadgeIcon', label: 'Check Badge' },
4243
]
4344

44-
function generateKey(): string {
45-
return Math.random().toString(36).substring(2, 10)
46-
}
47-
4845
interface TicketPageContentEditorProps {
4946
conferenceId: string
5047
conferenceTitle: string

src/components/admin/sponsor/SponsorContactEditor.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@heroicons/react/24/outline'
1212
import { StarIcon as StarIconSolid } from '@heroicons/react/24/solid'
1313
import { CONTACT_ROLE_OPTIONS } from '@/lib/sponsor/types'
14+
import { nanoid } from 'nanoid'
1415

1516
const meta = {
1617
title: 'Systems/Sponsors/Admin/Contacts/SponsorContactEditor',
@@ -68,7 +69,7 @@ function ContactEditorDemo() {
6869
setContacts([
6970
...contacts,
7071
{
71-
_key: String(Date.now()),
72+
_key: nanoid(),
7273
name: '',
7374
email: '',
7475
phone: '',

src/components/sponsor/SponsorPortal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { SponsorContactRoleSelect } from '@/components/admin/sponsor/SponsorContactRoleSelect'
1616
import { SponsorRegistrationLogoUpload } from '@/components/sponsor/SponsorRegistrationLogoUpload'
1717
import { formatNumber } from '@/lib/format'
18+
import { nanoid } from 'nanoid'
1819

1920
interface ContactPersonForm {
2021
name: string
@@ -240,8 +241,8 @@ export function SponsorPortal({ token }: { token: string }) {
240241

241242
const validContacts = contacts
242243
.filter((c) => c.name.trim() && c.email.trim())
243-
.map((c, index) => ({
244-
_key: `contact-${Date.now()}-${index}`,
244+
.map((c) => ({
245+
_key: nanoid(),
245246
name: c.name.trim(),
246247
email: c.email.trim(),
247248
phone: c.phone.trim() || undefined,

src/lib/sanity/helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Reference } from 'sanity'
2+
import { nanoid } from 'nanoid'
23

34
export function generateKey(prefix: string = 'item'): string {
4-
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
5+
return `${prefix}-${nanoid()}`
56
}
67

78
export function ensureArrayKeys<T extends Record<string, unknown>>(

src/lib/sponsor-crm/contract-templates.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
clientReadUncached as clientRead,
44
} from '@/lib/sanity/client'
55
import type { PortableTextBlock } from '@/lib/sponsor/types'
6+
import { nanoid } from 'nanoid'
67

78
export interface ContractSection {
89
_key: string
@@ -114,7 +115,6 @@ export async function createContractTemplate(
114115
data: ContractTemplateInput,
115116
): Promise<{ template?: ContractTemplate; error?: Error }> {
116117
try {
117-
let keyCounter = 0
118118
const doc = {
119119
_type: 'contractTemplate',
120120
title: data.title,
@@ -123,7 +123,7 @@ export async function createContractTemplate(
123123
language: data.language,
124124
currency: data.currency,
125125
sections: data.sections.map((s) => ({
126-
_key: `section-${++keyCounter}`,
126+
_key: nanoid(),
127127
heading: s.heading,
128128
body: s.body,
129129
})),
@@ -170,8 +170,8 @@ export async function updateContractTemplate(
170170
if (data.language !== undefined) updates.language = data.language
171171
if (data.currency !== undefined) updates.currency = data.currency
172172
if (data.sections !== undefined) {
173-
updates.sections = data.sections.map((s, index) => ({
174-
_key: s._key || `section-${Date.now()}-${index}`,
173+
updates.sections = data.sections.map((s) => ({
174+
_key: s._key || nanoid(),
175175
heading: s.heading,
176176
body: s.body,
177177
}))

0 commit comments

Comments
 (0)