@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.
- package/CHANGELOG.md +435 -0
- package/README.md +847 -0
- package/dist/index.d.ts +8749 -0
- package/dist/index.js +11167 -0
- package/dist/index.js.map +1 -0
- package/package.json +91 -0
- package/src/BaserowClient.ts +501 -0
- package/src/ClientWithCreds.ts +545 -0
- package/src/ClientWithCredsWs.ts +852 -0
- package/src/ClientWithToken.ts +171 -0
- package/src/contexts/DatabaseClientContext.ts +114 -0
- package/src/contexts/DatabaseContext.ts +870 -0
- package/src/contexts/DatabaseTokenContext.ts +331 -0
- package/src/contexts/FieldContext.ts +399 -0
- package/src/contexts/RowContext.ts +99 -0
- package/src/contexts/TableClientContext.ts +291 -0
- package/src/contexts/TableContext.ts +1247 -0
- package/src/contexts/TableOnlyContext.ts +74 -0
- package/src/contexts/WorkspaceContext.ts +490 -0
- package/src/express/errors.ts +260 -0
- package/src/express/index.ts +69 -0
- package/src/express/middleware.ts +225 -0
- package/src/express/serializers.ts +314 -0
- package/src/index.ts +247 -0
- package/src/presets/performance.ts +262 -0
- package/src/services/AuthService.ts +472 -0
- package/src/services/DatabaseService.ts +246 -0
- package/src/services/DatabaseTokenService.ts +186 -0
- package/src/services/FieldService.ts +1543 -0
- package/src/services/RowService.ts +982 -0
- package/src/services/SchemaControlService.ts +420 -0
- package/src/services/TableService.ts +781 -0
- package/src/services/WorkspaceService.ts +113 -0
- package/src/services/core/BaseAuthClient.ts +111 -0
- package/src/services/core/BaseClient.ts +107 -0
- package/src/services/core/BaseService.ts +71 -0
- package/src/services/core/HttpService.ts +115 -0
- package/src/services/core/ValidationService.ts +149 -0
- package/src/types/auth.ts +177 -0
- package/src/types/core.ts +91 -0
- package/src/types/errors.ts +105 -0
- package/src/types/fields.ts +456 -0
- package/src/types/index.ts +222 -0
- package/src/types/requests.ts +333 -0
- package/src/types/responses.ts +50 -0
- package/src/types/schema.ts +446 -0
- package/src/types/tokens.ts +36 -0
- package/src/types.ts +11 -0
- package/src/utils/auth.ts +174 -0
- package/src/utils/axios.ts +647 -0
- package/src/utils/field-cache.ts +164 -0
- package/src/utils/httpFactory.ts +66 -0
- package/src/utils/jwt-decoder.ts +188 -0
- package/src/utils/jwtTokens.ts +50 -0
- package/src/utils/performance.ts +105 -0
- package/src/utils/prisma-mapper.ts +961 -0
- package/src/utils/validation.ts +463 -0
- 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
|
+
}
|