@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,1543 @@
1
+ import { HttpService } from './core/HttpService'
2
+ import { ValidationService } from './core/ValidationService'
3
+ import {
4
+ Field,
5
+ CreateFieldRequest,
6
+ UpdateFieldRequest,
7
+ SelectOption,
8
+ NumberSeparator,
9
+ RatingStyle,
10
+ RatingColor,
11
+ FieldConstraint,
12
+ FieldAdvancedOptions,
13
+ ConstraintType,
14
+ BaserowResponse,
15
+ BaserowNotFoundError,
16
+ BaserowValidationError,
17
+ Logger
18
+ } from '../types/index'
19
+ import { sanitizeFieldName, validateFieldType } from '../utils/validation'
20
+
21
+ /**
22
+ * Servicio para operaciones CRUD de campos de Baserow
23
+ *
24
+ * Proporciona operaciones completas de campos incluyendo CRUD básico,
25
+ * métodos de creación fluidos para todos los tipos de campo soportados,
26
+ * y operaciones de gestión avanzadas como duplicación y reordenamiento.
27
+ *
28
+ * **Cobertura de Tipos de Campo (21/22 - 95%):**
29
+ * - **Texto (5)**: text, long_text, url, email, phone_number
30
+ * - **Numéricos (3)**: number, boolean, rating
31
+ * - **Fecha/Auditoría (5)**: date, last_modified, last_modified_by, created_on, created_by
32
+ * - **Selección (2)**: single_select, multiple_select
33
+ * - **Relacionales (2)**: link_row, formula
34
+ * - **Avanzados (5)**: file, autonumber, count, rollup, lookup
35
+ *
36
+ * **Características:**
37
+ * - API fluida para cada tipo de campo con parámetros específicos
38
+ * - Validación automática de tipos y configuraciones
39
+ * - Sanitización de nombres de campo
40
+ * - Operaciones avanzadas: duplicar, reordenar
41
+ * - Búsqueda de campos por nombre
42
+ * - **Soporte v1.35+**: Índices para performance y constraints para integridad
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * // Operaciones básicas
47
+ * const fields = await fieldService.findMany(tableId)
48
+ * const field = await fieldService.get(fieldId)
49
+ * const found = await fieldService.findUnique(tableId, 'email')
50
+ *
51
+ * // API fluida por tipo de campo (MANTENER - Sin cambios)
52
+ * const textField = await fieldService.createTextField(tableId, 'nombre', 'default')
53
+ * const numberField = await fieldService.createNumberField(tableId, 'precio', 2, true)
54
+ * const selectField = await fieldService.createSelectField(tableId, 'estado', [
55
+ * { value: 'active', color: 'blue' },
56
+ * { value: 'inactive', color: 'red' }
57
+ * ])
58
+ *
59
+ * // Campos avanzados (MANTENER - Sin cambios)
60
+ * const linkField = await fieldService.createLinkField(tableId, 'relacionado', targetTableId)
61
+ * const formulaField = await fieldService.createFormulaField(tableId, 'total', 'field("precio") * field("cantidad")')
62
+ *
63
+ * // Campos con índices y constraints (v1.35+)
64
+ * const emailField = await fieldService.createEmailField(tableId, 'email', undefined, {
65
+ * index: true,
66
+ * constraints: [{ type: 'unique_with_empty', active: true }]
67
+ * })
68
+ *
69
+ * // Gestión de índices y constraints
70
+ * await fieldService.setFieldIndex(fieldId, true)
71
+ * await fieldService.addFieldConstraint(fieldId, { type: 'unique', active: true })
72
+ * ```
73
+ *
74
+ * @since 1.0.0
75
+ */
76
+ export class FieldService extends HttpService {
77
+ private validationService: ValidationService
78
+
79
+ constructor(http: any, logger?: Logger) {
80
+ super(http, logger)
81
+ this.validationService = new ValidationService(http, logger)
82
+ }
83
+
84
+ // ===== PUBLIC API (Prisma-style) =====
85
+
86
+ /**
87
+ * Listar todos los campos de una tabla
88
+ *
89
+ * Obtiene todos los campos de una tabla específica con sus definiciones
90
+ * completas incluyendo tipo, configuración y metadatos.
91
+ *
92
+ * @param tableId - ID numérico de la tabla
93
+ * @returns Promise con array de campos de la tabla
94
+ *
95
+ * @throws {BaserowValidationError} Si el tableId es inválido
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const fields = await fieldService.findMany(123)
100
+ * fields.forEach(field => {
101
+ * console.log(`${field.name} (${field.type}): ${field.id}`)
102
+ * })
103
+ * ```
104
+ *
105
+ * @since 1.0.0
106
+ */
107
+ async findMany(tableId: number): Promise<Field[]> {
108
+ this.validationService.validateId(tableId, 'table ID')
109
+
110
+ try {
111
+ const response = await this.http.get<BaserowResponse<Field> | Field[]>(`/database/fields/table/${tableId}/`)
112
+
113
+ // La API puede devolver array directo o {results: []}
114
+ const fields = Array.isArray(response) ? response : response.results || []
115
+ this.logSuccess('find many fields', tableId, { count: fields.length })
116
+ return fields
117
+ } catch (error) {
118
+ this.handleHttpError(error, 'find many fields', tableId)
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Obtener campo por ID
124
+ *
125
+ * Recupera un campo específico por su ID con toda su configuración
126
+ * y metadatos. Útil para inspeccionar configuraciones detalladas.
127
+ *
128
+ * @param fieldId - ID numérico del campo
129
+ * @returns Promise con datos completos del campo
130
+ *
131
+ * @throws {BaserowNotFoundError} Si el campo no existe
132
+ * @throws {BaserowValidationError} Si el fieldId es inválido
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * const field = await fieldService.get(456)
137
+ * console.log(`Campo: ${field.name} - Tipo: ${field.type}`)
138
+ * ```
139
+ *
140
+ * @since 1.0.0
141
+ */
142
+ async get(fieldId: number): Promise<Field> {
143
+ this.validationService.validateId(fieldId, 'field ID')
144
+ return this.getById<Field>('/database/fields', fieldId)
145
+ }
146
+
147
+ /**
148
+ * Buscar campo por ID o nombre en una tabla
149
+ *
150
+ * Busca un campo específico por su ID o nombre dentro de una tabla.
151
+ * Útil para operaciones dinámicas donde se conoce el identificador pero no se sabe si es ID o nombre.
152
+ *
153
+ * @param tableId - ID numérico de la tabla
154
+ * @param identifier - ID numérico o nombre del campo a buscar
155
+ * @returns Promise con el campo encontrado o null si no existe
156
+ *
157
+ * @throws {BaserowValidationError} Si los parámetros son inválidos
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * const field = await fieldService.findUnique(123, 'email')
162
+ * if (field) {
163
+ * console.log(`Campo encontrado: ${field.id}`)
164
+ * } else {
165
+ * console.log('Campo no encontrado')
166
+ * }
167
+ * ```
168
+ *
169
+ * @since 1.0.0
170
+ */
171
+ async findUnique(tableId: number, identifier: string | number): Promise<Field | null> {
172
+ this.validationService.validateId(tableId, 'table ID')
173
+
174
+ if (typeof identifier === 'number') {
175
+ // Buscar por ID
176
+ try {
177
+ return await this.get(identifier)
178
+ } catch (error) {
179
+ if (error instanceof BaserowNotFoundError) {
180
+ return null
181
+ }
182
+ throw error
183
+ }
184
+ } else {
185
+ // Buscar por nombre
186
+ this.validationService.validateResourceName(identifier, 'field')
187
+ const fields = await this.findMany(tableId)
188
+ const found = fields.find(field => field.name === identifier) || null
189
+
190
+ if (found) {
191
+ this.logSuccess(`find field by name "${identifier}"`, found.id)
192
+ } else {
193
+ this.logDebug(`No field found with name "${identifier}" in table ${tableId}`)
194
+ }
195
+
196
+ return found
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Crear nuevo campo - API pública
202
+ *
203
+ * Crea un nuevo campo en la tabla especificada. Este es el método base
204
+ * que usan todos los métodos de creación específicos por tipo.
205
+ *
206
+ * @param tableId - ID numérico de la tabla
207
+ * @param data - Configuración completa del campo
208
+ * @returns Promise con el campo creado
209
+ *
210
+ * @throws {BaserowValidationError} Si los datos son inválidos
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * const field = await fieldService.create(123, {
215
+ * name: 'Mi Campo',
216
+ * type: 'text',
217
+ * text_default: 'Valor por defecto',
218
+ * description: 'Descripción del campo'
219
+ * })
220
+ * ```
221
+ *
222
+ * @since 1.0.0
223
+ */
224
+ async create(tableId: number, data: CreateFieldRequest): Promise<Field> {
225
+ return this.createFieldInternal(tableId, data)
226
+ }
227
+
228
+ // ===== PRIVATE METHODS =====
229
+
230
+ /**
231
+ * Crear nuevo campo (método interno)
232
+ * @private - Solo para uso interno del servicio
233
+ */
234
+ private async createFieldInternal(tableId: number, data: CreateFieldRequest): Promise<Field> {
235
+ this.validationService.validateId(tableId, 'table ID')
236
+ data.name = sanitizeFieldName(data.name)
237
+ this.validationService.validateResourceName(data.name, 'field')
238
+ validateFieldType(data.type, 'type')
239
+
240
+ const payload = {
241
+ table_id: tableId,
242
+ ...data
243
+ }
244
+
245
+ try {
246
+ this.logDebug(`Creating field "${data.name}" of type ${data.type} in table ${tableId}`, payload)
247
+ const response = await this.http.post<Field>(`/database/fields/table/${tableId}/`, payload)
248
+ this.logSuccess('create field', response.id, { name: data.name, type: data.type })
249
+ return response
250
+ } catch (error) {
251
+ this.handleHttpError(error, 'create field', tableId)
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Actualizar campo (método interno)
257
+ * @private - Solo para uso por FieldContext
258
+ */
259
+ private async updateFieldInternal(fieldId: number, data: UpdateFieldRequest): Promise<Field> {
260
+ this.validationService.validateId(fieldId, 'field ID')
261
+
262
+ if (data.name !== undefined) {
263
+ data.name = sanitizeFieldName(data.name)
264
+ this.validationService.validateResourceName(data.name, 'field')
265
+ }
266
+
267
+ try {
268
+ this.logDebug(`Updating field ${fieldId}`, data)
269
+ const response = await this.http.patch<Field>(`/database/fields/${fieldId}/`, data)
270
+ this.logSuccess('update field', fieldId)
271
+ return response
272
+ } catch (error) {
273
+ if ((error as any).status === 404) {
274
+ throw new BaserowNotFoundError('Field', fieldId)
275
+ }
276
+ this.handleHttpError(error, 'update field', fieldId)
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Eliminar campo (método interno)
282
+ * @private - Solo para uso por FieldContext
283
+ */
284
+ private async deleteFieldInternal(fieldId: number): Promise<void> {
285
+ this.validationService.validateId(fieldId, 'field ID')
286
+ return this.deleteById('/database/fields', fieldId)
287
+ }
288
+
289
+ /**
290
+ * Duplicar campo
291
+ *
292
+ * Crea una copia exacta de un campo existente con un nuevo nombre.
293
+ * Mantiene toda la configuración del campo original.
294
+ *
295
+ * @param fieldId - ID numérico del campo a duplicar
296
+ * @param newName - Nombre para el campo duplicado (opcional)
297
+ * @returns Promise con el campo duplicado
298
+ *
299
+ * @throws {BaserowValidationError} Si los parámetros son inválidos
300
+ *
301
+ * @example
302
+ * ```typescript
303
+ * const duplicated = await fieldService.duplicate(456, 'Copia del campo')
304
+ * console.log(`Campo duplicado: ${duplicated.id}`)
305
+ * ```
306
+ *
307
+ * @since 1.0.0
308
+ */
309
+ async duplicate(fieldId: number, newName?: string): Promise<Field> {
310
+ this.validationService.validateId(fieldId, 'field ID')
311
+
312
+ const originalField = await this.get(fieldId)
313
+ const name = newName ? sanitizeFieldName(newName) : `${originalField.name} (copy)`
314
+
315
+ if (newName) {
316
+ this.validationService.validateResourceName(name, 'field')
317
+ }
318
+
319
+ try {
320
+ this.logDebug(`Duplicating field ${fieldId} with name "${name}"`, { originalName: originalField.name })
321
+ const response = await this.http.post<Field>(`/database/fields/${fieldId}/duplicate/`, { name })
322
+ this.logSuccess('duplicate field', response.id, { originalId: fieldId, newName: name })
323
+ return response
324
+ } catch (error) {
325
+ this.handleHttpError(error, 'duplicate field', fieldId)
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Reordenar campos de una tabla
331
+ *
332
+ * Cambia el orden de visualización de los campos en una tabla.
333
+ * El orden se define por la secuencia de IDs proporcionada.
334
+ *
335
+ * @param tableId - ID numérico de la tabla
336
+ * @param fieldIds - Array de IDs de campos en el nuevo orden deseado
337
+ * @returns Promise que resuelve cuando el reordenamiento se completa
338
+ *
339
+ * @throws {BaserowValidationError} Si los parámetros son inválidos
340
+ *
341
+ * @example
342
+ * ```typescript
343
+ * // Reordenar campos: primero ID 3, luego 1, luego 2
344
+ * await fieldService.reorder(123, [3, 1, 2])
345
+ * console.log('Campos reordenados exitosamente')
346
+ * ```
347
+ *
348
+ * @since 1.0.0
349
+ */
350
+ async reorder(tableId: number, fieldIds: number[]): Promise<void> {
351
+ this.validationService.validateId(tableId, 'table ID')
352
+
353
+ if (!Array.isArray(fieldIds) || fieldIds.length === 0) {
354
+ throw new Error('fieldIds must be a non-empty array')
355
+ }
356
+
357
+ fieldIds.forEach(id => this.validationService.validateId(id, 'field ID'))
358
+
359
+ try {
360
+ this.logDebug(`Reordering ${fieldIds.length} fields in table ${tableId}`, { fieldIds })
361
+ await this.http.patch(`/database/tables/${tableId}/order-fields/`, {
362
+ field_ids: fieldIds
363
+ })
364
+ this.logSuccess('reorder fields', tableId, { fieldCount: fieldIds.length })
365
+ } catch (error) {
366
+ this.handleHttpError(error, 'reorder fields', tableId)
367
+ }
368
+ }
369
+
370
+ // ===== HELPERS PARA TIPOS ESPECÍFICOS =====
371
+
372
+ /**
373
+ * Crear campo de texto
374
+ *
375
+ * Crea un campo de texto simple con valor por defecto opcional.
376
+ * El tipo de campo más básico para almacenar cadenas de texto cortas.
377
+ *
378
+ * @param tableId - ID numérico de la tabla
379
+ * @param name - Nombre del campo
380
+ * @param defaultValue - Valor por defecto (opcional)
381
+ * @param description - Descripción del campo (opcional)
382
+ * @param advancedOptions - Opciones avanzadas: índice y constraints (v1.35+)
383
+ * @returns Promise con el campo de texto creado
384
+ *
385
+ * @example
386
+ * ```typescript
387
+ * const nameField = await fieldService.createTextField(123, 'nombre', 'Sin nombre')
388
+ * const titleField = await fieldService.createTextField(123, 'titulo')
389
+ *
390
+ * // Con índice y constraint (v1.35+)
391
+ * const codeField = await fieldService.createTextField(123, 'codigo', undefined, undefined, {
392
+ * index: true,
393
+ * constraints: [{ type: 'unique_with_empty', active: true }]
394
+ * })
395
+ * ```
396
+ *
397
+ * @since 1.0.0
398
+ */
399
+ async createTextField(
400
+ tableId: number,
401
+ name: string,
402
+ defaultValue?: string,
403
+ description?: string,
404
+ advancedOptions?: FieldAdvancedOptions
405
+ ): Promise<Field> {
406
+ const baseRequest = {
407
+ name,
408
+ type: 'text' as const,
409
+ text_default: defaultValue || '',
410
+ description
411
+ }
412
+
413
+ return this.create(tableId, this.applyAdvancedOptions(baseRequest, advancedOptions))
414
+ }
415
+
416
+ /**
417
+ * Crear campo de texto largo
418
+ *
419
+ * Crea un campo de texto largo (multilinea) para almacenar texto extenso.
420
+ * Ideal para descripciones, comentarios o contenido largo.
421
+ *
422
+ * @param tableId - ID numérico de la tabla
423
+ * @param name - Nombre del campo
424
+ * @param defaultValue - Valor por defecto (opcional)
425
+ * @param description - Descripción del campo (opcional)
426
+ * @returns Promise con el campo de texto largo creado
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * const descField = await fieldService.createLongTextField(123, 'descripcion')
431
+ * const notesField = await fieldService.createLongTextField(123, 'notas', 'Sin notas')
432
+ * ```
433
+ *
434
+ * @since 1.0.0
435
+ */
436
+ async createLongTextField(
437
+ tableId: number,
438
+ name: string,
439
+ defaultValue?: string,
440
+ description?: string
441
+ ): Promise<Field> {
442
+ return this.create(tableId, {
443
+ name,
444
+ type: 'long_text',
445
+ text_default: defaultValue || '',
446
+ description
447
+ })
448
+ }
449
+
450
+ /**
451
+ * Crear campo numérico
452
+ *
453
+ * Crea un campo numérico con configuración completa de formato y validación.
454
+ * Soporta decimales, prefijos/sufijos, separadores y valores por defecto.
455
+ *
456
+ * @param tableId - ID numérico de la tabla
457
+ * @param name - Nombre del campo
458
+ * @param decimalPlaces - Número de decimales (0-10, default: 0)
459
+ * @param allowNegative - Permitir números negativos (default: true)
460
+ * @param defaultValue - Valor numérico por defecto (opcional)
461
+ * @param prefix - Prefijo para mostrar (ej: '$', default: undefined)
462
+ * @param suffix - Sufijo para mostrar (ej: '%', default: undefined)
463
+ * @param separator - Separador de miles (default: 'COMMA_PERIOD')
464
+ * @param description - Descripción del campo (opcional)
465
+ * @returns Promise con el campo numérico creado
466
+ *
467
+ * @throws {BaserowValidationError} Si decimalPlaces no está entre 0-10
468
+ *
469
+ * @example
470
+ * ```typescript
471
+ * // Campo de precio con 2 decimales y símbolo $
472
+ * const priceField = await fieldService.createNumberField(123, 'precio', 2, true, 0, '$')
473
+ *
474
+ * // Campo de porcentaje
475
+ * const percentField = await fieldService.createNumberField(123, 'descuento', 1, false, 0, undefined, '%')
476
+ *
477
+ * // Campo entero simple
478
+ * const ageField = await fieldService.createNumberField(123, 'edad', 0)
479
+ * ```
480
+ *
481
+ * @since 1.0.0
482
+ */
483
+ async createNumberField(
484
+ tableId: number,
485
+ name: string,
486
+ decimalPlaces = 0,
487
+ allowNegative = true,
488
+ defaultValue?: number,
489
+ prefix?: string,
490
+ suffix?: string,
491
+ separator: NumberSeparator = 'COMMA_PERIOD',
492
+ description?: string
493
+ ): Promise<Field> {
494
+ // Validar number_decimal_places (debe estar entre 0-10)
495
+ if (!Number.isInteger(decimalPlaces) || decimalPlaces < 0 || decimalPlaces > 10) {
496
+ throw new BaserowValidationError(`Decimal places must be an integer between 0 and 10, got: ${decimalPlaces}`, {
497
+ number_decimal_places: [`Value must be between 0 and 10, got: ${decimalPlaces}`]
498
+ })
499
+ }
500
+
501
+ return this.create(tableId, {
502
+ name,
503
+ type: 'number',
504
+ number_decimal_places: decimalPlaces,
505
+ number_negative: allowNegative,
506
+ number_default: defaultValue !== undefined ? defaultValue.toFixed(decimalPlaces) : undefined,
507
+ number_prefix: prefix,
508
+ number_suffix: suffix,
509
+ number_separator: separator,
510
+ description
511
+ })
512
+ }
513
+
514
+ /**
515
+ * Crear campo booleano
516
+ *
517
+ * Crea un campo booleano (checkbox) para valores verdadero/falso.
518
+ * Ideal para estados, flags o configuraciones binarias.
519
+ *
520
+ * @param tableId - ID numérico de la tabla
521
+ * @param name - Nombre del campo
522
+ * @param defaultValue - Valor por defecto (default: false)
523
+ * @param description - Descripción del campo (opcional)
524
+ * @returns Promise con el campo booleano creado
525
+ *
526
+ * @example
527
+ * ```typescript
528
+ * const activeField = await fieldService.createBooleanField(123, 'activo', true)
529
+ * const verifiedField = await fieldService.createBooleanField(123, 'verificado')
530
+ * ```
531
+ *
532
+ * @since 1.0.0
533
+ */
534
+ async createBooleanField(tableId: number, name: string, defaultValue = false, description?: string): Promise<Field> {
535
+ return this.create(tableId, {
536
+ name,
537
+ type: 'boolean',
538
+ boolean_default: defaultValue,
539
+ description
540
+ })
541
+ }
542
+
543
+ /**
544
+ * Crear campo de selección simple
545
+ *
546
+ * Crea un campo de selección simple donde solo se puede elegir una opción.
547
+ * Las opciones incluyen valor y color para visualización.
548
+ *
549
+ * @param tableId - ID numérico de la tabla
550
+ * @param name - Nombre del campo
551
+ * @param options - Array de opciones con value y color
552
+ * @param description - Descripción del campo (opcional)
553
+ * @returns Promise con el campo de selección simple creado
554
+ *
555
+ * @throws {Error} Si no se proporcionan opciones
556
+ *
557
+ * @example
558
+ * ```typescript
559
+ * const statusField = await fieldService.createSelectField(123, 'estado', [
560
+ * { value: 'pendiente', color: 'yellow' },
561
+ * { value: 'completado', color: 'green' },
562
+ * { value: 'cancelado', color: 'red' }
563
+ * ])
564
+ * ```
565
+ *
566
+ * @since 1.0.0
567
+ */
568
+ async createSelectField(
569
+ tableId: number,
570
+ name: string,
571
+ options: SelectOption[],
572
+ description?: string
573
+ ): Promise<Field> {
574
+ if (!options || !Array.isArray(options) || options.length === 0) {
575
+ throw new Error('Select field must have at least one option')
576
+ }
577
+
578
+ if (!Array.isArray(options) || options.length === 0) {
579
+ throw new Error('Select field must have at least one option')
580
+ }
581
+
582
+ return this.create(tableId, {
583
+ name,
584
+ type: 'single_select',
585
+ select_options: options,
586
+ description
587
+ })
588
+ }
589
+
590
+ /**
591
+ * Crear campo de selección múltiple
592
+ *
593
+ * Crea un campo de selección múltiple donde se pueden elegir varias opciones.
594
+ * Útil para tags, categorías o cualquier clasificación múltiple.
595
+ *
596
+ * @param tableId - ID numérico de la tabla
597
+ * @param name - Nombre del campo
598
+ * @param options - Array de opciones con value y color
599
+ * @param description - Descripción del campo (opcional)
600
+ * @returns Promise con el campo de selección múltiple creado
601
+ *
602
+ * @throws {Error} Si no se proporcionan opciones
603
+ *
604
+ * @example
605
+ * ```typescript
606
+ * const tagsField = await fieldService.createMultiSelectField(123, 'tags', [
607
+ * { value: 'importante', color: 'red' },
608
+ * { value: 'urgente', color: 'orange' },
609
+ * { value: 'revision', color: 'blue' }
610
+ * ])
611
+ * ```
612
+ *
613
+ * @since 1.0.0
614
+ */
615
+ async createMultiSelectField(
616
+ tableId: number,
617
+ name: string,
618
+ options: SelectOption[],
619
+ description?: string
620
+ ): Promise<Field> {
621
+ if (!options || !Array.isArray(options) || options.length === 0) {
622
+ throw new Error('Multi-select field must have at least one option')
623
+ }
624
+
625
+ if (!Array.isArray(options) || options.length === 0) {
626
+ throw new Error('Multi-select field must have at least one option')
627
+ }
628
+
629
+ return this.create(tableId, {
630
+ name,
631
+ type: 'multiple_select',
632
+ select_options: options,
633
+ description
634
+ })
635
+ }
636
+
637
+ /**
638
+ * Crear campo de fecha
639
+ *
640
+ * Crea un campo de fecha con configuración completa de formato y zona horaria.
641
+ * Soporta fechas simples o fechas con hora, y configuración de timezone.
642
+ *
643
+ * @param tableId - ID numérico de la tabla
644
+ * @param name - Nombre del campo
645
+ * @param includeTime - Incluir hora además de fecha (default: false)
646
+ * @param dateFormat - Formato de fecha (default: 'ISO')
647
+ * @param timeFormat - Formato de hora (default: '24')
648
+ * @param showTzInfo - Mostrar información de timezone (default: false)
649
+ * @param forceTimezone - Timezone forzado (opcional)
650
+ * @param forceTimezoneOffset - Offset de timezone forzado (opcional)
651
+ * @param description - Descripción del campo (opcional)
652
+ * @returns Promise con el campo de fecha creado
653
+ *
654
+ * @example
655
+ * ```typescript
656
+ * // Fecha simple
657
+ * const birthField = await fieldService.createDateField(123, 'nacimiento')
658
+ *
659
+ * // Fecha con hora
660
+ * const createdField = await fieldService.createDateField(123, 'creado', true, 'ISO', '24')
661
+ * ```
662
+ *
663
+ * @since 1.0.0
664
+ */
665
+ async createDateField(
666
+ tableId: number,
667
+ name: string,
668
+ includeTime = false,
669
+ dateFormat = 'ISO',
670
+ timeFormat = '24',
671
+ showTzInfo = false,
672
+ forceTimezone?: string,
673
+ forceTimezoneOffset?: number,
674
+ description?: string
675
+ ): Promise<Field> {
676
+ return this.create(tableId, {
677
+ name,
678
+ type: 'date',
679
+ date_include_time: includeTime,
680
+ date_format: dateFormat,
681
+ date_time_format: timeFormat,
682
+ date_show_tzinfo: showTzInfo,
683
+ date_force_timezone: forceTimezone,
684
+ date_force_timezone_offset: forceTimezoneOffset,
685
+ description
686
+ })
687
+ }
688
+
689
+ /**
690
+ * Crear campo de enlace a otra tabla
691
+ *
692
+ * Crea un campo de relación que enlaza con otra tabla (foreign key).
693
+ * Permite crear relaciones entre tablas para modelar datos relacionales.
694
+ *
695
+ * @param tableId - ID numérico de la tabla origen
696
+ * @param name - Nombre del campo
697
+ * @param targetTableId - ID numérico de la tabla destino
698
+ * @param description - Descripción del campo (opcional)
699
+ * @returns Promise con el campo de enlace creado
700
+ *
701
+ * @throws {BaserowValidationError} Si targetTableId es inválido
702
+ *
703
+ * @example
704
+ * ```typescript
705
+ * // Relacionar tabla de pedidos con tabla de clientes
706
+ * const customerField = await fieldService.createLinkField(ordersTableId, 'cliente', customersTableId)
707
+ * ```
708
+ *
709
+ * @since 1.0.0
710
+ */
711
+ async createLinkField(tableId: number, name: string, targetTableId: number, description?: string): Promise<Field> {
712
+ this.validationService.validateId(targetTableId, 'target table ID')
713
+
714
+ return this.create(tableId, {
715
+ name,
716
+ type: 'link_row',
717
+ link_row_table_id: targetTableId,
718
+ description
719
+ })
720
+ }
721
+
722
+ /**
723
+ * Crear campo de fórmula
724
+ *
725
+ * Crea un campo calculado que usa una fórmula para derivar su valor
726
+ * de otros campos de la misma fila. El valor se actualiza automáticamente.
727
+ *
728
+ * @param tableId - ID numérico de la tabla
729
+ * @param name - Nombre del campo
730
+ * @param formula - Fórmula de cálculo (sintaxis de Baserow)
731
+ * @param description - Descripción del campo (opcional)
732
+ * @returns Promise con el campo de fórmula creado
733
+ *
734
+ * @throws {Error} Si la fórmula está vacía
735
+ *
736
+ * @example
737
+ * ```typescript
738
+ * // Campo que calcula el total como precio * cantidad
739
+ * const totalField = await fieldService.createFormulaField(
740
+ * 123,
741
+ * 'total',
742
+ * 'field("precio") * field("cantidad")'
743
+ * )
744
+ *
745
+ * // Campo que concatena nombre y apellido
746
+ * const fullNameField = await fieldService.createFormulaField(
747
+ * 123,
748
+ * 'nombre_completo',
749
+ * 'concat(field("nombre"), " ", field("apellido"))'
750
+ * )
751
+ * ```
752
+ *
753
+ * @since 1.0.0
754
+ */
755
+ async createFormulaField(tableId: number, name: string, formula: string, description?: string): Promise<Field> {
756
+ if (!formula || typeof formula !== 'string' || formula.trim() === '') {
757
+ throw new Error('Formula is required and must be a non-empty string')
758
+ }
759
+
760
+ return this.create(tableId, {
761
+ name,
762
+ type: 'formula',
763
+ formula,
764
+ description
765
+ })
766
+ }
767
+
768
+ /**
769
+ * Crear campo de URL
770
+ *
771
+ * Crea un campo especializado para almacenar URLs con validación automática.
772
+ * Los valores se validan como URLs válidas y se muestran como enlaces.
773
+ *
774
+ * @param tableId - ID numérico de la tabla
775
+ * @param name - Nombre del campo
776
+ * @param description - Descripción del campo (opcional)
777
+ * @returns Promise con el campo de URL creado
778
+ *
779
+ * @example
780
+ * ```typescript
781
+ * const websiteField = await fieldService.createUrlField(123, 'sitio_web')
782
+ * const linkedinField = await fieldService.createUrlField(123, 'perfil_linkedin')
783
+ * ```
784
+ *
785
+ * @since 1.0.0
786
+ */
787
+ async createUrlField(tableId: number, name: string, description?: string): Promise<Field> {
788
+ return this.create(tableId, {
789
+ name,
790
+ type: 'url',
791
+ description
792
+ })
793
+ }
794
+
795
+ /**
796
+ * Crear campo de email
797
+ *
798
+ * Crea un campo especializado para almacenar direcciones de email
799
+ * con validación automática de formato.
800
+ *
801
+ * @param tableId - ID numérico de la tabla
802
+ * @param name - Nombre del campo
803
+ * @param description - Descripción del campo (opcional)
804
+ * @param advancedOptions - Opciones avanzadas: índice y constraints (v1.35+)
805
+ * @returns Promise con el campo de email creado
806
+ *
807
+ * @example
808
+ * ```typescript
809
+ * const emailField = await fieldService.createEmailField(123, 'email')
810
+ * const contactField = await fieldService.createEmailField(123, 'email_contacto')
811
+ *
812
+ * // Con índice y restricción de unicidad (v1.35+)
813
+ * const uniqueEmailField = await fieldService.createEmailField(123, 'email', undefined, {
814
+ * index: true,
815
+ * constraints: [{ type: 'unique', active: true }]
816
+ * })
817
+ * ```
818
+ *
819
+ * @since 1.0.0
820
+ */
821
+ async createEmailField(
822
+ tableId: number,
823
+ name: string,
824
+ description?: string,
825
+ advancedOptions?: FieldAdvancedOptions
826
+ ): Promise<Field> {
827
+ const baseRequest = {
828
+ name,
829
+ type: 'email' as const,
830
+ description
831
+ }
832
+
833
+ return this.create(tableId, this.applyAdvancedOptions(baseRequest, advancedOptions))
834
+ }
835
+
836
+ /**
837
+ * Crear campo de teléfono
838
+ *
839
+ * Crea un campo especializado para almacenar números de teléfono
840
+ * con formateo automático y validación.
841
+ *
842
+ * @param tableId - ID numérico de la tabla
843
+ * @param name - Nombre del campo
844
+ * @param description - Descripción del campo (opcional)
845
+ * @returns Promise con el campo de teléfono creado
846
+ *
847
+ * @example
848
+ * ```typescript
849
+ * const phoneField = await fieldService.createPhoneField(123, 'telefono')
850
+ * const mobileField = await fieldService.createPhoneField(123, 'movil')
851
+ * ```
852
+ *
853
+ * @since 1.0.0
854
+ */
855
+ async createPhoneField(tableId: number, name: string, description?: string): Promise<Field> {
856
+ return this.create(tableId, {
857
+ name,
858
+ type: 'phone_number',
859
+ description
860
+ })
861
+ }
862
+
863
+ /**
864
+ * Crear campo de rating
865
+ *
866
+ * Crea un campo de calificación visual con estrellas, corazones u otros íconos.
867
+ * Ideal para ratings, puntuaciones o evaluaciones.
868
+ *
869
+ * @param tableId - ID numérico de la tabla
870
+ * @param name - Nombre del campo
871
+ * @param maxValue - Valor máximo (1-10, default: 5)
872
+ * @param color - Color del rating (default: 'yellow')
873
+ * @param style - Estilo del ícono (default: 'star')
874
+ * @param description - Descripción del campo (opcional)
875
+ * @returns Promise con el campo de rating creado
876
+ *
877
+ * @throws {Error} Si maxValue no está entre 1-10
878
+ *
879
+ * @example
880
+ * ```typescript
881
+ * // Rating de 5 estrellas amarillas
882
+ * const ratingField = await fieldService.createRatingField(123, 'calificacion')
883
+ *
884
+ * // Rating personalizado de 10 corazones rojos
885
+ * const loveField = await fieldService.createRatingField(123, 'amor', 10, 'red', 'heart')
886
+ * ```
887
+ *
888
+ * @since 1.0.0
889
+ */
890
+ async createRatingField(
891
+ tableId: number,
892
+ name: string,
893
+ maxValue: number = 5,
894
+ color: RatingColor = 'yellow',
895
+ style: RatingStyle = 'star',
896
+ description?: string
897
+ ): Promise<Field> {
898
+ this.validationService.validateId(tableId, 'table ID')
899
+ this.validationService.validateResourceName(name, 'field')
900
+
901
+ if (maxValue < 1 || maxValue > 10) {
902
+ throw new Error('maxValue must be between 1 and 10')
903
+ }
904
+
905
+ // Validación removida - ahora usamos RatingColor enum que tiene valores válidos
906
+
907
+ return this.create(tableId, {
908
+ name,
909
+ type: 'rating',
910
+ max_value: maxValue,
911
+ color: color,
912
+ style: style,
913
+ description
914
+ })
915
+ }
916
+
917
+ /**
918
+ * Crear campo de última modificación (fecha)
919
+ *
920
+ * Crea un campo de auditoría que se actualiza automáticamente
921
+ * con la fecha de la última modificación de la fila.
922
+ *
923
+ * @param tableId - ID numérico de la tabla
924
+ * @param name - Nombre del campo
925
+ * @param description - Descripción del campo (opcional)
926
+ * @returns Promise con el campo de última modificación creado
927
+ *
928
+ * @example
929
+ * ```typescript
930
+ * const updatedField = await fieldService.createLastModifiedField(123, 'actualizado')
931
+ * ```
932
+ *
933
+ * @since 1.0.0
934
+ */
935
+ async createLastModifiedField(tableId: number, name: string, description?: string): Promise<Field> {
936
+ return this.create(tableId, {
937
+ name,
938
+ type: 'last_modified',
939
+ description
940
+ })
941
+ }
942
+
943
+ /**
944
+ * Crear campo de última modificación por usuario
945
+ *
946
+ * Crea un campo de auditoría que se actualiza automáticamente
947
+ * con el usuario que realizó la última modificación de la fila.
948
+ *
949
+ * @param tableId - ID numérico de la tabla
950
+ * @param name - Nombre del campo
951
+ * @param description - Descripción del campo (opcional)
952
+ * @returns Promise con el campo de última modificación por usuario creado
953
+ *
954
+ * @example
955
+ * ```typescript
956
+ * const modifiedByField = await fieldService.createLastModifiedByField(123, 'modificado_por')
957
+ * ```
958
+ *
959
+ * @since 1.0.0
960
+ */
961
+ async createLastModifiedByField(tableId: number, name: string, description?: string): Promise<Field> {
962
+ return this.create(tableId, {
963
+ name,
964
+ type: 'last_modified_by',
965
+ description
966
+ })
967
+ }
968
+
969
+ /**
970
+ * Crear campo de fecha de creación
971
+ *
972
+ * Crea un campo de auditoría que se establece automáticamente
973
+ * con la fecha de creación de la fila (no se modifica después).
974
+ *
975
+ * @param tableId - ID numérico de la tabla
976
+ * @param name - Nombre del campo
977
+ * @param description - Descripción del campo (opcional)
978
+ * @returns Promise con el campo de fecha de creación creado
979
+ *
980
+ * @example
981
+ * ```typescript
982
+ * const createdField = await fieldService.createCreatedOnField(123, 'creado')
983
+ * ```
984
+ *
985
+ * @since 1.0.0
986
+ */
987
+ async createCreatedOnField(tableId: number, name: string, description?: string): Promise<Field> {
988
+ return this.create(tableId, {
989
+ name,
990
+ type: 'created_on',
991
+ description
992
+ })
993
+ }
994
+
995
+ /**
996
+ * Crear campo de usuario creador
997
+ *
998
+ * Crea un campo de auditoría que se establece automáticamente
999
+ * con el usuario que creó la fila (no se modifica después).
1000
+ *
1001
+ * @param tableId - ID numérico de la tabla
1002
+ * @param name - Nombre del campo
1003
+ * @param description - Descripción del campo (opcional)
1004
+ * @returns Promise con el campo de usuario creador creado
1005
+ *
1006
+ * @example
1007
+ * ```typescript
1008
+ * const createdByField = await fieldService.createCreatedByField(123, 'creado_por')
1009
+ * ```
1010
+ *
1011
+ * @since 1.0.0
1012
+ */
1013
+ async createCreatedByField(tableId: number, name: string, description?: string): Promise<Field> {
1014
+ return this.create(tableId, {
1015
+ name,
1016
+ type: 'created_by',
1017
+ description
1018
+ })
1019
+ }
1020
+
1021
+ /**
1022
+ * Crear campo de archivo
1023
+ *
1024
+ * Crea un campo para subir y almacenar archivos adjuntos.
1025
+ * Soporta múltiples archivos por fila con previsualización automática.
1026
+ *
1027
+ * @param tableId - ID numérico de la tabla
1028
+ * @param name - Nombre del campo
1029
+ * @param description - Descripción del campo (opcional)
1030
+ * @returns Promise con el campo de archivo creado
1031
+ *
1032
+ * @example
1033
+ * ```typescript
1034
+ * const attachmentField = await fieldService.createFileField(123, 'documentos')
1035
+ * const photoField = await fieldService.createFileField(123, 'fotos')
1036
+ * ```
1037
+ *
1038
+ * @since 1.0.0
1039
+ */
1040
+ async createFileField(tableId: number, name: string, description?: string): Promise<Field> {
1041
+ return this.create(tableId, {
1042
+ name,
1043
+ type: 'file',
1044
+ description
1045
+ })
1046
+ }
1047
+
1048
+ /**
1049
+ * Crear campo de numeración automática
1050
+ *
1051
+ * Crea un campo que asigna automáticamente números secuenciales únicos
1052
+ * a cada nueva fila. Útil para IDs de tickets, números de orden, etc.
1053
+ *
1054
+ * @param tableId - ID numérico de la tabla
1055
+ * @param name - Nombre del campo
1056
+ * @param description - Descripción del campo (opcional)
1057
+ * @returns Promise con el campo de numeración automática creado
1058
+ *
1059
+ * @example
1060
+ * ```typescript
1061
+ * const ticketField = await fieldService.createAutonumberField(123, 'ticket_id')
1062
+ * const orderField = await fieldService.createAutonumberField(123, 'numero_orden')
1063
+ * ```
1064
+ *
1065
+ * @since 1.0.0
1066
+ */
1067
+ async createAutonumberField(tableId: number, name: string, description?: string): Promise<Field> {
1068
+ return this.create(tableId, {
1069
+ name,
1070
+ type: 'autonumber',
1071
+ description
1072
+ })
1073
+ }
1074
+
1075
+ /**
1076
+ * Crear campo de conteo
1077
+ *
1078
+ * Crea un campo que cuenta automáticamente el número de registros
1079
+ * relacionados a través de un campo de enlace. Se actualiza automáticamente.
1080
+ *
1081
+ * @param tableId - ID numérico de la tabla
1082
+ * @param name - Nombre del campo
1083
+ * @param throughFieldId - ID del campo de enlace a través del cual contar
1084
+ * @param description - Descripción del campo (opcional)
1085
+ * @returns Promise con el campo de conteo creado
1086
+ *
1087
+ * @throws {BaserowValidationError} Si throughFieldId es inválido
1088
+ *
1089
+ * @example
1090
+ * ```typescript
1091
+ * // Contar número de pedidos por cliente
1092
+ * const orderCountField = await fieldService.createCountField(
1093
+ * customersTableId,
1094
+ * 'total_pedidos',
1095
+ * customerLinkFieldId
1096
+ * )
1097
+ * ```
1098
+ *
1099
+ * @since 1.0.0
1100
+ */
1101
+ async createCountField(tableId: number, name: string, throughFieldId: number, description?: string): Promise<Field> {
1102
+ this.validationService.validateId(throughFieldId, 'through field ID')
1103
+
1104
+ return this.create(tableId, {
1105
+ name,
1106
+ type: 'count',
1107
+ through_field_id: throughFieldId,
1108
+ description
1109
+ })
1110
+ }
1111
+
1112
+ /**
1113
+ * Crear campo de rollup (agregación)
1114
+ *
1115
+ * Crea un campo que agrega valores de registros relacionados usando
1116
+ * funciones como sum, avg, min, max, etc. Se actualiza automáticamente.
1117
+ *
1118
+ * @param tableId - ID numérico de la tabla
1119
+ * @param name - Nombre del campo
1120
+ * @param throughFieldId - ID del campo de enlace para la relación
1121
+ * @param targetFieldId - ID del campo a agregar en la tabla relacionada
1122
+ * @param rollupFunction - Función de agregación (default: 'sum')
1123
+ * @param description - Descripción del campo (opcional)
1124
+ * @returns Promise con el campo de rollup creado
1125
+ *
1126
+ * @throws {BaserowValidationError} Si los IDs son inválidos
1127
+ *
1128
+ * @example
1129
+ * ```typescript
1130
+ * // Sumar total de ventas por cliente
1131
+ * const totalSalesField = await fieldService.createRollupField(
1132
+ * customersTableId,
1133
+ * 'ventas_totales',
1134
+ * customerLinkFieldId,
1135
+ * orderAmountFieldId,
1136
+ * 'sum'
1137
+ * )
1138
+ *
1139
+ * // Calcular promedio de calificaciones
1140
+ * const avgRatingField = await fieldService.createRollupField(
1141
+ * productsTableId,
1142
+ * 'rating_promedio',
1143
+ * productLinkFieldId,
1144
+ * ratingFieldId,
1145
+ * 'avg'
1146
+ * )
1147
+ * ```
1148
+ *
1149
+ * @since 1.0.0
1150
+ */
1151
+ async createRollupField(
1152
+ tableId: number,
1153
+ name: string,
1154
+ throughFieldId: number,
1155
+ targetFieldId: number,
1156
+ rollupFunction: string = 'sum',
1157
+ description?: string
1158
+ ): Promise<Field> {
1159
+ this.validationService.validateId(throughFieldId, 'through field ID')
1160
+ this.validationService.validateId(targetFieldId, 'target field ID')
1161
+
1162
+ return this.create(tableId, {
1163
+ name,
1164
+ type: 'rollup',
1165
+ through_field_id: throughFieldId,
1166
+ target_field_id: targetFieldId,
1167
+ rollup_function: rollupFunction,
1168
+ description
1169
+ })
1170
+ }
1171
+
1172
+ /**
1173
+ * Crear campo de lookup (búsqueda)
1174
+ *
1175
+ * Crea un campo que muestra valores de un campo en registros relacionados.
1176
+ * Similar a un VLOOKUP en spreadsheets. Se actualiza automáticamente.
1177
+ *
1178
+ * @param tableId - ID numérico de la tabla
1179
+ * @param name - Nombre del campo
1180
+ * @param throughFieldId - ID del campo de enlace para la relación
1181
+ * @param targetFieldId - ID del campo a mostrar en la tabla relacionada
1182
+ * @param description - Descripción del campo (opcional)
1183
+ * @returns Promise con el campo de lookup creado
1184
+ *
1185
+ * @throws {BaserowValidationError} Si los IDs son inválidos
1186
+ *
1187
+ * @example
1188
+ * ```typescript
1189
+ * // Mostrar nombre del cliente en tabla de pedidos
1190
+ * const customerNameField = await fieldService.createLookupField(
1191
+ * ordersTableId,
1192
+ * 'nombre_cliente',
1193
+ * customerLinkFieldId,
1194
+ * customerNameFieldId
1195
+ * )
1196
+ *
1197
+ * // Mostrar categoría del producto en pedidos
1198
+ * const productCategoryField = await fieldService.createLookupField(
1199
+ * ordersTableId,
1200
+ * 'categoria_producto',
1201
+ * productLinkFieldId,
1202
+ * categoryFieldId
1203
+ * )
1204
+ * ```
1205
+ *
1206
+ * @since 1.0.0
1207
+ */
1208
+ async createLookupField(
1209
+ tableId: number,
1210
+ name: string,
1211
+ throughFieldId: number,
1212
+ targetFieldId: number,
1213
+ description?: string
1214
+ ): Promise<Field> {
1215
+ this.validationService.validateId(throughFieldId, 'through field ID')
1216
+ this.validationService.validateId(targetFieldId, 'target field ID')
1217
+
1218
+ return this.create(tableId, {
1219
+ name,
1220
+ type: 'lookup',
1221
+ through_field_id: throughFieldId,
1222
+ target_field_id: targetFieldId,
1223
+ description
1224
+ })
1225
+ }
1226
+
1227
+ /**
1228
+ * Verificar si un campo existe
1229
+ *
1230
+ * Verifica la existencia de un campo sin cargar todos sus metadatos.
1231
+ * Útil para validaciones antes de operaciones.
1232
+ *
1233
+ * @param fieldId - ID numérico del campo
1234
+ * @returns Promise que resuelve a true si existe, false si no
1235
+ *
1236
+ * @example
1237
+ * ```typescript
1238
+ * const exists = await fieldService.exists(456)
1239
+ * if (exists) {
1240
+ * console.log('El campo existe')
1241
+ * } else {
1242
+ * console.log('El campo no existe')
1243
+ * }
1244
+ * ```
1245
+ *
1246
+ * @since 1.0.0
1247
+ */
1248
+ async exists(fieldId: number): Promise<boolean> {
1249
+ try {
1250
+ await this.get(fieldId)
1251
+ return true
1252
+ } catch (error) {
1253
+ if (error instanceof BaserowNotFoundError) {
1254
+ return false
1255
+ }
1256
+ throw error
1257
+ }
1258
+ }
1259
+
1260
+ // ===== GESTIÓN DE ÍNDICES Y CONSTRAINTS (v1.35+) =====
1261
+
1262
+ /**
1263
+ * Configurar índice en campo para mejorar performance
1264
+ *
1265
+ * Habilita o deshabilita el índice en un campo para acelerar
1266
+ * las operaciones de filtrado hasta 10x más rápido.
1267
+ *
1268
+ * @param fieldId - ID numérico del campo
1269
+ * @param enabled - true para habilitar índice, false para deshabilitar
1270
+ * @returns Promise con el campo actualizado
1271
+ *
1272
+ * @throws {BaserowNotFoundError} Si el campo no existe
1273
+ * @throws {BaserowValidationError} Si el fieldId es inválido
1274
+ *
1275
+ * @example
1276
+ * ```typescript
1277
+ * // Habilitar índice para mejorar filtros en campo email
1278
+ * const field = await fieldService.setFieldIndex(emailFieldId, true)
1279
+ * console.log(`Índice ${field.index ? 'habilitado' : 'deshabilitado'}`)
1280
+ *
1281
+ * // Deshabilitar índice
1282
+ * await fieldService.setFieldIndex(fieldId, false)
1283
+ * ```
1284
+ *
1285
+ * @since 1.1.0
1286
+ */
1287
+ async setFieldIndex(fieldId: number, enabled: boolean): Promise<Field> {
1288
+ this.validationService.validateId(fieldId, 'field ID')
1289
+
1290
+ try {
1291
+ this.logDebug(`${enabled ? 'Enabling' : 'Disabling'} index for field ${fieldId}`)
1292
+ const response = await this.http.patch<Field>(`/database/fields/${fieldId}/`, {
1293
+ index: enabled
1294
+ })
1295
+ this.logSuccess(`set field index`, fieldId, { enabled })
1296
+ return response
1297
+ } catch (error) {
1298
+ if ((error as any).status === 404) {
1299
+ throw new BaserowNotFoundError('Field', fieldId)
1300
+ }
1301
+ this.handleHttpError(error, 'set field index', fieldId)
1302
+ }
1303
+ }
1304
+
1305
+ /**
1306
+ * Agregar restricción de valor a campo
1307
+ *
1308
+ * Agrega una nueva restricción de integridad al campo para
1309
+ * garantizar calidad de datos (ej: valores únicos).
1310
+ *
1311
+ * @param fieldId - ID numérico del campo
1312
+ * @param constraint - Configuración de la restricción
1313
+ * @returns Promise con el campo actualizado
1314
+ *
1315
+ * @throws {BaserowNotFoundError} Si el campo no existe
1316
+ * @throws {BaserowValidationError} Si los parámetros son inválidos
1317
+ *
1318
+ * @example
1319
+ * ```typescript
1320
+ * // Agregar restricción de unicidad (excluyendo vacíos)
1321
+ * const field = await fieldService.addFieldConstraint(emailFieldId, {
1322
+ * type: 'unique',
1323
+ * active: true
1324
+ * })
1325
+ *
1326
+ * // Agregar restricción de unicidad (incluyendo vacíos)
1327
+ * await fieldService.addFieldConstraint(codeFieldId, {
1328
+ * type: 'unique_with_empty',
1329
+ * active: true
1330
+ * })
1331
+ * ```
1332
+ *
1333
+ * @since 1.1.0
1334
+ */
1335
+ async addFieldConstraint(fieldId: number, constraint: FieldConstraint): Promise<Field> {
1336
+ this.validationService.validateId(fieldId, 'field ID')
1337
+
1338
+ if (!constraint.type || typeof constraint.active !== 'boolean') {
1339
+ throw new BaserowValidationError('Invalid constraint configuration', {
1340
+ constraint: ['type and active are required']
1341
+ })
1342
+ }
1343
+
1344
+ // Obtener constraints existentes
1345
+ const currentField = await this.get(fieldId)
1346
+ const existingConstraints = currentField.constraints || []
1347
+
1348
+ // Verificar si ya existe una constraint del mismo tipo
1349
+ const existingIndex = existingConstraints.findIndex(c => c.type === constraint.type)
1350
+ let updatedConstraints: FieldConstraint[]
1351
+
1352
+ if (existingIndex >= 0) {
1353
+ // Actualizar constraint existente
1354
+ updatedConstraints = [...existingConstraints]
1355
+ updatedConstraints[existingIndex] = constraint
1356
+ } else {
1357
+ // Agregar nueva constraint
1358
+ updatedConstraints = [...existingConstraints, constraint]
1359
+ }
1360
+
1361
+ try {
1362
+ this.logDebug(`Adding constraint ${constraint.type} to field ${fieldId}`, { constraint })
1363
+ const response = await this.http.patch<Field>(`/database/fields/${fieldId}/`, {
1364
+ constraints: updatedConstraints
1365
+ })
1366
+ this.logSuccess('add field constraint', fieldId, { type: constraint.type })
1367
+ return response
1368
+ } catch (error) {
1369
+ if ((error as any).status === 404) {
1370
+ throw new BaserowNotFoundError('Field', fieldId)
1371
+ }
1372
+ this.handleHttpError(error, 'add field constraint', fieldId)
1373
+ }
1374
+ }
1375
+
1376
+ /**
1377
+ * Eliminar restricción de valor de campo
1378
+ *
1379
+ * Elimina una restricción específica del campo por tipo.
1380
+ *
1381
+ * @param fieldId - ID numérico del campo
1382
+ * @param constraintType - Tipo de restricción a eliminar
1383
+ * @returns Promise con el campo actualizado
1384
+ *
1385
+ * @throws {BaserowNotFoundError} Si el campo no existe
1386
+ * @throws {BaserowValidationError} Si el fieldId es inválido
1387
+ *
1388
+ * @example
1389
+ * ```typescript
1390
+ * // Eliminar restricción de unicidad
1391
+ * const field = await fieldService.removeFieldConstraint(emailFieldId, 'unique')
1392
+ * console.log(`Constraints restantes: ${field.constraints?.length || 0}`)
1393
+ * ```
1394
+ *
1395
+ * @since 1.1.0
1396
+ */
1397
+ async removeFieldConstraint(fieldId: number, constraintType: ConstraintType): Promise<Field> {
1398
+ this.validationService.validateId(fieldId, 'field ID')
1399
+
1400
+ // Obtener constraints existentes
1401
+ const currentField = await this.get(fieldId)
1402
+ const existingConstraints = currentField.constraints || []
1403
+
1404
+ // Filtrar constraint a eliminar
1405
+ const updatedConstraints = existingConstraints.filter(c => c.type !== constraintType)
1406
+
1407
+ try {
1408
+ this.logDebug(`Removing constraint ${constraintType} from field ${fieldId}`)
1409
+ const response = await this.http.patch<Field>(`/database/fields/${fieldId}/`, {
1410
+ constraints: updatedConstraints
1411
+ })
1412
+ this.logSuccess('remove field constraint', fieldId, { type: constraintType })
1413
+ return response
1414
+ } catch (error) {
1415
+ if ((error as any).status === 404) {
1416
+ throw new BaserowNotFoundError('Field', fieldId)
1417
+ }
1418
+ this.handleHttpError(error, 'remove field constraint', fieldId)
1419
+ }
1420
+ }
1421
+
1422
+ /**
1423
+ * Obtener restricciones de campo
1424
+ *
1425
+ * Recupera todas las restricciones activas de un campo específico.
1426
+ *
1427
+ * @param fieldId - ID numérico del campo
1428
+ * @returns Promise con array de restricciones del campo
1429
+ *
1430
+ * @throws {BaserowNotFoundError} Si el campo no existe
1431
+ * @throws {BaserowValidationError} Si el fieldId es inválido
1432
+ *
1433
+ * @example
1434
+ * ```typescript
1435
+ * const constraints = await fieldService.getFieldConstraints(emailFieldId)
1436
+ * constraints.forEach(constraint => {
1437
+ * console.log(`Constraint ${constraint.type}: ${constraint.active ? 'activa' : 'inactiva'}`)
1438
+ * })
1439
+ * ```
1440
+ *
1441
+ * @since 1.1.0
1442
+ */
1443
+ async getFieldConstraints(fieldId: number): Promise<FieldConstraint[]> {
1444
+ this.validationService.validateId(fieldId, 'field ID')
1445
+
1446
+ const field = await this.get(fieldId)
1447
+ return field.constraints || []
1448
+ }
1449
+
1450
+ /**
1451
+ * Eliminar todas las restricciones de un campo
1452
+ *
1453
+ * Remueve completamente todas las constraints configuradas en un campo específico,
1454
+ * dejándolo sin ninguna restricción de integridad. Esta operación es útil para
1455
+ * limpiar campos que han acumulado múltiples constraints o para resetear
1456
+ * la configuración de restricciones.
1457
+ *
1458
+ * @param fieldId - ID del campo del cual eliminar todas las constraints
1459
+ * @returns Promise con el campo actualizado sin constraints
1460
+ *
1461
+ * @throws {BaserowValidationError} Si fieldId es inválido
1462
+ * @throws {BaserowNotFoundError} Si el campo no existe
1463
+ * @throws {BaserowError} Si hay error en la comunicación con la API
1464
+ *
1465
+ * @example
1466
+ * ```typescript
1467
+ * // Eliminar todas las constraints de un campo
1468
+ * const field = await fieldService.removeAllFieldConstraints(emailFieldId)
1469
+ * console.log(`Campo "${field.name}" ahora sin constraints`)
1470
+ *
1471
+ * // Verificar que se eliminaron todas
1472
+ * const remainingConstraints = await fieldService.getFieldConstraints(emailFieldId)
1473
+ * console.log(`Constraints restantes: ${remainingConstraints.length}`) // 0
1474
+ * ```
1475
+ *
1476
+ * @since 1.1.0
1477
+ */
1478
+ async removeAllFieldConstraints(fieldId: number): Promise<Field> {
1479
+ this.validationService.validateId(fieldId, 'field ID')
1480
+
1481
+ try {
1482
+ this.logDebug(`Removing all constraints from field ${fieldId}`)
1483
+
1484
+ const updatedField = await this.updateFieldInternal(fieldId, {
1485
+ constraints: []
1486
+ })
1487
+
1488
+ this.logSuccess('remove all field constraints', fieldId, { removedAll: true })
1489
+ return updatedField
1490
+ } catch (error) {
1491
+ this.handleHttpError(error, 'remove all field constraints', fieldId)
1492
+ throw error
1493
+ }
1494
+ }
1495
+
1496
+ /**
1497
+ * Aplicar opciones avanzadas a solicitud de creación de campo
1498
+ *
1499
+ * Método helper interno que aplica índices y constraints a la configuración
1500
+ * de creación de campo.
1501
+ *
1502
+ * @param baseRequest - Solicitud base de creación
1503
+ * @param advancedOptions - Opciones avanzadas (índice y constraints)
1504
+ * @returns Solicitud enriquecida con opciones avanzadas
1505
+ *
1506
+ * @private
1507
+ * @since 1.1.0
1508
+ */
1509
+ private applyAdvancedOptions(
1510
+ baseRequest: CreateFieldRequest,
1511
+ advancedOptions?: FieldAdvancedOptions
1512
+ ): CreateFieldRequest {
1513
+ if (!advancedOptions) return baseRequest
1514
+
1515
+ const enrichedRequest = { ...baseRequest }
1516
+
1517
+ if (advancedOptions.index !== undefined) {
1518
+ enrichedRequest.index = advancedOptions.index
1519
+ }
1520
+
1521
+ if (advancedOptions.constraints && advancedOptions.constraints.length > 0) {
1522
+ enrichedRequest.constraints = advancedOptions.constraints
1523
+ }
1524
+
1525
+ return enrichedRequest
1526
+ }
1527
+
1528
+ // ===== FRIEND ACCESS PATTERN FOR FIELD CONTEXT =====
1529
+ // Symbol-based access que no aparece en la API pública pero permite acceso interno
1530
+
1531
+ /**
1532
+ * Friend access para FieldContext
1533
+ * No aparece en intellisense normal ni en la API pública
1534
+ * @internal
1535
+ */
1536
+ get [Symbol.for('fieldContext')]() {
1537
+ return {
1538
+ createField: this.createFieldInternal.bind(this),
1539
+ updateField: this.updateFieldInternal.bind(this),
1540
+ deleteField: this.deleteFieldInternal.bind(this)
1541
+ }
1542
+ }
1543
+ }