File size: 5,123 Bytes
96c8713
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
type Fetch = typeof fetch

/**
 * Resolves the fetch implementation to use
 * Uses custom fetch if provided, otherwise uses native fetch
 *
 * @param customFetch - Optional custom fetch implementation
 * @returns Resolved fetch function
 */
export const resolveFetch = (customFetch?: Fetch): Fetch => {
  if (customFetch) {
    return (...args) => customFetch(...args)
  }
  return (...args) => fetch(...args)
}

/**
 * Resolves the Response constructor to use
 * Returns native Response constructor
 *
 * @returns Response constructor
 */
export const resolveResponse = (): typeof Response => {
  return Response
}

/**
 * Determine if input is a plain object
 * An object is plain if it's created by either {}, new Object(), or Object.create(null)
 *
 * @param value - Value to check
 * @returns True if value is a plain object
 * @source https://github.com/sindresorhus/is-plain-obj
 */
export const isPlainObject = (value: object): boolean => {
  if (typeof value !== 'object' || value === null) {
    return false
  }

  const prototype = Object.getPrototypeOf(value)
  return (
    (prototype === null ||
      prototype === Object.prototype ||
      Object.getPrototypeOf(prototype) === null) &&
    !(Symbol.toStringTag in value) &&
    !(Symbol.iterator in value)
  )
}

/**
 * Recursively converts object keys from snake_case to camelCase
 * Used for normalizing API responses
 *
 * @param item - Object to convert
 * @returns Converted object with camelCase keys
 */
export const recursiveToCamel = (item: Record<string, any>): unknown => {
  if (Array.isArray(item)) {
    return item.map((el) => recursiveToCamel(el))
  } else if (typeof item === 'function' || item !== Object(item)) {
    return item
  }

  const result: Record<string, any> = {}
  Object.entries(item).forEach(([key, value]) => {
    const newKey = key.replace(/([-_][a-z])/gi, (c) => c.toUpperCase().replace(/[-_]/g, ''))
    result[newKey] = recursiveToCamel(value)
  })

  return result
}

/**
 * Validates if a given bucket name is valid according to Supabase Storage API rules
 * Mirrors backend validation from: storage/src/storage/limits.ts:isValidBucketName()
 *
 * Rules:
 * - Length: 1-100 characters
 * - Allowed characters: alphanumeric (a-z, A-Z, 0-9), underscore (_), and safe special characters
 * - Safe special characters: ! - . * ' ( ) space & $ @ = ; : + , ?
 * - Forbidden: path separators (/, \), path traversal (..), leading/trailing whitespace
 *
 * AWS S3 Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html
 *
 * @param bucketName - The bucket name to validate
 * @returns true if valid, false otherwise
 */
export const isValidBucketName = (bucketName: string): boolean => {
  if (!bucketName || typeof bucketName !== 'string') {
    return false
  }

  // Check length constraints (1-100 characters)
  if (bucketName.length === 0 || bucketName.length > 100) {
    return false
  }

  // Check for leading/trailing whitespace
  if (bucketName.trim() !== bucketName) {
    return false
  }

  // Explicitly reject path separators (security)
  // Note: Consecutive periods (..) are allowed by backend - the AWS restriction
  // on relative paths applies to object keys, not bucket names
  if (bucketName.includes('/') || bucketName.includes('\\')) {
    return false
  }

  // Validate against allowed character set
  // Pattern matches backend regex: /^(\w|!|-|\.|\*|'|\(|\)| |&|\$|@|=|;|:|\+|,|\?)*$/
  // This explicitly excludes path separators (/, \) and other problematic characters
  const bucketNameRegex = /^[\w!.\*'() &$@=;:+,?-]+$/
  return bucketNameRegex.test(bucketName)
}

/**
 * Normalizes a number array to float32 format
 * Ensures all vector values are valid 32-bit floats
 *
 * @param values - Array of numbers to normalize
 * @returns Normalized float32 array
 */
export const normalizeToFloat32 = (values: number[]): number[] => {
  // Use Float32Array to ensure proper precision
  return Array.from(new Float32Array(values))
}

/**
 * Validates vector dimensions match expected dimension
 * Throws error if dimensions don't match
 *
 * @param vector - Vector data to validate
 * @param expectedDimension - Expected vector dimension
 * @throws Error if dimensions don't match
 */
export const validateVectorDimension = (
  vector: { float32: number[] },
  expectedDimension?: number
): void => {
  if (expectedDimension !== undefined && vector.float32.length !== expectedDimension) {
    throw new Error(
      `Vector dimension mismatch: expected ${expectedDimension}, got ${vector.float32.length}`
    )
  }
}

/**
 * Percent-encodes each segment of a storage path so URL delimiters within a
 * key (e.g. `?`, `#`) can't be interpreted as a querystring/fragment start.
 *
 * Splits on `/` so real path separators stay literal — the storage server
 * routes on them and decodes each segment back to the original key.
 *
 * @param path - A bucket id or `bucketId/objectKey` path
 * @returns The path with each `/`-delimited segment percent-encoded
 */
export const encodeStoragePath = (path: string): string =>
  path.split('/').map(encodeURIComponent).join('/')