Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
.vscode
**/*.cache
**/*.egg-info
**/package-lock.json
**/seed-auth-map.json
38 changes: 38 additions & 0 deletions backend/typescript/config/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint-disable */

/*
Sequelize CLI config
- We're using this to tell sequelize which DB to talk to for each environment (dev/test/prod)
- Values mostly come from the docker .env so it works inside the same containers
*/

require("dotenv").config();

module.exports = {
// Local
development: {
username: process.env.POSTGRES_USER || "postgres",
password: process.env.POSTGRES_PASSWORD || "password",
database: process.env.POSTGRES_DB_NAME || "humane_society_dev",
host: process.env.DB_HOST || "db",
port: process.env.DB_PORT || 5432,
dialect: "postgres",
logging: false,
},
// CI / local tests
test: {
username: process.env.POSTGRES_USER || "postgres",
password: process.env.POSTGRES_PASSWORD || "password",
database: process.env.POSTGRES_DB_TEST || "humane_society_test",
host: process.env.DB_HOST || "db",
port: process.env.DB_PORT || 5432,
dialect: "postgres",
logging: false,
},
// Prod
production: {
use_env_variable: "DATABASE_URL",
dialect: "postgres",
logging: false,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable */
import type { QueryInterface, Sequelize } from "sequelize";

const TABLE = "users";
const UQ_EMAIL = "users_email_unique";
const UQ_AUTH_ID = "users_auth_id_unique";

module.exports = {
up: async (queryInterface: QueryInterface, _sequelize: Sequelize) => {
await queryInterface.sequelize.transaction(async (t) => {
// adding unique constraints
await queryInterface.addConstraint(TABLE, {
fields: ["email"],
type: "unique",
name: UQ_EMAIL,
transaction: t,
});
await queryInterface.addConstraint(TABLE, {
fields: ["auth_id"],
type: "unique",
name: UQ_AUTH_ID,
transaction: t,
});
});
},

down: async (queryInterface: QueryInterface, _sequelize: Sequelize) => {
await queryInterface.sequelize.transaction(async (t) => {
await queryInterface.removeConstraint(TABLE, UQ_AUTH_ID, { transaction: t });
await queryInterface.removeConstraint(TABLE, UQ_EMAIL, { transaction: t });
});
},
};
7 changes: 6 additions & 1 deletion backend/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
"lint": "eslint . --ext .ts,.js",
"fix": "eslint . --ext .ts,.js --fix",
"postinstall": "tsc",
"sequelize": "ts-node ./node_modules/.bin/sequelize-cli"
"sequelize": "ts-node ./node_modules/.bin/sequelize-cli",
"seed": "yarn sequelize db:seed:all",
"start:ts": "ts-node -r dotenv/config server.ts",
"migrate": "yarn sequelize db:migrate",
"seed:undo": "yarn sequelize db:seed:undo:all",
"seed:auth": "ts-node -r dotenv/config scripts/seed-auth-users.ts"
},
"keywords": [],
"author": "",
Expand Down
50 changes: 50 additions & 0 deletions backend/typescript/scripts/seed-auth-users.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document code blocks for clarity

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable */
import * as fs from "fs";
import * as path from "path";
import admin from "firebase-admin";
import "dotenv/config";

if (!process.env.FIREBASE_AUTH_EMULATOR_HOST) {
console.error(
"Set FIREBASE_AUTH_EMULATOR_HOST (e.g., host.docker.internal:9099).",
);
process.exit(1);
}

if (!admin.apps.length) {
admin.initializeApp({
projectId: process.env.FIREBASE_PROJECT_ID || "uw-blueprint-starter-code",
});
}

const auth = admin.auth();

const USERS: Array<{ label: string; email: string; password: string; displayName: string }> = require(
path.resolve(__dirname, "../seeders/mockData/auth.json")
);

(async () => {
// label -> uid map
const map: Record<string, string> = {};
for (const u of USERS) {
try {
// reuse user if in emulator already
const ex = await auth.getUserByEmail(u.email);
map[u.label] = ex.uid;
} catch {
const created = await auth.createUser({
email: u.email,
password: u.password,
displayName: u.displayName,
emailVerified: true,
});
map[u.label] = created.uid;
}
}

// Seeder files will consume this to map something like "admin_001" -> actual UID
const outPath = path.resolve(__dirname, "../seeders/seed-auth-map.json");
fs.writeFileSync(outPath, JSON.stringify(map, null, 2));
console.log("Wrote UID map:", outPath, map);
process.exit(0);
})();
55 changes: 55 additions & 0 deletions backend/typescript/seeders/20251023-01-seed-users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable */
import type { QueryInterface } from "sequelize";
import { resolveTable, tsKeys, withTS, Rec } from "../utilities/_utils";

// Pull real UIDs created by `yarn seed:auth`
let uidMap: Record<string, string>;
try {
uidMap = require("./seed-auth-map.json");
} catch {
throw new Error(
"seed-auth-map.json not found. Run `docker compose exec ts-backend yarn seed:auth` first.",
);
}

module.exports = {
up: async (queryInterface: QueryInterface) => {
const Users = await resolveTable(queryInterface, ["Users", "users"]);
const uTS = await tsKeys(queryInterface, Users);

// Load user fixtures
const FIXTURES: Array<{
label: string;
first_name: string;
last_name: string;
role: string;
email: string;
color_level: number;
animal_tags: string;
status: string;
can_see_all_logs: boolean;
can_assign_users_to_tasks: boolean;
phone_number: string;
}> = require("./mockData/users.json");

const users: Rec[] = FIXTURES.map((f) => ({
...f,
auth_id: uidMap[f.label],
}))
.map((r) => {
const { label, ...rest } = r;
return rest;
})
.map((r) => withTS(r, uTS.createdKey, uTS.updatedKey));

await queryInterface.bulkInsert(Users, users);
},

down: async (queryInterface: QueryInterface) => {
const Users = await resolveTable(queryInterface, ["Users", "users"]);
const FIXTURES: Array<{ label: string }> = require("./mockData/users.json");
await queryInterface.bulkDelete(Users, {
auth_id: FIXTURES.map((f) => uidMap[f.label]),
} as any);
},
};
20 changes: 20 additions & 0 deletions backend/typescript/seeders/20251023-02-seed-pets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable */
import type { QueryInterface } from "sequelize";
import { resolveTable, tsKeys, withTS, Rec } from "../utilities/_utils";

const PET_FIXTURES: Rec[] = require("./mockData/pets.json")

module.exports = {
up: async (queryInterface: QueryInterface) => {
const Pets = await resolveTable(queryInterface, ["Pets", "pets"]);
const pTS = await tsKeys(queryInterface, Pets);

const pets: Rec[] = PET_FIXTURES.map((r) => withTS(r, pTS.createdKey, pTS.updatedKey));
await queryInterface.bulkInsert(Pets, pets);
},

down: async (queryInterface: QueryInterface) => {
const Pets = await resolveTable(queryInterface, ["Pets", "pets"]);
await queryInterface.bulkDelete(Pets, {}, {});
},
};
41 changes: 41 additions & 0 deletions backend/typescript/seeders/20251023-03-seed-templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* eslint-disable */
import type { QueryInterface } from "sequelize";
import { resolveTable, tsKeys, withTS, Rec } from "../utilities/_utils";

module.exports = {
up: async (queryInterface: QueryInterface) => {
const Templates = await resolveTable(queryInterface, [
"task_templates",
"TaskTemplates",
"Task_Templates",
"taskTemplates",
]);
const ttTS = await tsKeys(queryInterface, Templates);

// Load template fixtures
const FIXTURES: Array<{
task_name: string;
category: string;
instruction: string;
}> = require("./mockData/templates.json");

const templates: Rec[] = FIXTURES.map((r) =>
withTS(r, ttTS.createdKey, ttTS.updatedKey)
);

await queryInterface.bulkInsert(Templates, templates);
},

down: async (queryInterface: QueryInterface) => {
const Templates = await resolveTable(queryInterface, [
"task_templates",
"TaskTemplates",
"Task_Templates",
"taskTemplates",
]);
const FIXTURES: Array<{ task_name: string }> = require("./mockData/templates.json");
await queryInterface.bulkDelete(Templates, {
task_name: FIXTURES.map((f) => f.task_name),
} as any);
},
};
89 changes: 89 additions & 0 deletions backend/typescript/seeders/20251023-04-seed-tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* eslint-disable */
import type { QueryInterface } from "sequelize";
import { resolveTable, tsKeys, withTS, Rec } from "../utilities/_utils";

let uidMap: Record<string, string>;
try {
uidMap = require("./seed-auth-map.json");
} catch {
throw new Error(
"seed-auth-map.json not found. Run `docker compose exec ts-backend yarn seed:auth` first.",
);
}

module.exports = {
up: async (queryInterface: QueryInterface) => {
const Users = await resolveTable(queryInterface, ["Users", "users"]);
const Pets = await resolveTable(queryInterface, ["Pets", "pets"]);
const Templates = await resolveTable(queryInterface, [
"task_templates",
"TaskTemplates",
"Task_Templates",
"taskTemplates",
]);
const Tasks = await resolveTable(queryInterface, ["tasks", "Tasks"]);
const tkTS = await tsKeys(queryInterface, Tasks);

// Load task fixtures
const FIXTURES: Array<{
userLabel: string | null;
petName: string;
templateName: string;
offsetHours?: number;
startNow?: boolean;
notes: string;
}> = require("./mockData/tasks.json");

// Look up FK IDs dynamically (no hardcoded numbers)
const userAuthIds: string[] = Array.from(
new Set(
FIXTURES
.map((f) => f.userLabel)
.filter((x): x is string => Boolean(x))
.map((label) => uidMap[label])
)
);
const petNames: string[] = Array.from(
new Set(FIXTURES.map((f) => f.petName))
);

const [userRows]: any = await queryInterface.sequelize.query(
`SELECT id, auth_id FROM "${Users}" WHERE auth_id IN (:ids)`,
{ replacements: { ids: userAuthIds } },
);
const [petRows]: any = await queryInterface.sequelize.query(
`SELECT id, name FROM "${Pets}" WHERE name IN (:names)`,
{ replacements: { names: petNames } },
);
const [tmplRows]: any = await queryInterface.sequelize.query(
`SELECT id, task_name FROM "${Templates}"`,
);

const idOf = (arr: any[], key: string, val: string) =>
(arr.find((x) => x[key] === val) || {}).id ?? null;

const tasks: Rec[] = FIXTURES.map((f) => {
const scheduled = new Date(
Date.now() + ((f.offsetHours ?? 0) * 60 * 60 * 1000)
);
const base: any = {
user_id: f.userLabel ? idOf(userRows, "auth_id", uidMap[f.userLabel]) : null,
pet_id: idOf(petRows, "name", f.petName),
task_template_id: idOf(tmplRows, "task_name", f.templateName),
scheduled_start_time: scheduled,
notes: f.notes,
};
if (f.startNow) base.start_time = new Date();
return base;
}).map((r) => withTS(r, tkTS.createdKey, tkTS.updatedKey));

await queryInterface.bulkInsert(Tasks, tasks);
},

down: async (queryInterface: QueryInterface) => {
const Tasks = await resolveTable(queryInterface, ["tasks", "Tasks"]);
await queryInterface.sequelize.query(
`DELETE FROM "${Tasks}" WHERE notes LIKE 'seed_%'`,
);
},
};
11 changes: 11 additions & 0 deletions backend/typescript/seeders/mockData/auth.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
{ "label": "admin_001", "email": "john.smith@humanesociety.com", "password": "Passw0rd!", "displayName": "John Smith" },
{ "label": "admin_002", "email": "sarah.johnson@humanesociety.com", "password": "Passw0rd!", "displayName": "Sarah Johnson" },
{ "label": "behaviourist_001", "email": "emily.wilson@humanesociety.com", "password": "Passw0rd!", "displayName": "Emily Wilson" },
{ "label": "behaviourist_002", "email": "michael.brown@humanesociety.com", "password": "Passw0rd!", "displayName": "Michael Brown" },
{ "label": "staff_001", "email": "lisa.davis@humanesociety.com", "password": "Passw0rd!", "displayName": "Lisa Davis" },
{ "label": "staff_002", "email": "robert.miller@humanesociety.com", "password": "Passw0rd!", "displayName": "Robert Miller" },
{ "label": "volunteer_001", "email": "amanda.garcia@volunteer.com", "password": "Passw0rd!", "displayName": "Amanda Garcia" },
{ "label": "volunteer_002", "email": "kevin.martinez@volunteer.com", "password": "Passw0rd!", "displayName": "Kevin Martinez" }
]

Loading