## 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>
157 lines
6.1 KiB
JavaScript
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 });
|
|
};
|