refactor: unify schema - eliminate core_* tables
BREAKING CHANGE: Removed core_companies, core_users, core_user_roles tables Migrations: - Create 020_unify_schema.sql: adds tenant_id, email, name to users table - Create user_roles table (replaces core_user_roles) - Disable 009_create_core_tables.sql (renamed to .disabled) - Update 010_seed_super_admin.sql to use unified tables Backend Repositories: - company_repository.go: use companies table with INT id - user_repository.go: use users/user_roles with INT id conversion Seeders: - All seeders now use companies/users/user_roles tables - Removed all core_* table insertions - Query companies by slug to get SERIAL id This eliminates the redundancy between core_* and legacy tables.
This commit is contained in:
parent
af2719f2e6
commit
7d99e77468
11 changed files with 269 additions and 210 deletions
|
|
@ -4,9 +4,10 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rede5/gohorsejobs/backend/internal/core/domain/entity"
|
||||
)
|
||||
|
||||
|
|
@ -19,70 +20,97 @@ func NewCompanyRepository(db *sql.DB) *CompanyRepository {
|
|||
}
|
||||
|
||||
func (r *CompanyRepository) Save(ctx context.Context, company *entity.Company) (*entity.Company, error) {
|
||||
if company.ID == "" {
|
||||
company.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
// companies table uses SERIAL id, we let DB generate it
|
||||
query := `
|
||||
INSERT INTO core_companies (id, name, document, contact, status, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO companies (name, slug, type, document, email, description, verified, active, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
slug := company.Name // TODO: slugify function
|
||||
var id int
|
||||
err := r.db.QueryRowContext(ctx, query,
|
||||
company.ID,
|
||||
company.Name,
|
||||
slug,
|
||||
"company",
|
||||
company.Document,
|
||||
company.Contact,
|
||||
company.Status,
|
||||
company.Contact, // mapped to email
|
||||
"{}", // description as JSON
|
||||
true, // verified
|
||||
company.Status == "ACTIVE",
|
||||
company.CreatedAt,
|
||||
company.UpdatedAt,
|
||||
).Scan(&company.ID)
|
||||
).Scan(&id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
company.ID = strconv.Itoa(id)
|
||||
return company, nil
|
||||
}
|
||||
|
||||
func (r *CompanyRepository) FindByID(ctx context.Context, id string) (*entity.Company, error) {
|
||||
query := `SELECT id, name, document, contact, status, created_at, updated_at FROM core_companies WHERE id = $1`
|
||||
row := r.db.QueryRowContext(ctx, query, id)
|
||||
query := `SELECT id, name, document, email,
|
||||
CASE WHEN active THEN 'ACTIVE' ELSE 'INACTIVE' END as status,
|
||||
created_at, updated_at
|
||||
FROM companies WHERE id = $1`
|
||||
|
||||
numID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid company id: %s", id)
|
||||
}
|
||||
|
||||
row := r.db.QueryRowContext(ctx, query, numID)
|
||||
|
||||
c := &entity.Company{}
|
||||
err := row.Scan(&c.ID, &c.Name, &c.Document, &c.Contact, &c.Status, &c.CreatedAt, &c.UpdatedAt)
|
||||
var dbID int
|
||||
err = row.Scan(&dbID, &c.Name, &c.Document, &c.Contact, &c.Status, &c.CreatedAt, &c.UpdatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
c.ID = strconv.Itoa(dbID)
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (r *CompanyRepository) Update(ctx context.Context, company *entity.Company) (*entity.Company, error) {
|
||||
company.UpdatedAt = time.Now()
|
||||
|
||||
numID, err := strconv.Atoi(company.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid company id: %s", company.ID)
|
||||
}
|
||||
|
||||
query := `
|
||||
UPDATE core_companies
|
||||
SET name=$1, document=$2, contact=$3, status=$4, updated_at=$5
|
||||
UPDATE companies
|
||||
SET name=$1, document=$2, email=$3, active=$4, updated_at=$5
|
||||
WHERE id=$6
|
||||
`
|
||||
_, err := r.db.ExecContext(ctx, query,
|
||||
_, err = r.db.ExecContext(ctx, query,
|
||||
company.Name,
|
||||
company.Document,
|
||||
company.Contact,
|
||||
company.Status,
|
||||
company.Status == "ACTIVE",
|
||||
company.UpdatedAt,
|
||||
company.ID,
|
||||
numID,
|
||||
)
|
||||
return company, err
|
||||
}
|
||||
|
||||
func (r *CompanyRepository) Delete(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM core_companies WHERE id = $1`
|
||||
_, err := r.db.ExecContext(ctx, query, id)
|
||||
numID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid company id: %s", id)
|
||||
}
|
||||
query := `DELETE FROM companies WHERE id = $1`
|
||||
_, err = r.db.ExecContext(ctx, query, numID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CompanyRepository) FindAll(ctx context.Context) ([]*entity.Company, error) {
|
||||
query := `SELECT id, name, document, contact, status, created_at, updated_at FROM core_companies`
|
||||
query := `SELECT id, name, document, email,
|
||||
CASE WHEN active THEN 'ACTIVE' ELSE 'INACTIVE' END as status,
|
||||
created_at, updated_at
|
||||
FROM companies`
|
||||
rows, err := r.db.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -92,9 +120,11 @@ func (r *CompanyRepository) FindAll(ctx context.Context) ([]*entity.Company, err
|
|||
var companies []*entity.Company
|
||||
for rows.Next() {
|
||||
c := &entity.Company{}
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Document, &c.Contact, &c.Status, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
||||
var dbID int
|
||||
if err := rows.Scan(&dbID, &c.Name, &c.Document, &c.Contact, &c.Status, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.ID = strconv.Itoa(dbID)
|
||||
companies = append(companies, c)
|
||||
}
|
||||
return companies, nil
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ package postgres
|
|||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rede5/gohorsejobs/backend/internal/core/domain/entity"
|
||||
)
|
||||
|
||||
|
|
@ -19,47 +19,58 @@ func NewUserRepository(db *sql.DB) *UserRepository {
|
|||
}
|
||||
|
||||
func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.User, error) {
|
||||
if user.ID == "" {
|
||||
user.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Serialize metadata
|
||||
metadata, err := json.Marshal(user.Metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Convert tenant_id from string to int
|
||||
var tenantID *int
|
||||
if user.TenantID != "" {
|
||||
tid, err := strconv.Atoi(user.TenantID)
|
||||
if err == nil {
|
||||
tenantID = &tid
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Insert User
|
||||
// 1. Insert User - users table has SERIAL id
|
||||
query := `
|
||||
INSERT INTO core_users (id, tenant_id, name, email, password_hash, status, metadata, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
INSERT INTO users (identifier, password_hash, role, full_name, email, name, tenant_id, status, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id
|
||||
`
|
||||
_, err = tx.ExecContext(ctx, query,
|
||||
user.ID,
|
||||
user.TenantID,
|
||||
|
||||
var id int
|
||||
// Map the first role to the role column, default to 'jobSeeker'
|
||||
role := "jobSeeker"
|
||||
if len(user.Roles) > 0 {
|
||||
role = user.Roles[0].Name
|
||||
}
|
||||
|
||||
err = tx.QueryRowContext(ctx, query,
|
||||
user.Email, // identifier = email for now
|
||||
user.PasswordHash,
|
||||
role,
|
||||
user.Name,
|
||||
user.Email,
|
||||
user.PasswordHash,
|
||||
user.Name,
|
||||
tenantID,
|
||||
user.Status,
|
||||
metadata,
|
||||
user.CreatedAt,
|
||||
user.UpdatedAt,
|
||||
)
|
||||
).Scan(&id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.ID = strconv.Itoa(id)
|
||||
|
||||
// 2. Insert Roles
|
||||
// 2. Insert Roles into user_roles table
|
||||
if len(user.Roles) > 0 {
|
||||
roleQuery := `INSERT INTO core_user_roles (user_id, role) VALUES ($1, $2)`
|
||||
roleQuery := `INSERT INTO user_roles (user_id, role) VALUES ($1, $2) ON CONFLICT DO NOTHING`
|
||||
for _, role := range user.Roles {
|
||||
_, err := tx.ExecContext(ctx, roleQuery, user.ID, role.Name)
|
||||
_, err := tx.ExecContext(ctx, roleQuery, id, role.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -74,49 +85,66 @@ func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.U
|
|||
}
|
||||
|
||||
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entity.User, error) {
|
||||
query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at FROM core_users WHERE email = $1`
|
||||
query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''),
|
||||
COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at
|
||||
FROM users WHERE email = $1 OR identifier = $1`
|
||||
row := r.db.QueryRowContext(ctx, query, email)
|
||||
|
||||
u := &entity.User{}
|
||||
err := row.Scan(&u.ID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt)
|
||||
var dbID int
|
||||
err := row.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil // Return nil if not found
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u.Roles, _ = r.getRoles(ctx, u.ID)
|
||||
u.ID = strconv.Itoa(dbID)
|
||||
u.Roles, _ = r.getRoles(ctx, dbID)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByID(ctx context.Context, id string) (*entity.User, error) {
|
||||
query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at FROM core_users WHERE id = $1`
|
||||
row := r.db.QueryRowContext(ctx, query, id)
|
||||
numID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid user id: %s", id)
|
||||
}
|
||||
|
||||
query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''),
|
||||
COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at
|
||||
FROM users WHERE id = $1`
|
||||
row := r.db.QueryRowContext(ctx, query, numID)
|
||||
|
||||
u := &entity.User{}
|
||||
err := row.Scan(&u.ID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt)
|
||||
var dbID int
|
||||
err = row.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u.Roles, _ = r.getRoles(ctx, u.ID)
|
||||
u.ID = strconv.Itoa(dbID)
|
||||
u.Roles, _ = r.getRoles(ctx, dbID)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, limit, offset int) ([]*entity.User, int, error) {
|
||||
numTenantID, err := strconv.Atoi(tenantID)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("invalid tenant id: %s", tenantID)
|
||||
}
|
||||
|
||||
var total int
|
||||
countQuery := `SELECT COUNT(*) FROM core_users WHERE tenant_id = $1`
|
||||
if err := r.db.QueryRowContext(ctx, countQuery, tenantID).Scan(&total); err != nil {
|
||||
countQuery := `SELECT COUNT(*) FROM users WHERE tenant_id = $1`
|
||||
if err := r.db.QueryRowContext(ctx, countQuery, numTenantID).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at
|
||||
FROM core_users
|
||||
query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''),
|
||||
COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at
|
||||
FROM users
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3`
|
||||
rows, err := r.db.QueryContext(ctx, query, tenantID, limit, offset)
|
||||
rows, err := r.db.QueryContext(ctx, query, numTenantID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
|
@ -125,31 +153,40 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, l
|
|||
var users []*entity.User
|
||||
for rows.Next() {
|
||||
u := &entity.User{}
|
||||
if err := rows.Scan(&u.ID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt); err != nil {
|
||||
var dbID int
|
||||
if err := rows.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// Populate roles N+1? Ideally join, but for now simple
|
||||
u.Roles, _ = r.getRoles(ctx, u.ID)
|
||||
u.ID = strconv.Itoa(dbID)
|
||||
u.Roles, _ = r.getRoles(ctx, dbID)
|
||||
users = append(users, u)
|
||||
}
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity.User, error) {
|
||||
// Not fully implemented for roles update for brevity, just fields
|
||||
numID, err := strconv.Atoi(user.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid user id: %s", user.ID)
|
||||
}
|
||||
|
||||
user.UpdatedAt = time.Now()
|
||||
query := `UPDATE core_users SET name=$1, email=$2, status=$3, updated_at=$4 WHERE id=$5`
|
||||
_, err := r.db.ExecContext(ctx, query, user.Name, user.Email, user.Status, user.UpdatedAt, user.ID)
|
||||
query := `UPDATE users SET name=$1, email=$2, status=$3, updated_at=$4 WHERE id=$5`
|
||||
_, err = r.db.ExecContext(ctx, query, user.Name, user.Email, user.Status, user.UpdatedAt, numID)
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) Delete(ctx context.Context, id string) error {
|
||||
_, err := r.db.ExecContext(ctx, `DELETE FROM core_users WHERE id=$1`, id)
|
||||
numID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid user id: %s", id)
|
||||
}
|
||||
_, err = r.db.ExecContext(ctx, `DELETE FROM users WHERE id=$1`, numID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *UserRepository) getRoles(ctx context.Context, userID string) ([]entity.Role, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `SELECT role FROM core_user_roles WHERE user_id = $1`, userID)
|
||||
func (r *UserRepository) getRoles(ctx context.Context, userID int) ([]entity.Role, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `SELECT role FROM user_roles WHERE user_id = $1`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,39 @@
|
|||
-- Migration: Create Super Admin and System Tenant
|
||||
-- Description: Inserts the default System Tenant and Super Admin user.
|
||||
-- Use fixed UUIDs for reproducibility in dev environments.
|
||||
-- Migration: Create Super Admin and System Company
|
||||
-- Description: Inserts the default System Company and Super Admin user.
|
||||
-- Uses unified tables (companies, users, user_roles)
|
||||
|
||||
-- 1. Insert System Tenant
|
||||
INSERT INTO core_companies (id, name, document, contact, status, created_at, updated_at)
|
||||
-- 1. Insert System Company (for SuperAdmin context)
|
||||
INSERT INTO companies (name, slug, type, document, email, description, verified, active)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000001', -- Fixed System Tenant ID
|
||||
'System Tenant (SuperAdmin Context)',
|
||||
'SYSTEM',
|
||||
'admin@system.local',
|
||||
'ACTIVE',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
'GoHorse System',
|
||||
'gohorse-system',
|
||||
'system',
|
||||
'00.000.000/0001-91',
|
||||
'admin@gohorsejobs.com',
|
||||
'{"tagline": "System Administration Tenant"}',
|
||||
true,
|
||||
true
|
||||
) ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- 2. Insert Super Admin User
|
||||
-- WARNING: This hash is generated WITHOUT PASSWORD_PEPPER.
|
||||
-- For development only. Use seeder-api for proper user creation.
|
||||
-- Password: "password123" (BCrypt hash without pepper)
|
||||
INSERT INTO core_users (id, tenant_id, name, email, password_hash, status, created_at, updated_at)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000002', -- Fixed SuperAdmin User ID
|
||||
'00000000-0000-0000-0000-000000000001', -- Link to System Tenant
|
||||
-- Password: "Admin@2025!" (should be created by seeder with proper pepper)
|
||||
INSERT INTO users (identifier, password_hash, role, full_name, email, name, status, tenant_id)
|
||||
SELECT
|
||||
'superadmin',
|
||||
'$2a$10$UWrE9xN39lVagJHlXZsxwOVI3NRSEd1VJ6UzMblW6LOxNmsOZtj9K', -- placeholder, seeder will update
|
||||
'superadmin',
|
||||
'Super Administrator',
|
||||
'admin@todai.jobs',
|
||||
'$2a$10$UWrE9xN39lVagJHlXZsxwOVI3NRSEd1VJ6UzMblW6LOxNmsOZtj9K', -- "password123"
|
||||
'ACTIVE',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
'admin@gohorsejobs.com',
|
||||
'Super Administrator',
|
||||
'active',
|
||||
c.id
|
||||
FROM companies c WHERE c.slug = 'gohorse-system'
|
||||
ON CONFLICT (identifier) DO NOTHING;
|
||||
|
||||
-- 3. Assign Super Admin Role
|
||||
INSERT INTO core_user_roles (user_id, role)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000002',
|
||||
'SUPER_ADMIN'
|
||||
) ON CONFLICT (user_id, role) DO NOTHING;
|
||||
INSERT INTO user_roles (user_id, role)
|
||||
SELECT u.id, 'SUPER_ADMIN'
|
||||
FROM users u WHERE u.identifier = 'superadmin'
|
||||
ON CONFLICT (user_id, role) DO NOTHING;
|
||||
|
|
|
|||
33
backend/migrations/020_unify_schema.sql
Normal file
33
backend/migrations/020_unify_schema.sql
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
-- Migration: Unify schema - add missing fields to users table
|
||||
-- Description: Add fields from core_users to users table for unified architecture
|
||||
|
||||
-- Add tenant_id (references companies.id)
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS tenant_id INT REFERENCES companies(id);
|
||||
|
||||
-- Add email if not exists (core_users had this)
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS email VARCHAR(255);
|
||||
|
||||
-- Add name if not exists (mapped from full_name or separate)
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS name VARCHAR(255);
|
||||
|
||||
-- Add status field for compatibility
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'active';
|
||||
|
||||
-- Create user_roles table (replaces core_user_roles)
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
PRIMARY KEY (user_id, role)
|
||||
);
|
||||
|
||||
-- Index for user_roles
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_role ON user_roles(role);
|
||||
|
||||
-- Index for tenant lookup
|
||||
CREATE INDEX IF NOT EXISTS idx_users_tenant_id ON users(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
|
||||
-- Comments
|
||||
COMMENT ON COLUMN users.tenant_id IS 'Company ID this user belongs to (NULL for superadmin)';
|
||||
COMMENT ON TABLE user_roles IS 'Additional roles for users (e.g., admin, recruiter)';
|
||||
|
|
@ -139,21 +139,14 @@ export async function seedAcmeCorp() {
|
|||
'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Wile_E._Coyote_with_Acme_Rocket.svg/200px-Wile_E._Coyote_with_Acme_Rocket.svg.png'
|
||||
]);
|
||||
|
||||
// Create Core Company entry
|
||||
const acmeCoreId = 'acacacac-acac-acac-acac-acacacacacac'; // Memorable UUID
|
||||
await pool.query(`
|
||||
INSERT INTO core_companies (id, name, document, status)
|
||||
VALUES ($1, $2, $3, 'ACTIVE')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, [acmeCoreId, 'ACME Corporation', acmeCNPJ]);
|
||||
|
||||
console.log(' ✓ ACME Corporation criada');
|
||||
|
||||
// 2. Get ACME company ID (use coreId for FK)
|
||||
const acmeCompanyId = acmeCoreId;
|
||||
// 2. Get ACME company ID from companies table
|
||||
const acmeRes = await pool.query("SELECT id FROM companies WHERE slug = 'ACME Corporation'");
|
||||
const acmeCompanyId = acmeRes.rows[0]?.id;
|
||||
|
||||
// 3. Get seed user from core_users (for FK)
|
||||
const seedUserRes = await pool.query("SELECT id FROM core_users LIMIT 1");
|
||||
// 3. Get seed user from users table (for FK)
|
||||
const seedUserRes = await pool.query("SELECT id FROM users LIMIT 1");
|
||||
const seedUserId = seedUserRes.rows[0]?.id;
|
||||
|
||||
// 4. Seed 69 jobs
|
||||
|
|
|
|||
|
|
@ -87,24 +87,16 @@ export async function seedCompanies() {
|
|||
true,
|
||||
true
|
||||
]);
|
||||
|
||||
// Seed Core Company (One-to-one mapping)
|
||||
const coreId = crypto.randomUUID();
|
||||
await pool.query(`
|
||||
INSERT INTO core_companies (id, name, document, status)
|
||||
VALUES ($1, $2, $3, 'ACTIVE')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, [coreId, company.name, generateCNPJ(i)]);
|
||||
}
|
||||
|
||||
// Seed System Tenant for SuperAdmin
|
||||
// Seed System Company for SuperAdmin
|
||||
await pool.query(`
|
||||
INSERT INTO core_companies (id, name, document, status)
|
||||
VALUES ('00000000-0000-0000-0000-000000000000', 'GoHorse System', '00.000.000/0001-91', 'ACTIVE')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
INSERT INTO companies (name, slug, type, document, email, description, verified, active)
|
||||
VALUES ('GoHorse System', 'gohorse-system', 'system', '00.000.000/0001-91', 'admin@gohorsejobs.com', '{"tagline": "System Administration"}', true, true)
|
||||
ON CONFLICT (slug) DO NOTHING
|
||||
`);
|
||||
|
||||
console.log(` ✓ ${companyData.length} companies seeded (Legacy & Core)`);
|
||||
console.log(` ✓ ${companyData.length + 1} companies seeded`);
|
||||
} catch (error) {
|
||||
console.error(' ❌ Error seeding companies:', error.message);
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ async function createCompanyAndJobs(companyData, jobs) {
|
|||
const regionsRes = await pool.query('SELECT id FROM regions LIMIT 1');
|
||||
const defaultRegionId = regionsRes.rows[0]?.id || null;
|
||||
|
||||
// Create Company
|
||||
// Create Company (companies uses SERIAL id)
|
||||
await pool.query(`
|
||||
INSERT INTO companies (name, slug, type, document, address, region_id, phone, email, website, description, verified, active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
|
|
@ -283,17 +283,11 @@ async function createCompanyAndJobs(companyData, jobs) {
|
|||
true
|
||||
]);
|
||||
|
||||
// Core Company
|
||||
await pool.query(`
|
||||
INSERT INTO core_companies (id, name, document, status)
|
||||
VALUES ($1, $2, $3, 'ACTIVE')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, [companyData.coreId, companyData.name, companyData.cnpj]);
|
||||
// Get company ID from companies table (SERIAL id)
|
||||
const companyRes = await pool.query("SELECT id FROM companies WHERE slug = $1", [companyData.slug]);
|
||||
const companyId = companyRes.rows[0]?.id;
|
||||
|
||||
// Get company ID (use coreId for FK reference)
|
||||
const companyId = companyData.coreId;
|
||||
|
||||
const seedUserRes = await pool.query("SELECT id FROM core_users LIMIT 1");
|
||||
const seedUserRes = await pool.query("SELECT id FROM users LIMIT 1");
|
||||
const seedUserId = seedUserRes.rows[0]?.id || 1;
|
||||
|
||||
const workModes = ['onsite', 'hybrid', 'remote'];
|
||||
|
|
|
|||
|
|
@ -177,18 +177,12 @@ export async function seedStarkIndustries() {
|
|||
true
|
||||
]);
|
||||
|
||||
// Core Company
|
||||
await pool.query(`
|
||||
INSERT INTO core_companies (id, name, document, status)
|
||||
VALUES ($1, $2, $3, 'ACTIVE')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, ['57575757-5757-5757-5757-575757575757', 'Stark Industries', '77.777.777/0001-77']);
|
||||
|
||||
// Get company ID (use coreId directly)
|
||||
const companyId = '57575757-5757-5757-5757-575757575757';
|
||||
// Get company ID (SERIAL) from companies table
|
||||
const companyRes = await pool.query("SELECT id FROM companies WHERE slug = 'Stark Industries'");
|
||||
const companyId = companyRes.rows[0]?.id;
|
||||
|
||||
// Get seed user
|
||||
const seedUserRes = await pool.query("SELECT id FROM core_users LIMIT 1");
|
||||
const seedUserRes = await pool.query("SELECT id FROM users LIMIT 1");
|
||||
const seedUserId = seedUserRes.rows[0]?.id || 1;
|
||||
|
||||
// Create jobs
|
||||
|
|
@ -265,7 +259,7 @@ export async function seedLosPollosHermanos() {
|
|||
|
||||
// Core Company
|
||||
await pool.query(`
|
||||
INSERT INTO core_companies (id, name, document, status)
|
||||
INSERT INTO companies (id, name, document, status)
|
||||
VALUES ($1, $2, $3, 'ACTIVE')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, ['66666666-6666-6666-6666-666666666666', 'Los Pollos Hermanos', '66.666.666/0001-66']);
|
||||
|
|
@ -274,7 +268,7 @@ export async function seedLosPollosHermanos() {
|
|||
// Get company ID (use coreId directly)
|
||||
const companyId = '66666666-6666-6666-6666-666666666666';
|
||||
|
||||
const seedUserRes = await pool.query("SELECT id FROM core_users LIMIT 1");
|
||||
const seedUserRes = await pool.query("SELECT id FROM users LIMIT 1");
|
||||
const seedUserId = seedUserRes.rows[0]?.id || 1;
|
||||
|
||||
const benefits = ['Frango Grátis (apenas o legal)', 'Plano de Saúde Premium', 'Seguro de Vida Reforçado', 'Carro da Empresa (GPS desativado)'];
|
||||
|
|
@ -352,7 +346,7 @@ export async function seedSpringfieldNuclear() {
|
|||
|
||||
// Core Company
|
||||
await pool.query(`
|
||||
INSERT INTO core_companies (id, name, document, status)
|
||||
INSERT INTO companies (id, name, document, status)
|
||||
VALUES ($1, $2, $3, 'ACTIVE')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, ['88888888-8888-8888-8888-888888888888', 'Springfield Nuclear Power Plant', '88.888.888/0001-88']);
|
||||
|
|
@ -361,7 +355,7 @@ export async function seedSpringfieldNuclear() {
|
|||
// Get company ID (use coreId directly)
|
||||
const companyId = '88888888-8888-8888-8888-888888888888';
|
||||
|
||||
const seedUserRes = await pool.query("SELECT id FROM core_users LIMIT 1");
|
||||
const seedUserRes = await pool.query("SELECT id FROM users LIMIT 1");
|
||||
const seedUserId = seedUserRes.rows[0]?.id || 1;
|
||||
|
||||
const benefits = ['Tickets de Cafeteria', 'Dosímetro Pessoal', 'Desconto na Taverna do Moe', 'Estacionamento (longe do reator)'];
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ export async function seedNotifications() {
|
|||
console.log('🔔 Seeding notifications...');
|
||||
|
||||
try {
|
||||
// Get users from core_users
|
||||
const usersRes = await pool.query('SELECT id FROM core_users LIMIT 5');
|
||||
// Get users from users
|
||||
const usersRes = await pool.query('SELECT id FROM users LIMIT 5');
|
||||
const users = usersRes.rows;
|
||||
|
||||
if (users.length === 0) {
|
||||
|
|
|
|||
|
|
@ -1,110 +1,105 @@
|
|||
|
||||
import bcrypt from 'bcrypt';
|
||||
import crypto from 'crypto';
|
||||
import { pool } from '../db.js';
|
||||
|
||||
// Get pepper from environment - MUST match backend PASSWORD_PEPPER
|
||||
const PASSWORD_PEPPER = process.env.PASSWORD_PEPPER || '';
|
||||
|
||||
export async function seedUsers() {
|
||||
console.log('👤 Seeding users (Core Architecture)...');
|
||||
console.log('👤 Seeding users (Unified Architecture)...');
|
||||
|
||||
try {
|
||||
// Fetch core companies to map users
|
||||
const companiesResult = await pool.query('SELECT id, name FROM core_companies');
|
||||
// Fetch companies to map users (now using companies table, not core_companies)
|
||||
const companiesResult = await pool.query('SELECT id, name, slug FROM companies');
|
||||
const companyMap = {}; // name -> id
|
||||
companiesResult.rows.forEach(c => companyMap[c.name] = c.id);
|
||||
const systemTenantId = '00000000-0000-0000-0000-000000000000';
|
||||
|
||||
// Get system tenant ID
|
||||
const systemResult = await pool.query("SELECT id FROM companies WHERE slug = 'gohorse-system'");
|
||||
const systemTenantId = systemResult.rows[0]?.id || null;
|
||||
|
||||
// 1. Create SuperAdmin (Requested: superadmin / Admin@2025!)
|
||||
const superAdminPassword = await bcrypt.hash('Admin@2025!' + PASSWORD_PEPPER, 10);
|
||||
const superAdminId = crypto.randomUUID();
|
||||
// User requested identifier 'superadmin'
|
||||
const superAdminIdentifier = 'superadmin';
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO core_users (id, tenant_id, name, email, password_hash, status)
|
||||
VALUES ($1, $2, $3, $4, $5, 'ACTIVE')
|
||||
ON CONFLICT (tenant_id, email) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
||||
INSERT INTO users (identifier, password_hash, role, full_name, email, name, tenant_id, status)
|
||||
VALUES ($1, $2, 'superadmin', $3, $4, $5, $6, 'active')
|
||||
ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
||||
RETURNING id
|
||||
`, [superAdminId, systemTenantId, 'System Administrator', superAdminIdentifier, superAdminPassword]);
|
||||
`, ['superadmin', superAdminPassword, 'System Administrator', 'admin@gohorsejobs.com', 'System Administrator', systemTenantId]);
|
||||
|
||||
const actualSuperAdminId = result.rows[0].id;
|
||||
const superAdminId = result.rows[0].id;
|
||||
|
||||
// Role
|
||||
// Role in user_roles table
|
||||
await pool.query(`
|
||||
INSERT INTO core_user_roles (user_id, role)
|
||||
INSERT INTO user_roles (user_id, role)
|
||||
VALUES ($1, 'superadmin')
|
||||
ON CONFLICT (user_id, role) DO NOTHING
|
||||
`, [actualSuperAdminId]);
|
||||
`, [superAdminId]);
|
||||
|
||||
console.log(' ✓ SuperAdmin created (superadmin)');
|
||||
|
||||
// 2. Create Company Admins
|
||||
const companyAdmins = [
|
||||
// Requested: takeshi_yamamoto / Takeshi@2025 (Company Admin)
|
||||
{ identifier: 'takeshi_yamamoto', fullName: 'Takeshi Yamamoto', company: 'TechCorp', email: 'takeshi_yamamoto', pass: 'Takeshi@2025', roles: ['admin', 'company'] },
|
||||
{ identifier: 'takeshi_yamamoto', fullName: 'Takeshi Yamamoto', company: 'TechCorp', email: 'takeshi@techcorp.com', pass: 'Takeshi@2025', roles: ['admin', 'company'] },
|
||||
{ identifier: 'kenji', fullName: 'Kenji Tanaka', company: 'AppMakers', email: 'kenji@appmakers.mobile', pass: 'Takeshi@2025', roles: ['admin', 'company'] },
|
||||
// Requested: maria_santos / User@2025 (Recruiter) - Placing in DesignHub
|
||||
{ identifier: 'maria_santos', fullName: 'Maria Santos', company: 'DesignHub', email: 'maria_santos', pass: 'User@2025', roles: ['recruiter', 'company'] }
|
||||
{ identifier: 'maria_santos', fullName: 'Maria Santos', company: 'DesignHub', email: 'maria@designhub.com', pass: 'User@2025', roles: ['recruiter', 'company'] }
|
||||
];
|
||||
|
||||
for (const admin of companyAdmins) {
|
||||
const hash = await bcrypt.hash(admin.pass + PASSWORD_PEPPER, 10);
|
||||
const userId = crypto.randomUUID();
|
||||
const tenantId = companyMap[admin.company];
|
||||
|
||||
if (!tenantId) {
|
||||
console.warn(`Original company ${admin.company} not found for user ${admin.fullName}, skipping.`);
|
||||
console.warn(` ⚠️ Company ${admin.company} not found for user ${admin.fullName}, skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO core_users (id, tenant_id, name, email, password_hash, status)
|
||||
VALUES ($1, $2, $3, $4, $5, 'ACTIVE')
|
||||
ON CONFLICT (tenant_id, email) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
||||
INSERT INTO users (identifier, password_hash, role, full_name, email, name, tenant_id, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'active')
|
||||
ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
||||
RETURNING id
|
||||
`, [userId, tenantId, admin.fullName, admin.email, hash]);
|
||||
`, [admin.identifier, hash, admin.roles[0], admin.fullName, admin.email, admin.fullName, tenantId]);
|
||||
|
||||
const actualUserId = result.rows[0].id;
|
||||
const userId = result.rows[0].id;
|
||||
|
||||
for (const role of admin.roles) {
|
||||
await pool.query(`
|
||||
INSERT INTO core_user_roles (user_id, role)
|
||||
INSERT INTO user_roles (user_id, role)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id, role) DO NOTHING
|
||||
`, [actualUserId, role]);
|
||||
`, [userId, role]);
|
||||
}
|
||||
|
||||
console.log(` ✓ User created: ${admin.email}`);
|
||||
console.log(` ✓ User created: ${admin.identifier}`);
|
||||
}
|
||||
|
||||
// 3. Create Candidates (Job Seekers)
|
||||
// Requested: paulo_santos / User@2025
|
||||
const candidates = [
|
||||
{ fullName: 'Paulo Santos', email: 'paulo_santos', pass: 'User@2025' },
|
||||
{ fullName: 'Maria Reyes', email: 'maria@email.com', pass: 'User@2025' }
|
||||
{ identifier: 'paulo_santos', fullName: 'Paulo Santos', email: 'paulo@email.com', pass: 'User@2025' },
|
||||
{ identifier: 'maria_email', fullName: 'Maria Reyes', email: 'maria@email.com', pass: 'User@2025' }
|
||||
];
|
||||
|
||||
for (const cand of candidates) {
|
||||
const hash = await bcrypt.hash(cand.pass + PASSWORD_PEPPER, 10);
|
||||
const userId = crypto.randomUUID();
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO core_users (id, tenant_id, name, email, password_hash, status)
|
||||
VALUES ($1, $2, $3, $4, $5, 'ACTIVE')
|
||||
ON CONFLICT (tenant_id, email) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
||||
INSERT INTO users (identifier, password_hash, role, full_name, email, name, status)
|
||||
VALUES ($1, $2, 'jobSeeker', $3, $4, $5, 'active')
|
||||
ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
||||
RETURNING id
|
||||
`, [userId, systemTenantId, cand.fullName, cand.email, hash]);
|
||||
`, [cand.identifier, hash, cand.fullName, cand.email, cand.fullName]);
|
||||
|
||||
const actualUserId = result.rows[0].id;
|
||||
const userId = result.rows[0].id;
|
||||
|
||||
await pool.query(`
|
||||
INSERT INTO core_user_roles (user_id, role)
|
||||
INSERT INTO user_roles (user_id, role)
|
||||
VALUES ($1, 'candidate')
|
||||
ON CONFLICT (user_id, role) DO NOTHING
|
||||
`, [actualUserId]);
|
||||
console.log(` ✓ Candidate created: ${cand.email}`);
|
||||
`, [userId]);
|
||||
|
||||
console.log(` ✓ Candidate created: ${cand.identifier}`);
|
||||
}
|
||||
|
||||
console.log('👤 Seeding legacy candidates...');
|
||||
|
|
@ -120,7 +115,7 @@ export async function seedUsers() {
|
|||
title: 'Full Stack Developer',
|
||||
experience: '5 years of experience',
|
||||
skills: ['React', 'Node.js', 'TypeScript', 'AWS', 'Docker'],
|
||||
bio: 'Developer passionate about building innovative solutions. Experience in React, Node.js, and cloud computing.',
|
||||
bio: 'Developer passionate about building innovative solutions.',
|
||||
objective: 'Grow as a full stack developer building scalable products.'
|
||||
},
|
||||
{
|
||||
|
|
@ -133,7 +128,7 @@ export async function seedUsers() {
|
|||
title: 'UX/UI Designer',
|
||||
experience: '3 years of experience',
|
||||
skills: ['Figma', 'Adobe XD', 'UI Design', 'Prototyping', 'Design Systems'],
|
||||
bio: 'Designer focused on creating memorable experiences. Specialist in design systems and prototyping.',
|
||||
bio: 'Designer focused on creating memorable experiences.',
|
||||
objective: 'Design intuitive experiences for web and mobile products.'
|
||||
},
|
||||
{
|
||||
|
|
@ -146,7 +141,7 @@ export async function seedUsers() {
|
|||
title: 'Data Engineer',
|
||||
experience: '7 years of experience',
|
||||
skills: ['Python', 'SQL', 'Spark', 'Machine Learning', 'Data Visualization'],
|
||||
bio: 'Data engineer with a strong background in machine learning and big data. Passionate about turning data into insights.',
|
||||
bio: 'Data engineer with a strong background in machine learning.',
|
||||
objective: 'Build robust data pipelines and analytics products.'
|
||||
},
|
||||
{
|
||||
|
|
@ -159,7 +154,7 @@ export async function seedUsers() {
|
|||
title: 'Product Manager',
|
||||
experience: '6 years of experience',
|
||||
skills: ['Product Management', 'Agile', 'Scrum', 'Data Analysis', 'User Research'],
|
||||
bio: 'Product Manager with experience in digital products and agile methodologies. Focused on delivering user value.',
|
||||
bio: 'Product Manager with experience in digital products.',
|
||||
objective: 'Lead cross-functional teams to deliver customer-centric products.'
|
||||
},
|
||||
{
|
||||
|
|
@ -172,7 +167,7 @@ export async function seedUsers() {
|
|||
title: 'DevOps Engineer',
|
||||
experience: '4 years of experience',
|
||||
skills: ['Docker', 'Kubernetes', 'AWS', 'Terraform', 'CI/CD'],
|
||||
bio: 'DevOps engineer specialized in automation and cloud infrastructure. Experience with Kubernetes and CI/CD.',
|
||||
bio: 'DevOps engineer specialized in automation and cloud infrastructure.',
|
||||
objective: 'Improve delivery pipelines and cloud reliability.'
|
||||
}
|
||||
];
|
||||
|
|
@ -181,20 +176,9 @@ export async function seedUsers() {
|
|||
const hash = await bcrypt.hash('User@2025' + PASSWORD_PEPPER, 10);
|
||||
await pool.query(`
|
||||
INSERT INTO users (
|
||||
identifier,
|
||||
password_hash,
|
||||
role,
|
||||
full_name,
|
||||
email,
|
||||
phone,
|
||||
city,
|
||||
state,
|
||||
title,
|
||||
experience,
|
||||
skills,
|
||||
objective,
|
||||
bio
|
||||
) VALUES ($1, $2, 'jobSeeker', $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
identifier, password_hash, role, full_name, email, name, phone,
|
||||
city, state, title, experience, skills, objective, bio, status
|
||||
) VALUES ($1, $2, 'jobSeeker', $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, 'active')
|
||||
ON CONFLICT (identifier) DO UPDATE SET
|
||||
full_name = EXCLUDED.full_name,
|
||||
email = EXCLUDED.email,
|
||||
|
|
@ -211,6 +195,7 @@ export async function seedUsers() {
|
|||
hash,
|
||||
cand.fullName,
|
||||
cand.email,
|
||||
cand.fullName,
|
||||
cand.phone,
|
||||
cand.city,
|
||||
cand.state,
|
||||
|
|
|
|||
Loading…
Reference in a new issue