Initial commit: Ham Radio Award Portal
Features implemented: - User authentication (register/login) with JWT - SQLite database with Drizzle ORM - SvelteKit frontend with authentication flow - ElysiaJS backend with CORS enabled - Award definition JSON schemas (DXCC, WAS, VUCC, SAT) - Responsive dashboard with user profile Tech stack: - Backend: ElysiaJS, Drizzle ORM, SQLite, JWT - Frontend: SvelteKit, Svelte stores - Runtime: Bun - Language: JavaScript Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
146
src/backend/db/schema/index.js
Normal file
146
src/backend/db/schema/index.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
/**
|
||||
* @typedef {Object} User
|
||||
* @property {number} id
|
||||
* @property {string} email
|
||||
* @property {string} passwordHash
|
||||
* @property {string} callsign
|
||||
* @property {string|null} lotwUsername
|
||||
* @property {string|null} lotwPassword
|
||||
* @property {Date} createdAt
|
||||
* @property {Date} updatedAt
|
||||
*/
|
||||
|
||||
export const users = sqliteTable('users', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
email: text('email').notNull().unique(),
|
||||
passwordHash: text('password_hash').notNull(),
|
||||
callsign: text('callsign').notNull(),
|
||||
lotwUsername: text('lotw_username'),
|
||||
lotwPassword: text('lotw_password'), // Encrypted
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} QSO
|
||||
* @property {number} id
|
||||
* @property {number} userId
|
||||
* @property {string} callsign
|
||||
* @property {string} qsoDate
|
||||
* @property {string} timeOn
|
||||
* @property {string|null} band
|
||||
* @property {string|null} mode
|
||||
* @property {number|null} freq
|
||||
* @property {number|null} freqRx
|
||||
* @property {string|null} entity
|
||||
* @property {number|null} entityId
|
||||
* @property {string|null} grid
|
||||
* @property {string|null} gridSource
|
||||
* @property {string|null} continent
|
||||
* @property {number|null} cqZone
|
||||
* @property {number|null} ituZone
|
||||
* @property {string|null} state
|
||||
* @property {string|null} county
|
||||
* @property {string|null} satName
|
||||
* @property {string|null} satMode
|
||||
* @property {string|null} lotwQslRdate
|
||||
* @property {string|null} lotwQslRstatus
|
||||
* @property {Date|null} lotwSyncedAt
|
||||
* @property {Date} createdAt
|
||||
*/
|
||||
|
||||
export const qsos = sqliteTable('qsos', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id').notNull().references(() => users.id),
|
||||
|
||||
// QSO fields
|
||||
callsign: text('callsign').notNull(),
|
||||
qsoDate: text('qso_date').notNull(), // ADIF format: YYYYMMDD
|
||||
timeOn: text('time_on').notNull(), // HHMMSS
|
||||
band: text('band'), // 160m, 80m, 40m, etc.
|
||||
mode: text('mode'), // CW, SSB, FT8, etc.
|
||||
freq: integer('freq'), // Frequency in Hz
|
||||
freqRx: integer('freq_rx'), // RX frequency (satellite)
|
||||
|
||||
// Entity/location fields
|
||||
entity: text('entity'), // DXCC entity name
|
||||
entityId: integer('entity_id'), // DXCC entity number
|
||||
grid: text('grid'), // Maidenhead grid square
|
||||
gridSource: text('grid_source'), // LOTW, USER, CALC
|
||||
continent: text('continent'), // NA, SA, EU, AF, AS, OC, AN
|
||||
cqZone: integer('cq_zone'),
|
||||
ituZone: integer('itu_zone'),
|
||||
state: text('state'), // US state, CA province, etc.
|
||||
county: text('county'),
|
||||
|
||||
// Satellite fields
|
||||
satName: text('sat_name'),
|
||||
satMode: text('sat_mode'),
|
||||
|
||||
// LoTW confirmation
|
||||
lotwQslRdate: text('lotw_qsl_rdate'), // Confirmation date
|
||||
lotwQslRstatus: text('lotw_qsl_rstatus'), // 'Y', 'N', '?'
|
||||
|
||||
// Cache metadata
|
||||
lotwSyncedAt: integer('lotw_synced_at', { mode: 'timestamp' }),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} Award
|
||||
* @property {string} id
|
||||
* @property {string} name
|
||||
* @property {string|null} description
|
||||
* @property {string} definition
|
||||
* @property {boolean} isActive
|
||||
* @property {Date} createdAt
|
||||
*/
|
||||
|
||||
export const awards = sqliteTable('awards', {
|
||||
id: text('id').primaryKey(), // 'dxcc', 'was', 'vucc'
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
definition: text('definition').notNull(), // JSON rule definition
|
||||
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} AwardProgress
|
||||
* @property {number} id
|
||||
* @property {number} userId
|
||||
* @property {string} awardId
|
||||
* @property {number} workedCount
|
||||
* @property {number} confirmedCount
|
||||
* @property {number} totalRequired
|
||||
* @property {string|null} workedEntities
|
||||
* @property {string|null} confirmedEntities
|
||||
* @property {Date|null} lastCalculatedAt
|
||||
* @property {Date|null} lastQsoSyncAt
|
||||
* @property {Date} updatedAt
|
||||
*/
|
||||
|
||||
export const awardProgress = sqliteTable('award_progress', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id').notNull().references(() => users.id),
|
||||
awardId: text('award_id').notNull().references(() => awards.id),
|
||||
|
||||
// Calculated progress
|
||||
workedCount: integer('worked_count').notNull().default(0),
|
||||
confirmedCount: integer('confirmed_count').notNull().default(0),
|
||||
totalRequired: integer('total_required').notNull(),
|
||||
|
||||
// Detailed breakdown (JSON)
|
||||
workedEntities: text('worked_entities'), // JSON array
|
||||
confirmedEntities: text('confirmed_entities'), // JSON array
|
||||
|
||||
// Cache control
|
||||
lastCalculatedAt: integer('last_calculated_at', { mode: 'timestamp' }),
|
||||
lastQsoSyncAt: integer('last_qso_sync_at', { mode: 'timestamp' }),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
// Export all schemas
|
||||
export const schema = { users, qsos, awards, awardProgress };
|
||||
Reference in New Issue
Block a user