You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
senzapaura_es/src/lib/server/db/schema/catalogo.ts

369 lines
12 KiB

/**
* El catálogo: estilos, discos, canciones y todo lo que cuelga de una canción.
*
* Hasta ahora esto vivía en `src/content/**` como Markdown con frontmatter, y
* una canción llevaba veintiún campos estructurados en YAML: `registro`,
* `duracion`, `interpretes`, `versiones`, `videos`, `etiquetas`… El cuerpo del
* Markdown sí era contenido de verdad —los versos—, pero todo lo demás eran
* datos con forma de tabla metidos en un archivo de texto.
*/
import { sql } from 'drizzle-orm';
import {
boolean,
char,
index,
integer,
numeric,
pgTable,
primaryKey,
text,
uniqueIndex
} from 'drizzle-orm/pg-core';
import { ahora, estado, id, instante } from './comun';
import { medio, pais, persona } from './base';
/**
* Una sección del catálogo: baladas románticas, clásica, rock, pop melódico.
*
* NO es lo mismo que un género del taller. Aquello —bolero, tango, cumbia— es
* música hispanoamericana descrita para poder escribirla; esto es cómo el
* titular ordena su propia obra. Compartían la palabra «estilo» y por eso
* compartían nombre en el código, que es de donde venía la confusión.
*/
export const estilo = pgTable(
'estilo',
{
id: id(),
slug: text('slug').notNull().unique(),
nombre: text('nombre').notNull(),
resumen: text('resumen').notNull().default(''),
/** Menor, más arriba. */
orden: integer('orden').notNull().default(0),
/** El estilo principal del artista. Solo puede haber uno: ver el índice. */
principal: boolean('principal').notNull().default(false),
imagenId: text('imagen_id').references(() => medio.id, { onDelete: 'set null' }),
estado: estado(),
creadoEn: ahora('creado_en'),
actualizadoEn: instante('actualizado_en')
},
(t) => [
/*
* Índice parcial: solo entran las filas con `principal` a cierto, así que
* la unicidad se aplica únicamente entre ellas y las demás no estorban.
* Hasta ahora, que solo hubiera un estilo principal era un acuerdo escrito
* en un comentario; ahora es la base la que no deja poner dos.
*/
uniqueIndex('estilo_principal_unico')
.on(t.principal)
.where(sql`${t.principal}`),
index('estilo_orden').on(t.orden)
]
);
/** Formatos en los que existe una edición. */
export const FORMATOS = ['digital', 'cd', 'vinilo', 'casete'] as const;
export const album = pgTable(
'album',
{
id: id(),
slug: text('slug').notNull().unique(),
titulo: text('titulo').notNull(),
estiloId: text('estilo_id')
.notNull()
.references(() => estilo.id),
tipo: text('tipo', { enum: ['album', 'ep', 'single', 'coleccion'] })
.notNull()
.default('album'),
/** Fecha de publicación, ISO `YYYY-MM-DD`. */
fecha: text('fecha').notNull(),
portadaId: text('portada_id').references(() => medio.id, { onDelete: 'set null' }),
sello: text('sello'),
resumen: text('resumen').notNull().default(''),
destacado: boolean('destacado').notNull().default(false),
/* --- Datos de edición: lo que iría en la contraportada --------------- */
numeroCatalogo: text('numero_catalogo'),
/** Se pintan, no se filtran: array y no tabla puente. */
formatos: text('formatos').array().$type<(typeof FORMATOS)[number][]>(),
paisEdicionCodigo: char('pais_edicion_codigo', { length: 2 }).references(() => pais.codigo),
estado: estado(),
creadoEn: ahora('creado_en'),
actualizadoEn: instante('actualizado_en')
},
(t) => [index('album_estilo').on(t.estiloId), index('album_fecha').on(t.fecha)]
);
/**
* Un tema. Y también cada una de sus versiones.
*
* **Una versión es una fila de esta misma tabla**, con `versionDeId` apuntando
* a la principal. No es una tabla aparte, y no es un `obra`/`grabacion` partido
* en dos: en ese reparto el estilo y las etiquetas colgarían de la obra, y una
* versión no podría tener los suyos —una balada con arreglo de bolero no
* cabría—. Aquí cualquier campo puede cambiar de una versión a otra.
*
* Eso arregla de paso algo que estaba mal: **el disco es de la fila**. Antes
* `album` y `numero` colgaban del tema, así que la «Maqueta 2018» era el corte
* 1 del álbum igual que la versión de estudio, y es falso.
*
* Al crear una versión sus campos se **copian** de la principal; no se heredan
* al leer. Un nulo heredado nunca se sabe si significa «no lo sé» o «lo mismo
* de arriba». Y aquí copiar es además lo correcto: la maqueta de 2018 no tiene
* la misma letra, le falta el puente.
*/
export const cancion = pgTable(
'cancion',
{
id: id(),
slug: text('slug').notNull().unique(),
titulo: text('titulo').notNull(),
/**
* Nula en las principales. Una versión apunta aquí a su tema de
* referencia.
*
* La clave foránea se declara en la migración y no aquí: Drizzle no deja
* referenciar una tabla dentro de su propia definición. Allí va también la
* guarda que impide la versión de una versión y los ciclos.
*
* Ojo al consultar: casi todo lo que lista o cuenta canciones quiere solo
* las principales. Para eso está la vista `cancion_principal`; pedir
* versiones tiene que ser lo que se escribe a propósito.
*/
versionDeId: text('version_de_id'),
/** Nombre del arreglo: «Versión orquestal», «Maqueta 2018». */
versionNombre: text('version_nombre'),
/** Qué tiene de particular ese arreglo. Va en la ficha, junto al nombre. */
versionNota: text('version_nota'),
/** Orden entre las versiones de un mismo tema. */
versionOrden: integer('version_orden').notNull().default(0),
estiloId: text('estilo_id')
.notNull()
.references(() => estilo.id),
albumId: text('album_id').references(() => album.id, { onDelete: 'set null' }),
numero: integer('numero'),
audioId: text('audio_id').references(() => medio.id, { onDelete: 'set null' }),
muestraId: text('muestra_id').references(() => medio.id, { onDelete: 'set null' }),
duracionS: integer('duracion_s'),
ano: integer('ano'),
/**
* Los versos, con sus saltos de línea: es verso, no prosa, y una letra que
* pierde el corte deja de ser la letra.
*
* Es el CUERPO del Markdown de hoy, no un campo del frontmatter. El campo
* `letra:` que aparece en un archivo es otra cosa: quién firma la letra, y
* eso va a `cancion_persona`.
*/
letra: text('letra'),
/** ISO 639. Decide si la ficha enseña una columna o dos. */
idioma: text('idioma').notNull().default('es'),
/** De dónde sale el tema. Del frontmatter, en párrafos. */
inspiracion: text('inspiracion'),
instrumental: boolean('instrumental').notNull().default(false),
destacada: boolean('destacada').notNull().default(false),
portadaId: text('portada_id').references(() => medio.id, { onDelete: 'set null' }),
/** Solo en las principales: comprar una entrega todas sus versiones. */
alaVenta: boolean('ala_venta').notNull().default(false),
estado: estado(),
creadoEn: ahora('creado_en'),
actualizadoEn: instante('actualizado_en')
},
(t) => [
index('cancion_estilo').on(t.estiloId),
index('cancion_album').on(t.albumId),
index('cancion_version_de').on(t.versionDeId),
index('cancion_destacada').on(t.destacada)
]
);
/** La letra en otro idioma. Antes solo cabía una, al español. */
export const traduccion = pgTable(
'traduccion',
{
cancionId: text('cancion_id')
.notNull()
.references(() => cancion.id, { onDelete: 'cascade' }),
idioma: text('idioma').notNull(),
texto: text('texto').notNull()
},
(t) => [primaryKey({ columns: [t.cancionId, t.idioma] })]
);
/** Quién hizo qué en un tema. El orden en que se acredita importa. */
export const cancionPersona = pgTable(
'cancion_persona',
{
cancionId: text('cancion_id')
.notNull()
.references(() => cancion.id, { onDelete: 'cascade' }),
personaId: text('persona_id')
.notNull()
.references(() => persona.id),
rol: text('rol', {
enum: ['letra', 'musica', 'interprete', 'arreglos', 'produccion']
}).notNull(),
orden: integer('orden').notNull().default(0)
},
(t) => [primaryKey({ columns: [t.cancionId, t.personaId, t.rol] })]
);
/**
* Registro de la obra ante una entidad de gestión.
*
* El `iswc` identifica la COMPOSICIÓN, así que se repite entre un tema y sus
* versiones: es la misma obra. El `isrc` identifica una GRABACIÓN, y por eso va
* en la fila de cada versión. Tenerlos juntos en la canción era un fallo del
* modelo anterior, que se veía en cuanto un tema tenía dos grabaciones.
*/
export const registroObra = pgTable('registro_obra', {
cancionId: text('cancion_id')
.primaryKey()
.references(() => cancion.id, { onDelete: 'cascade' }),
iswc: text('iswc'),
isrc: text('isrc'),
entidad: text('entidad'),
numero: text('numero'),
/** No es lo mismo que el sello discográfico. */
editorial: text('editorial')
});
/**
* El reparto de autoría, que es como lo liquidan las entidades de gestión.
*
* `numeric(5,2)` y no entero: los repartos reales llevan decimales. Y suma 100
* por obra, comprobado con una restricción en la migración y no solo al validar
* el formulario, porque un reparto que no suma cien no es un dato incompleto:
* es dinero mal repartido.
*/
export const autoria = pgTable(
'autoria',
{
cancionId: text('cancion_id')
.notNull()
.references(() => cancion.id, { onDelete: 'cascade' }),
personaId: text('persona_id')
.notNull()
.references(() => persona.id),
rol: text('rol', { enum: ['letra', 'musica', 'letra y musica'] }).notNull(),
porcentaje: numeric('porcentaje', { precision: 5, scale: 2 }).notNull()
},
(t) => [primaryKey({ columns: [t.cancionId, t.personaId, t.rol] })]
);
/** Un vídeo del tema en YouTube. Se guarda el id, no la URL: la URL cambia. */
export const video = pgTable(
'video',
{
id: id(),
cancionId: text('cancion_id')
.notNull()
.references(() => cancion.id, { onDelete: 'cascade' }),
youtubeId: text('youtube_id').notNull(),
titulo: text('titulo').notNull(),
orden: integer('orden').notNull().default(0)
},
(t) => [index('video_cancion').on(t.cancionId)]
);
export const PLATAFORMAS = [
'spotify',
'appleMusic',
'youtube',
'youtubeMusic',
'bandcamp',
'soundcloud',
'deezer',
'tidal',
'amazonMusic'
] as const;
/*
* Dos tablas de enlaces y no una polimórfica con (tipo, id): una clave foránea
* de verdad vale más que ahorrarse una tabla.
*/
export const cancionEnlace = pgTable(
'cancion_enlace',
{
cancionId: text('cancion_id')
.notNull()
.references(() => cancion.id, { onDelete: 'cascade' }),
plataforma: text('plataforma', { enum: PLATAFORMAS }).notNull(),
url: text('url').notNull()
},
(t) => [primaryKey({ columns: [t.cancionId, t.plataforma] })]
);
export const albumEnlace = pgTable(
'album_enlace',
{
albumId: text('album_id')
.notNull()
.references(() => album.id, { onDelete: 'cascade' }),
plataforma: text('plataforma', { enum: PLATAFORMAS }).notNull(),
url: text('url').notNull()
},
(t) => [primaryKey({ columns: [t.albumId, t.plataforma] })]
);
/** Alguien acreditado en la edición de un disco: producción, portada, mezcla. */
export const creditoAlbum = pgTable(
'credito_album',
{
id: id(),
albumId: text('album_id')
.notNull()
.references(() => album.id, { onDelete: 'cascade' }),
personaId: text('persona_id').references(() => persona.id),
/** Si no hay ficha de persona, al menos el nombre. */
nombre: text('nombre'),
rol: text('rol').notNull(),
orden: integer('orden').notNull().default(0)
},
(t) => [index('credito_album_album').on(t.albumId)]
);
/**
* Etiquetas: lo que cruza el catálogo por donde no lo hacen el estilo ni el
* disco.
*
* Antes se deducían del texto libre de cada archivo, así que «desamor» y
* «Desamor» eran dos etiquetas distintas y nadie lo veía. Ahora el vocabulario
* es una tabla, y lo comparten las canciones y la temática de los géneros: eso
* es lo que permite cruzar «el desamor: nueve temas míos y cuatro géneros que
* viven de él».
*/
export const etiqueta = pgTable('etiqueta', {
id: id(),
slug: text('slug').notNull().unique(),
/** Como se escribió la primera vez; es lo que se lee en pantalla. */
nombre: text('nombre').notNull()
});
export const cancionEtiqueta = pgTable(
'cancion_etiqueta',
{
cancionId: text('cancion_id')
.notNull()
.references(() => cancion.id, { onDelete: 'cascade' }),
etiquetaId: text('etiqueta_id')
.notNull()
.references(() => etiqueta.id, { onDelete: 'cascade' }),
/*
* El orden en que se escribieron, que no es el alfabético y no es
* casual: la primera etiqueta de un tema es de lo que va, y las de
* después matizan. Sin esta columna, la ficha las reordenaba sola.
*/
orden: integer('orden').notNull().default(0)
},
(t) => [primaryKey({ columns: [t.cancionId, t.etiquetaId] })]
);

Powered by TurnKey Linux.