@gzl10/baserow 1.2.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +435 -0
  2. package/README.md +847 -0
  3. package/dist/index.d.ts +8749 -0
  4. package/dist/index.js +11167 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +91 -0
  7. package/src/BaserowClient.ts +501 -0
  8. package/src/ClientWithCreds.ts +545 -0
  9. package/src/ClientWithCredsWs.ts +852 -0
  10. package/src/ClientWithToken.ts +171 -0
  11. package/src/contexts/DatabaseClientContext.ts +114 -0
  12. package/src/contexts/DatabaseContext.ts +870 -0
  13. package/src/contexts/DatabaseTokenContext.ts +331 -0
  14. package/src/contexts/FieldContext.ts +399 -0
  15. package/src/contexts/RowContext.ts +99 -0
  16. package/src/contexts/TableClientContext.ts +291 -0
  17. package/src/contexts/TableContext.ts +1247 -0
  18. package/src/contexts/TableOnlyContext.ts +74 -0
  19. package/src/contexts/WorkspaceContext.ts +490 -0
  20. package/src/express/errors.ts +260 -0
  21. package/src/express/index.ts +69 -0
  22. package/src/express/middleware.ts +225 -0
  23. package/src/express/serializers.ts +314 -0
  24. package/src/index.ts +247 -0
  25. package/src/presets/performance.ts +262 -0
  26. package/src/services/AuthService.ts +472 -0
  27. package/src/services/DatabaseService.ts +246 -0
  28. package/src/services/DatabaseTokenService.ts +186 -0
  29. package/src/services/FieldService.ts +1543 -0
  30. package/src/services/RowService.ts +982 -0
  31. package/src/services/SchemaControlService.ts +420 -0
  32. package/src/services/TableService.ts +781 -0
  33. package/src/services/WorkspaceService.ts +113 -0
  34. package/src/services/core/BaseAuthClient.ts +111 -0
  35. package/src/services/core/BaseClient.ts +107 -0
  36. package/src/services/core/BaseService.ts +71 -0
  37. package/src/services/core/HttpService.ts +115 -0
  38. package/src/services/core/ValidationService.ts +149 -0
  39. package/src/types/auth.ts +177 -0
  40. package/src/types/core.ts +91 -0
  41. package/src/types/errors.ts +105 -0
  42. package/src/types/fields.ts +456 -0
  43. package/src/types/index.ts +222 -0
  44. package/src/types/requests.ts +333 -0
  45. package/src/types/responses.ts +50 -0
  46. package/src/types/schema.ts +446 -0
  47. package/src/types/tokens.ts +36 -0
  48. package/src/types.ts +11 -0
  49. package/src/utils/auth.ts +174 -0
  50. package/src/utils/axios.ts +647 -0
  51. package/src/utils/field-cache.ts +164 -0
  52. package/src/utils/httpFactory.ts +66 -0
  53. package/src/utils/jwt-decoder.ts +188 -0
  54. package/src/utils/jwtTokens.ts +50 -0
  55. package/src/utils/performance.ts +105 -0
  56. package/src/utils/prisma-mapper.ts +961 -0
  57. package/src/utils/validation.ts +463 -0
  58. package/src/validators/schema.ts +419 -0
@@ -0,0 +1,113 @@
1
+ import { HttpService } from './core/HttpService'
2
+ import { Workspace, BaserowResponse, BaserowNotFoundError, BaserowError } from '../types'
3
+ import { validateRequired, validatePositiveNumber, validateString } from '../utils/validation'
4
+
5
+ export interface CreateWorkspaceRequest {
6
+ name: string
7
+ }
8
+
9
+ export interface UpdateWorkspaceRequest {
10
+ name?: string
11
+ }
12
+
13
+ export class WorkspaceService extends HttpService {
14
+ // ===== PUBLIC API (Prisma-style) =====
15
+
16
+ /**
17
+ * Listar todos los workspaces del usuario
18
+ */
19
+ async findMany(): Promise<Workspace[]> {
20
+ const response = await this.http.get<BaserowResponse<Workspace> | Workspace[]>('/workspaces/')
21
+ return Array.isArray(response) ? response : response.results
22
+ }
23
+
24
+ /**
25
+ * Buscar workspace por ID o nombre
26
+ */
27
+ async findUnique(identifier: string | number): Promise<Workspace | null> {
28
+ if (typeof identifier === 'number') {
29
+ // Buscar por ID
30
+ const workspaces = await this.findMany()
31
+ return workspaces.find(ws => ws.id === identifier) || null
32
+ } else {
33
+ // Buscar por nombre
34
+ validateRequired(identifier, 'workspace identifier')
35
+ const workspaces = await this.findMany()
36
+ return workspaces.find(ws => ws.name === identifier) || null
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Crear workspace - API pública
42
+ */
43
+ async create(data: CreateWorkspaceRequest): Promise<Workspace> {
44
+ return this.createWorkspaceInternal(data)
45
+ }
46
+
47
+ // ===== INTERNAL METHODS (Private) =====
48
+
49
+ /**
50
+ * Crear nuevo workspace (método interno)
51
+ * @private - Solo para uso interno del servicio
52
+ */
53
+ private async createWorkspaceInternal(data: CreateWorkspaceRequest): Promise<Workspace> {
54
+ validateString(data.name, 'name')
55
+
56
+ return await this.http.post<Workspace>('/workspaces/', {
57
+ name: data.name
58
+ })
59
+ }
60
+
61
+ /**
62
+ * Actualizar workspace (método interno)
63
+ * @private - Solo para uso por WorkspaceContext
64
+ */
65
+ private async updateWorkspaceInternal(workspaceId: number, data: UpdateWorkspaceRequest): Promise<Workspace> {
66
+ validatePositiveNumber(workspaceId, 'workspaceId')
67
+
68
+ if (data.name !== undefined) {
69
+ validateString(data.name, 'name')
70
+ }
71
+
72
+ try {
73
+ return await this.http.patch<Workspace>(`/workspaces/${workspaceId}/`, data)
74
+ } catch (error) {
75
+ if (error instanceof BaserowError && error.status === 404) {
76
+ throw new BaserowNotFoundError('Workspace', workspaceId)
77
+ }
78
+ throw error
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Eliminar workspace (método interno)
84
+ * @private - Solo para uso por WorkspaceContext
85
+ */
86
+ private async deleteWorkspaceInternal(workspaceId: number): Promise<void> {
87
+ validatePositiveNumber(workspaceId, 'workspaceId')
88
+
89
+ try {
90
+ await this.http.delete(`/workspaces/${workspaceId}/`)
91
+ } catch (error) {
92
+ if (error instanceof BaserowError && error.status === 404) {
93
+ throw new BaserowNotFoundError('Workspace', workspaceId)
94
+ }
95
+ throw error
96
+ }
97
+ }
98
+
99
+ // ===== FRIEND ACCESS PATTERN FOR WORKSPACE CONTEXT =====
100
+ // Symbol-based access que no aparece en la API pública pero permite acceso interno
101
+
102
+ /**
103
+ * Friend access para WorkspaceContext
104
+ * No aparece en intellisense normal ni en la API pública
105
+ * @internal
106
+ */
107
+ get [Symbol.for('workspaceContext')]() {
108
+ return {
109
+ updateWorkspace: this.updateWorkspaceInternal.bind(this),
110
+ deleteWorkspace: this.deleteWorkspaceInternal.bind(this)
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Clase base abstracta para clientes con autenticación JWT
3
+ * Extiende BaseClient agregando funcionalidades de autenticación
4
+ */
5
+
6
+ import { BaseClient } from './BaseClient'
7
+ import { HttpClient } from '../../utils/axios'
8
+ import { Logger } from '../../types/index'
9
+ import { AuthService } from '../AuthService'
10
+
11
+ export abstract class BaseAuthClient<TConfig = any, TExcludeKeys extends keyof TConfig = never> extends BaseClient<
12
+ TConfig,
13
+ TExcludeKeys
14
+ > {
15
+ protected auth: AuthService
16
+
17
+ constructor(config: TConfig, http: HttpClient, auth: AuthService, logger?: Logger) {
18
+ super(config, http, logger)
19
+ this.auth = auth
20
+ }
21
+
22
+ /**
23
+ * Verificar si el cliente está autenticado
24
+ *
25
+ * Comprueba si hay un token JWT válido y no expirado.
26
+ * Si el token está próximo a expirar, el auto-refresh se encargará de renovarlo.
27
+ *
28
+ * @returns `true` si está autenticado con token válido, `false` en caso contrario
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * if (admin.isAuthenticated()) {
33
+ * console.log('Cliente autenticado correctamente')
34
+ * // Proceder con operaciones que requieren autenticación
35
+ * } else {
36
+ * console.log('Cliente no autenticado')
37
+ * }
38
+ * ```
39
+ *
40
+ * @since 1.0.0
41
+ */
42
+ isAuthenticated(): boolean {
43
+ return this.auth.isAuthenticated()
44
+ }
45
+
46
+ /**
47
+ * Obtener el token JWT actual
48
+ *
49
+ * Retorna el token de acceso actual si está disponible.
50
+ * Útil para debugging o para usar el token en otras APIs.
51
+ *
52
+ * @returns Token JWT actual o undefined si no está autenticado
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const token = admin.getCurrentToken()
57
+ * if (token) {
58
+ * console.log('Token actual:', token.substring(0, 10) + '...')
59
+ * }
60
+ * ```
61
+ *
62
+ * @since 1.0.0
63
+ */
64
+ getCurrentToken(): string | undefined {
65
+ return this.auth.getCurrentToken()
66
+ }
67
+
68
+ /**
69
+ * Refrescar el token JWT manualmente
70
+ *
71
+ * Normalmente el auto-refresh maneja esto automáticamente, pero este método
72
+ * permite forzar un refresh del token antes de que expire.
73
+ *
74
+ * @returns Promise que resuelve al nuevo token de acceso
75
+ * @throws {BaserowAuthError} Si el refresh falla (refresh token expirado/inválido)
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * try {
80
+ * const newToken = await admin.refreshToken()
81
+ * console.log('Token refrescado exitosamente')
82
+ * } catch (error) {
83
+ * console.error('Error refrescando token:', error.message)
84
+ * // Re-autenticar usuario
85
+ * }
86
+ * ```
87
+ *
88
+ * @since 1.0.0
89
+ */
90
+ async refreshToken(): Promise<string> {
91
+ return await this.auth.refreshAccessToken()
92
+ }
93
+
94
+ /**
95
+ * Cerrar sesión y limpiar tokens
96
+ *
97
+ * Invalida el token JWT actual y limpia toda la información de autenticación.
98
+ * Después de logout() el cliente no podrá realizar operaciones autenticadas.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * admin.logout()
103
+ * console.log(admin.isAuthenticated()) // false
104
+ * ```
105
+ *
106
+ * @since 1.0.0
107
+ */
108
+ logout(): void {
109
+ this.auth.logout()
110
+ }
111
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Clase base abstracta para todos los clientes de Baserow
3
+ * Proporciona funcionalidades comunes como getConfig() unificado
4
+ */
5
+
6
+ import { BaseService } from './BaseService'
7
+ import { HttpClient } from '../../utils/axios'
8
+ import { Logger } from '../../types/index'
9
+
10
+ export abstract class BaseClient<TConfig = any, TExcludeKeys extends keyof TConfig = never> extends BaseService {
11
+ protected config: TConfig
12
+
13
+ constructor(config: TConfig, http: HttpClient, logger?: Logger) {
14
+ super(http, logger)
15
+ this.config = config
16
+ }
17
+
18
+ /**
19
+ * Obtener configuración del cliente sin campos sensibles
20
+ *
21
+ * Implementación unificada que excluye automáticamente campos sensibles
22
+ * como credenciales, manteniendo consistencia entre todos los clientes.
23
+ *
24
+ * @param excludeKeys - Campos adicionales a excluir de la configuración
25
+ * @returns Configuración del cliente sin campos sensibles
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const config = client.getConfig()
30
+ * console.log('URL:', config.url)
31
+ * console.log('Performance:', config.performance)
32
+ * // credentials no aparece en el resultado
33
+ * ```
34
+ */
35
+ protected getConfigBase<K extends keyof TConfig = TExcludeKeys>(
36
+ excludeKeys: K[] = [] as K[]
37
+ ): Readonly<Omit<TConfig, K | TExcludeKeys>> {
38
+ const result = { ...this.config } as any
39
+
40
+ // Excluir campos predefinidos para este tipo de cliente
41
+ const defaultExcludes = this.getDefaultExcludeKeys()
42
+ const allExcludes = [...defaultExcludes, ...excludeKeys]
43
+
44
+ allExcludes.forEach(key => delete result[key])
45
+
46
+ return result
47
+ }
48
+
49
+ /**
50
+ * Campos que deben excluirse por defecto para este tipo de cliente
51
+ * Subclases pueden sobrescribir este método para definir exclusiones específicas
52
+ */
53
+ protected abstract getDefaultExcludeKeys(): string[]
54
+
55
+ /**
56
+ * Verificar si el servidor Baserow está disponible (health check)
57
+ *
58
+ * Realiza una verificación de conectividad básica al servidor Baserow
59
+ * sin requerir autenticación. Útil para verificar disponibilidad del servicio.
60
+ *
61
+ * @returns Promise que resuelve a true si el servidor está disponible
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const isHealthy = await client.health()
66
+ * if (!isHealthy) {
67
+ * console.log('Servidor Baserow no disponible')
68
+ * }
69
+ * ```
70
+ *
71
+ * @since 1.0.0
72
+ */
73
+ async health(): Promise<boolean> {
74
+ try {
75
+ // Usar endpoint de salud oficial (no requiere autenticación)
76
+ const healthUrl = `${(this.config as any).url.replace(/\/$/, '')}/api/_health/`
77
+ const response = await fetch(healthUrl, {
78
+ method: 'GET',
79
+ headers: { 'Content-Type': 'application/json' }
80
+ })
81
+
82
+ return response.ok
83
+ } catch (error) {
84
+ this.logError('Health check failed', error)
85
+ return false
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Destruir cliente y liberar recursos
91
+ *
92
+ * Limpia todas las conexiones HTTP, timers y recursos del cliente.
93
+ * Debe llamarse cuando el cliente ya no se va a usar para evitar memory leaks.
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * // Al final de la aplicación o cuando ya no se necesite el cliente
98
+ * client.destroy()
99
+ * ```
100
+ *
101
+ * @since 1.0.0
102
+ */
103
+ destroy(): void {
104
+ this.logDebug(`Destroying ${this.constructor.name}`)
105
+ this.http.destroy()
106
+ }
107
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Clase base para todos los servicios de Baserow
3
+ * Proporciona funcionalidades comunes como HTTP client, logging y validación
4
+ */
5
+
6
+ import { HttpClient } from '../../utils/axios'
7
+ import { Logger } from '../../types/index'
8
+
9
+ export abstract class BaseService {
10
+ protected logger?: Logger
11
+
12
+ constructor(
13
+ protected http: HttpClient,
14
+ logger?: Logger
15
+ ) {
16
+ this.logger = logger
17
+ }
18
+
19
+ /**
20
+ * Log de información
21
+ */
22
+ protected logInfo(message: string, ...params: any[]): void {
23
+ this.logger?.info?.(`[${this.constructor.name}] ${message}`, ...params)
24
+ }
25
+
26
+ /**
27
+ * Log de warnings
28
+ */
29
+ protected logWarn(message: string, ...params: any[]): void {
30
+ this.logger?.warn?.(`[${this.constructor.name}] ${message}`, ...params)
31
+ }
32
+
33
+ /**
34
+ * Log de errores
35
+ */
36
+ protected logError(message: string, error?: any, ...params: any[]): void {
37
+ this.logger?.error?.(`[${this.constructor.name}] ${message}`, error, ...params)
38
+ }
39
+
40
+ /**
41
+ * Log de debug
42
+ */
43
+ protected logDebug(message: string, ...params: any[]): void {
44
+ this.logger?.debug?.(`[${this.constructor.name}] ${message}`, ...params)
45
+ }
46
+
47
+ /**
48
+ * Método helper para construir URLs de endpoints
49
+ */
50
+ protected buildEndpoint(...segments: (string | number)[]): string {
51
+ const cleanedSegments = segments.map(segment => String(segment).replace(/^\/|\/$/g, ''))
52
+ return cleanedSegments.join('/') + '/'
53
+ }
54
+
55
+ /**
56
+ * Método helper para manejar errores HTTP de forma consistente
57
+ */
58
+ protected handleHttpError(error: any, operation: string, resourceId?: number | string): never {
59
+ const context = resourceId ? `${operation} for resource ${resourceId}` : operation
60
+ this.logError(`HTTP error during ${context}`, error)
61
+ throw error
62
+ }
63
+
64
+ /**
65
+ * Log de operación exitosa
66
+ */
67
+ protected logSuccess(operation: string, resourceId?: number | string, details?: any): void {
68
+ const context = resourceId ? `${operation} for resource ${resourceId}` : operation
69
+ this.logInfo(`✅ ${context} completed successfully`, details)
70
+ }
71
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Servicio HTTP especializado para operaciones CRUD comunes
3
+ * Elimina duplicación de código HTTP entre servicios
4
+ */
5
+
6
+ import { BaseService } from './BaseService'
7
+ import { BaserowResponse, BaserowNotFoundError } from '../../types/index'
8
+
9
+ export abstract class HttpService extends BaseService {
10
+ /**
11
+ * GET genérico para listar recursos
12
+ */
13
+ protected async getList<T>(endpoint: string, params?: Record<string, any>): Promise<T[]> {
14
+ try {
15
+ this.logDebug(`Fetching list from ${endpoint}`, params)
16
+ const response = await this.http.get<BaserowResponse<T>>(endpoint, { params })
17
+ this.logSuccess(`list operation`, undefined, { count: response.count })
18
+ return response.results
19
+ } catch (error) {
20
+ this.handleHttpError(error, 'list operation')
21
+ }
22
+ }
23
+
24
+ /**
25
+ * GET genérico para obtener un recurso por ID
26
+ */
27
+ protected async getById<T>(endpoint: string, id: number): Promise<T> {
28
+ try {
29
+ this.logDebug(`Fetching resource ${id} from ${endpoint}`)
30
+ const response = await this.http.get<T>(this.buildEndpoint(endpoint, id))
31
+ this.logSuccess('get operation', id)
32
+ return response
33
+ } catch (error) {
34
+ if ((error as any).status === 404) {
35
+ const resourceName = endpoint.split('/').pop() || 'resource'
36
+ throw new BaserowNotFoundError(resourceName, id)
37
+ }
38
+ this.handleHttpError(error, 'get operation', id)
39
+ }
40
+ }
41
+
42
+ /**
43
+ * POST genérico para crear recursos
44
+ */
45
+ protected async createResource<T, C = any>(endpoint: string, data: C): Promise<T> {
46
+ try {
47
+ this.logDebug(`Creating resource at ${endpoint}`, data)
48
+ const response = await this.http.post<T>(endpoint, data)
49
+ this.logSuccess('create operation', (response as any).id || 'new')
50
+ return response
51
+ } catch (error) {
52
+ this.handleHttpError(error, 'create operation')
53
+ }
54
+ }
55
+
56
+ /**
57
+ * PATCH genérico para actualizar recursos
58
+ */
59
+ protected async updateResource<T, U = any>(endpoint: string, id: number, data: U): Promise<T> {
60
+ try {
61
+ this.logDebug(`Updating resource ${id} at ${endpoint}`, data)
62
+ const response = await this.http.patch<T>(this.buildEndpoint(endpoint, id), data)
63
+ this.logSuccess('update operation', id)
64
+ return response
65
+ } catch (error) {
66
+ if ((error as any).status === 404) {
67
+ const resourceName = endpoint.split('/').pop() || 'resource'
68
+ throw new BaserowNotFoundError(resourceName, id)
69
+ }
70
+ this.handleHttpError(error, 'update operation', id)
71
+ }
72
+ }
73
+
74
+ /**
75
+ * DELETE genérico para eliminar recursos
76
+ */
77
+ protected async deleteById(endpoint: string, id: number): Promise<void> {
78
+ try {
79
+ this.logDebug(`Deleting resource ${id} from ${endpoint}`)
80
+ await this.http.delete(this.buildEndpoint(endpoint, id))
81
+ this.logSuccess('delete operation', id)
82
+ } catch (error) {
83
+ if ((error as any).status === 404) {
84
+ const resourceName = endpoint.split('/').pop() || 'resource'
85
+ throw new BaserowNotFoundError(resourceName, id)
86
+ }
87
+ this.handleHttpError(error, 'delete operation', id)
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Método helper para buscar recursos por nombre
93
+ */
94
+ protected async findResourceByName<T extends { id: number; name: string }>(
95
+ endpoint: string,
96
+ name: string,
97
+ listParams?: Record<string, any>
98
+ ): Promise<T | null> {
99
+ try {
100
+ this.logDebug(`Searching for resource with name "${name}" in ${endpoint}`)
101
+ const items = await this.getList<T>(endpoint, listParams)
102
+ const found = items.find(item => item.name === name) || null
103
+
104
+ if (found) {
105
+ this.logSuccess(`find by name "${name}"`, found.id)
106
+ } else {
107
+ this.logDebug(`No resource found with name "${name}"`)
108
+ }
109
+
110
+ return found
111
+ } catch (error) {
112
+ this.handleHttpError(error, `find by name "${name}"`)
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Servicio de validación centralizado
3
+ * Elimina duplicación de validaciones entre servicios
4
+ */
5
+
6
+ import { BaseService } from './BaseService'
7
+ import { BaserowConfigError, BaserowValidationError } from '../../types/index'
8
+ import { validateRequired, validatePositiveNumber, validateString, validateUrl } from '../../utils/validation'
9
+
10
+ export class ValidationService extends BaseService {
11
+ /**
12
+ * Valida configuración básica de Baserow
13
+ */
14
+ validateBaserowConfig(url: string, token?: string): void {
15
+ try {
16
+ validateRequired(url, 'url')
17
+ validateUrl(url, 'url')
18
+
19
+ if (token) {
20
+ validateRequired(token, 'token')
21
+ validateString(token, 'token')
22
+ }
23
+ } catch (error) {
24
+ throw new BaserowConfigError(`Invalid configuration: ${(error as Error).message}`)
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Valida credenciales de usuario
30
+ */
31
+ validateCredentials(email: string, password: string): void {
32
+ try {
33
+ validateRequired(email, 'email')
34
+ validateString(email, 'email')
35
+ validateRequired(password, 'password')
36
+ validateString(password, 'password')
37
+
38
+ // Validación básica de email
39
+ if (!email.includes('@')) {
40
+ throw new Error('Invalid email format')
41
+ }
42
+ } catch (error) {
43
+ throw new BaserowValidationError(`Invalid credentials: ${(error as Error).message}`, {
44
+ email: email ? [] : ['Email is required'],
45
+ password: password ? [] : ['Password is required']
46
+ })
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Valida IDs numéricos
52
+ */
53
+ validateId(id: number, fieldName: string): void {
54
+ try {
55
+ validateRequired(id, fieldName)
56
+ validatePositiveNumber(id, fieldName)
57
+ } catch (error) {
58
+ throw new BaserowValidationError(`Invalid ${fieldName}: ${(error as Error).message}`, {
59
+ [fieldName]: [(error as Error).message]
60
+ })
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Valida nombres de recursos
66
+ */
67
+ validateResourceName(name: string, resourceType: string): void {
68
+ try {
69
+ validateRequired(name, 'name')
70
+ validateString(name, 'name')
71
+
72
+ if (name.trim() !== name) {
73
+ throw new Error('Name cannot start or end with whitespace')
74
+ }
75
+
76
+ if (name.length > 255) {
77
+ throw new Error('Name cannot exceed 255 characters')
78
+ }
79
+ } catch (error) {
80
+ throw new BaserowValidationError(`Invalid ${resourceType} name: ${(error as Error).message}`, {
81
+ name: [(error as Error).message]
82
+ })
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Valida datos de tabla de creación
88
+ */
89
+ validateTableData(data: Array<Array<string>>): void {
90
+ if (!Array.isArray(data)) {
91
+ throw new BaserowValidationError('Table data must be an array', {
92
+ data: ['Must be an array of arrays']
93
+ })
94
+ }
95
+
96
+ if (data.length === 0) {
97
+ throw new BaserowValidationError('Table data cannot be empty', {
98
+ data: ['At least one row is required']
99
+ })
100
+ }
101
+
102
+ const firstRowLength = data[0]?.length || 0
103
+ if (firstRowLength === 0) {
104
+ throw new BaserowValidationError('First row cannot be empty', {
105
+ data: ['First row must have at least one column']
106
+ })
107
+ }
108
+
109
+ // Verificar que todas las filas tengan la misma longitud
110
+ for (let i = 0; i < data.length; i++) {
111
+ if (!Array.isArray(data[i])) {
112
+ throw new BaserowValidationError(`Row ${i} must be an array`, {
113
+ data: [`Row ${i} is not an array`]
114
+ })
115
+ }
116
+
117
+ if (data[i].length !== firstRowLength) {
118
+ throw new BaserowValidationError(`All rows must have the same number of columns`, {
119
+ data: [`Row ${i} has ${data[i].length} columns, expected ${firstRowLength}`]
120
+ })
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Combina múltiples validaciones en una sola llamada
127
+ */
128
+ validateMultiple(validations: Array<() => void>): void {
129
+ const errors: string[] = []
130
+
131
+ for (const validation of validations) {
132
+ try {
133
+ validation()
134
+ } catch (error) {
135
+ if (error instanceof BaserowValidationError) {
136
+ errors.push(error.message)
137
+ } else {
138
+ errors.push((error as Error).message || 'Unknown validation error')
139
+ }
140
+ }
141
+ }
142
+
143
+ if (errors.length > 0) {
144
+ throw new BaserowValidationError(`Multiple validation errors: ${errors.join(', ')}`, {
145
+ multiple: errors
146
+ })
147
+ }
148
+ }
149
+ }