Add full Scryfall bulk data pipeline:
- Migration: 13 new columns on `cards` (oracle_id, illustration_id,
color_identity, keywords, legalities, flavor_text, artist, released_at,
layout, edhrec_rank, reserved, reprint, finishes) with GIN indexes
for JSONB search.
- Migration: `tags` + `card_tags` tables for Tagger community data.
- Script: `bulk-import-scryfall.js` — downloads Oracle Cards bulk file
(168 MB) and upserts all 36k+ MTG cards with rich metadata.
- Script: `import-scryfall-tags.js` — imports oracle tags (4.5k tags,
227k taggings) and art tags (11k tags, 458k taggings).
- Lib: `bulk-sync.js` — runtime bulk sync callable from the admin API.
- Admin UI: mode toggle (incremental vs bulk) on catalog sync panel.
Enables Commander deck validation (color_identity), format legality
checks, keyword search, EDHREC popularity ranking, and functional
card tagging ("removal", "ramp", "draw") for deck building assistance.
Co-authored-by: Cursor <cursoragent@cursor.com>
53 lines
2.1 KiB
JavaScript
53 lines
2.1 KiB
JavaScript
/**
|
|
* Create tables for Scryfall Tagger community tags (oracle + art).
|
|
* Tags join to cards via oracle_id (oracle tags) or illustration_id (art tags).
|
|
*/
|
|
export const shorthands = undefined;
|
|
|
|
export const up = (pgm) => {
|
|
pgm.createTable('tags', {
|
|
id: { type: 'uuid', primaryKey: true },
|
|
slug: { type: 'varchar(255)', notNull: true },
|
|
label: { type: 'varchar(255)', notNull: true },
|
|
type: { type: 'varchar(20)', notNull: true, comment: 'oracle or illustration' },
|
|
description: { type: 'text' },
|
|
parent_ids: { type: 'jsonb' },
|
|
child_ids: { type: 'jsonb' },
|
|
aliases: { type: 'jsonb' },
|
|
created_at: { type: 'timestamp', default: pgm.func('CURRENT_TIMESTAMP') },
|
|
updated_at: { type: 'timestamp', default: pgm.func('CURRENT_TIMESTAMP') },
|
|
});
|
|
|
|
pgm.createIndex('tags', 'type');
|
|
pgm.createIndex('tags', 'slug');
|
|
pgm.createIndex('tags', 'label');
|
|
|
|
pgm.createTable('card_tags', {
|
|
id: { type: 'serial', primaryKey: true },
|
|
tag_id: { type: 'uuid', notNull: true, references: 'tags(id)', onDelete: 'CASCADE' },
|
|
card_id: { type: 'integer', references: 'cards(id)', onDelete: 'CASCADE' },
|
|
oracle_id: { type: 'varchar(36)', comment: 'For oracle tags — matches cards.oracle_id' },
|
|
illustration_id: { type: 'varchar(36)', comment: 'For art tags — matches cards.illustration_id' },
|
|
weight: { type: 'varchar(20)', default: 'median' },
|
|
annotation: { type: 'text' },
|
|
created_at: { type: 'timestamp', default: pgm.func('CURRENT_TIMESTAMP') },
|
|
});
|
|
|
|
pgm.createIndex('card_tags', 'tag_id');
|
|
pgm.createIndex('card_tags', 'card_id');
|
|
pgm.createIndex('card_tags', 'oracle_id');
|
|
pgm.createIndex('card_tags', 'illustration_id');
|
|
pgm.addConstraint('card_tags', 'card_tags_unique_tag_oracle', {
|
|
unique: ['tag_id', 'oracle_id'],
|
|
where: 'oracle_id IS NOT NULL',
|
|
});
|
|
pgm.addConstraint('card_tags', 'card_tags_unique_tag_illustration', {
|
|
unique: ['tag_id', 'illustration_id'],
|
|
where: 'illustration_id IS NOT NULL',
|
|
});
|
|
};
|
|
|
|
export const down = (pgm) => {
|
|
pgm.dropTable('card_tags');
|
|
pgm.dropTable('tags');
|
|
};
|