meteor_detection_system/meteor-web-backend/migrations/1766209603920_create-missing-tables.js
grabbit f557c06771 feat: complete database schema migration to UUID primary keys
## Database Migrations (18 new)
- Migrate all primary keys from SERIAL to UUID
- Add soft delete (deleted_at) to all 19 entities
- Add missing indexes for performance optimization
- Add CHECK constraints for data validation
- Add user audit fields (last_login_at, timezone, locale)
- Add weather station location fields (latitude, longitude, elevation)
- Add foreign key relationships (CameraDevice→Device, ValidatedEvent→WeatherStation)
- Prepare private key encryption fields

## Backend Entity Updates
- All entities updated with UUID primary keys
- Added @DeleteDateColumn for soft delete support
- Updated relations and foreign key types

## Backend Service/Controller Updates
- Changed ID parameters from number to string (UUID)
- Removed ParseIntPipe from controllers
- Updated TypeORM queries for string IDs

## Frontend Updates
- Updated all service interfaces to use string IDs
- Fixed CameraDevice.location as JSONB object
- Updated weather.ts with new fields (elevation, timezone)
- Added Supabase integration hooks and lib
- Fixed chart components for new data structure

## Cleanup
- Removed deprecated .claude/agents configuration files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 03:33:26 +08:00

157 lines
6.1 KiB
JavaScript

/**
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {
// 1. Create subscription_plans table
pgm.createTable('subscription_plans', {
id: { type: 'serial', primaryKey: true },
plan_id: { type: 'varchar(50)', notNull: true, unique: true },
name: { type: 'varchar(100)', notNull: true },
description: { type: 'text' },
price: { type: 'decimal(10,2)', notNull: true },
currency: { type: 'varchar(10)', default: "'CNY'" },
interval: { type: 'varchar(20)', notNull: true },
interval_count: { type: 'integer', default: 1 },
stripe_price_id: { type: 'varchar(100)', unique: true },
features: { type: 'jsonb' },
is_popular: { type: 'boolean', default: false },
is_active: { type: 'boolean', default: true },
created_at: { type: 'timestamptz', default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', default: pgm.func('NOW()') },
});
// 2. Create user_subscriptions table
pgm.createTable('user_subscriptions', {
id: { type: 'serial', primaryKey: true },
user_profile_id: {
type: 'uuid',
notNull: true,
references: 'user_profiles(id)',
onDelete: 'CASCADE',
},
subscription_plan_id: {
type: 'integer',
notNull: true,
references: 'subscription_plans(id)',
onDelete: 'CASCADE',
},
stripe_subscription_id: { type: 'varchar(100)', unique: true },
status: { type: 'varchar(20)', notNull: true, default: "'active'" },
current_period_start: { type: 'timestamptz' },
current_period_end: { type: 'timestamptz' },
cancel_at_period_end: { type: 'boolean', default: false },
canceled_at: { type: 'timestamptz' },
trial_start: { type: 'timestamptz' },
trial_end: { type: 'timestamptz' },
created_at: { type: 'timestamptz', default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', default: pgm.func('NOW()') },
});
// 3. Create subscription_history table
pgm.createTable('subscription_history', {
id: { type: 'serial', primaryKey: true },
user_subscription_id: {
type: 'integer',
notNull: true,
references: 'user_subscriptions(id)',
onDelete: 'CASCADE',
},
action: { type: 'varchar(50)', notNull: true },
old_status: { type: 'varchar(20)' },
new_status: { type: 'varchar(20)' },
metadata: { type: 'jsonb' },
created_at: { type: 'timestamptz', default: pgm.func('NOW()') },
});
// 4. Create payment_records table
pgm.createTable('payment_records', {
id: { type: 'serial', primaryKey: true },
user_subscription_id: {
type: 'integer',
notNull: true,
references: 'user_subscriptions(id)',
onDelete: 'CASCADE',
},
stripe_payment_intent_id: { type: 'varchar(100)', unique: true },
amount: { type: 'decimal(10,2)', notNull: true },
currency: { type: 'varchar(10)', notNull: true },
status: { type: 'varchar(20)', notNull: true },
payment_method: { type: 'varchar(50)' },
failure_reason: { type: 'text' },
paid_at: { type: 'timestamptz' },
created_at: { type: 'timestamptz', default: pgm.func('NOW()') },
});
// 5. Create camera_devices table
pgm.createTable('camera_devices', {
id: { type: 'serial', primaryKey: true },
device_id: { type: 'varchar(100)', notNull: true, unique: true },
name: { type: 'varchar(100)', notNull: true },
location: { type: 'varchar(200)', notNull: true },
status: { type: 'varchar(20)', default: "'offline'" },
last_seen_at: { type: 'timestamptz' },
temperature: { type: 'decimal(5,2)' },
cooler_power: { type: 'decimal(5,2)' },
gain: { type: 'integer' },
exposure_count: { type: 'integer', default: 0 },
uptime: { type: 'decimal(10,2)' },
firmware_version: { type: 'varchar(50)' },
serial_number: { type: 'varchar(100)', unique: true },
created_at: { type: 'timestamptz', default: pgm.func('NOW()') },
updated_at: { type: 'timestamptz', default: pgm.func('NOW()') },
});
// 6. Create weather_observations table
pgm.createTable('weather_observations', {
id: { type: 'serial', primaryKey: true },
weather_station_id: {
type: 'integer',
notNull: true,
references: 'weather_stations(id)',
onDelete: 'CASCADE',
},
observation_time: { type: 'timestamptz', notNull: true },
temperature: { type: 'decimal(5,2)', notNull: true },
humidity: { type: 'decimal(5,2)', notNull: true },
cloud_cover: { type: 'decimal(5,2)', notNull: true },
visibility: { type: 'decimal(6,2)', notNull: true },
wind_speed: { type: 'decimal(5,2)', notNull: true },
wind_direction: { type: 'integer', notNull: true },
condition: { type: 'varchar(50)', notNull: true },
observation_quality: { type: 'varchar(20)', notNull: true },
pressure: { type: 'decimal(7,2)', notNull: true },
precipitation: { type: 'decimal(5,2)', notNull: true },
created_at: { type: 'timestamptz', default: pgm.func('NOW()') },
});
// Create indexes for foreign keys
pgm.createIndex('user_subscriptions', 'user_profile_id');
pgm.createIndex('user_subscriptions', 'subscription_plan_id');
pgm.createIndex('subscription_history', 'user_subscription_id');
pgm.createIndex('payment_records', 'user_subscription_id');
pgm.createIndex('weather_observations', 'weather_station_id');
pgm.createIndex('weather_observations', 'observation_time');
};
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {
// Drop tables in reverse order due to foreign key constraints
pgm.dropTable('weather_observations', { ifExists: true, cascade: true });
pgm.dropTable('camera_devices', { ifExists: true, cascade: true });
pgm.dropTable('payment_records', { ifExists: true, cascade: true });
pgm.dropTable('subscription_history', { ifExists: true, cascade: true });
pgm.dropTable('user_subscriptions', { ifExists: true, cascade: true });
pgm.dropTable('subscription_plans', { ifExists: true, cascade: true });
};