diff --git a/src/app.ts b/src/app.ts index 064fb01..4788423 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,6 +10,8 @@ import { userTable } from "./db/schema/auth/auth.schema.ts"; import authRouter from "./modules/auth/auth.routes.ts"; import healthRouter from "./modules/health/health.routes.ts"; import onboardingRouter from "./modules/onboarding/onboarding.routes.ts"; +import recruiterOrganizationRouter from "./modules/recruiter/organization/recruiter-organization.routes.ts"; +import recruiterProfileRouter from "./modules/recruiter/profile/recruiter-profile.routes.ts"; import { auth } from "./shared/auth/auth.ts"; import { errorHandler } from "./shared/middlewares/error-handler.middleware.ts"; @@ -39,6 +41,8 @@ app.get("/", async (_req, res) => { app.use("/api", healthRouter); app.use("/api", authRouter); app.use("/api", onboardingRouter); +app.use("/api", recruiterProfileRouter); +app.use("/api", recruiterOrganizationRouter); app.use(errorHandler); diff --git a/src/db/migrations/0006_curious_leopardon.sql b/src/db/migrations/0006_curious_leopardon.sql new file mode 100644 index 0000000..7cdfdb7 --- /dev/null +++ b/src/db/migrations/0006_curious_leopardon.sql @@ -0,0 +1 @@ +ALTER TABLE "recruiter_profile" RENAME COLUMN "company_website" TO "organization_website"; \ No newline at end of file diff --git a/src/db/migrations/0007_regular_microbe.sql b/src/db/migrations/0007_regular_microbe.sql new file mode 100644 index 0000000..998e242 --- /dev/null +++ b/src/db/migrations/0007_regular_microbe.sql @@ -0,0 +1,2 @@ +ALTER TABLE "candidate_profile" RENAME COLUMN "country" TO "country_code";--> statement-breakpoint +ALTER TABLE "recruiter_profile" RENAME COLUMN "country" TO "country_code"; \ No newline at end of file diff --git a/src/db/migrations/meta/0006_snapshot.json b/src/db/migrations/meta/0006_snapshot.json new file mode 100644 index 0000000..d759998 --- /dev/null +++ b/src/db/migrations/meta/0006_snapshot.json @@ -0,0 +1,1079 @@ +{ + "id": "94bb3d88-1602-4b40-bf18-ebd0aae647c9", + "prevId": "0b8b08ce-f7e7-4c76-bad4-be6074614caa", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.candidate_profile": { + "name": "candidate_profile", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_role": { + "name": "primary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "highest_education": { + "name": "highest_education", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_status": { + "name": "current_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "years_of_experience_min": { + "name": "years_of_experience_min", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "years_of_experience_max": { + "name": "years_of_experience_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "professional_bio": { + "name": "professional_bio", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portfolio_url": { + "name": "portfolio_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linkedin_url": { + "name": "linkedin_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "candidate_profile_user_id_user_id_fk": { + "name": "candidate_profile_user_id_user_id_fk", + "tableFrom": "candidate_profile", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "Users can manage their candidate profile": { + "name": "Users can manage their candidate profile", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))", + "withCheck": "(user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.candidate_profile_tag": { + "name": "candidate_profile_tag", + "schema": "", + "columns": { + "candidate_user_id": { + "name": "candidate_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "candidate_profile_tag_tagId_idx": { + "name": "candidate_profile_tag_tagId_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "candidate_profile_tag_candidate_user_id_candidate_profile_user_id_fk": { + "name": "candidate_profile_tag_candidate_user_id_candidate_profile_user_id_fk", + "tableFrom": "candidate_profile_tag", + "tableTo": "candidate_profile", + "columnsFrom": ["candidate_user_id"], + "columnsTo": ["user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "candidate_profile_tag_tag_id_tag_id_fk": { + "name": "candidate_profile_tag_tag_id_tag_id_fk", + "tableFrom": "candidate_profile_tag", + "tableTo": "tag", + "columnsFrom": ["tag_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "candidate_profile_tag_candidate_user_id_tag_id_pk": { + "name": "candidate_profile_tag_candidate_user_id_tag_id_pk", + "columns": ["candidate_user_id", "tag_id"] + } + }, + "uniqueConstraints": {}, + "policies": { + "Users can manage their profile tags": { + "name": "Users can manage their profile tags", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["app_user"], + "using": "(candidate_user_id = current_setting('app.current_user_id', true))", + "withCheck": "(candidate_user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_onboarding": { + "name": "user_onboarding", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "is_completed": { + "name": "is_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "current_step": { + "name": "current_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "draft": { + "name": "draft", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_onboarding_userId_idx": { + "name": "user_onboarding_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_onboarding_user_id_user_id_fk": { + "name": "user_onboarding_user_id_user_id_fk", + "tableFrom": "user_onboarding", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "Users can see their own onboarding": { + "name": "Users can see their own onboarding", + "as": "PERMISSIVE", + "for": "SELECT", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + }, + "Users can insert their own onboarding": { + "name": "Users can insert their own onboarding", + "as": "PERMISSIVE", + "for": "INSERT", + "to": ["app_user"], + "withCheck": "(user_id = current_setting('app.current_user_id', true))" + }, + "Users can update their own onboarding": { + "name": "Users can update their own onboarding", + "as": "PERMISSIVE", + "for": "UPDATE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))", + "withCheck": "(user_id = current_setting('app.current_user_id', true))" + }, + "Users can delete their own onboarding": { + "name": "Users can delete their own onboarding", + "as": "PERMISSIVE", + "for": "DELETE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recruiter_profile": { + "name": "recruiter_profile", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_size": { + "name": "organization_size", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "industry": { + "name": "industry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_website": { + "name": "organization_website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "llm_provider": { + "name": "llm_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_api_key": { + "name": "llm_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "recruiter_profile_user_id_user_id_fk": { + "name": "recruiter_profile_user_id_user_id_fk", + "tableFrom": "recruiter_profile", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "Users can manage their recruiter profile": { + "name": "Users can manage their recruiter profile", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))", + "withCheck": "(user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recruiter_profile_tag": { + "name": "recruiter_profile_tag", + "schema": "", + "columns": { + "recruiter_user_id": { + "name": "recruiter_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recruiter_profile_tag_tagId_idx": { + "name": "recruiter_profile_tag_tagId_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recruiter_profile_tag_recruiter_user_id_recruiter_profile_user_id_fk": { + "name": "recruiter_profile_tag_recruiter_user_id_recruiter_profile_user_id_fk", + "tableFrom": "recruiter_profile_tag", + "tableTo": "recruiter_profile", + "columnsFrom": ["recruiter_user_id"], + "columnsTo": ["user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recruiter_profile_tag_tag_id_tag_id_fk": { + "name": "recruiter_profile_tag_tag_id_tag_id_fk", + "tableFrom": "recruiter_profile_tag", + "tableTo": "tag", + "columnsFrom": ["tag_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recruiter_profile_tag_recruiter_user_id_tag_id_pk": { + "name": "recruiter_profile_tag_recruiter_user_id_tag_id_pk", + "columns": ["recruiter_user_id", "tag_id"] + } + }, + "uniqueConstraints": {}, + "policies": { + "Users can manage their recruiter tags": { + "name": "Users can manage their recruiter tags", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["app_user"], + "using": "(recruiter_user_id = current_setting('app.current_user_id', true))", + "withCheck": "(recruiter_user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag": { + "name": "tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "tag_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tag_name_idx": { + "name": "tag_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tag_type_idx": { + "name": "tag_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tag_name_unique": { + "name": "tag_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.account": { + "name": "account", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "Users can update their own accounts": { + "name": "Users can update their own accounts", + "as": "PERMISSIVE", + "for": "UPDATE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + }, + "System can insert all accounts": { + "name": "System can insert all accounts", + "as": "PERMISSIVE", + "for": "INSERT", + "to": ["app_user"], + "withCheck": "true" + }, + "System can see all accounts": { + "name": "System can see all accounts", + "as": "PERMISSIVE", + "for": "SELECT", + "to": ["app_user"], + "using": "true" + }, + "Users can delete their own accounts": { + "name": "Users can delete their own accounts", + "as": "PERMISSIVE", + "for": "DELETE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.session": { + "name": "session", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": { + "System can see all sessions": { + "name": "System can see all sessions", + "as": "PERMISSIVE", + "for": "SELECT", + "to": ["app_user"], + "using": "true" + }, + "Users can delete their own sessions": { + "name": "Users can delete their own sessions", + "as": "PERMISSIVE", + "for": "DELETE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + }, + "Users can update their own sessions": { + "name": "Users can update their own sessions", + "as": "PERMISSIVE", + "for": "UPDATE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + }, + "System can create sessions during signup": { + "name": "System can create sessions during signup", + "as": "PERMISSIVE", + "for": "INSERT", + "to": ["app_user"], + "withCheck": "true" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user": { + "name": "user", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "auth", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "terms_accepted": { + "name": "terms_accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "terms_accepted_at": { + "name": "terms_accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terms_accepted_version": { + "name": "terms_accepted_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": { + "System can see all users": { + "name": "System can see all users", + "as": "PERMISSIVE", + "for": "SELECT", + "to": ["app_user"], + "using": "true" + }, + "System can insert new users": { + "name": "System can insert new users", + "as": "PERMISSIVE", + "for": "INSERT", + "to": ["app_user"], + "withCheck": "true" + }, + "Users can delete their own profile": { + "name": "Users can delete their own profile", + "as": "PERMISSIVE", + "for": "DELETE", + "to": ["app_user"], + "using": "(id = current_setting('app.current_user_id', true))" + }, + "Users can update their own profile": { + "name": "Users can update their own profile", + "as": "PERMISSIVE", + "for": "UPDATE", + "to": ["app_user"], + "using": "(id = current_setting('app.current_user_id', true))", + "withCheck": "(id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verification": { + "name": "verification", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "App user can manage verification tokens": { + "name": "App user can manage verification tokens", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["app_user"], + "using": "true", + "withCheck": "true" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "auth.user_role": { + "name": "user_role", + "schema": "auth", + "values": ["candidate", "recruiter", "admin"] + } + }, + "schemas": { + "auth": "auth" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/db/migrations/meta/0007_snapshot.json b/src/db/migrations/meta/0007_snapshot.json new file mode 100644 index 0000000..f37c40d --- /dev/null +++ b/src/db/migrations/meta/0007_snapshot.json @@ -0,0 +1,1079 @@ +{ + "id": "8538c797-295c-4d4c-9fd0-0b6caa1a499b", + "prevId": "94bb3d88-1602-4b40-bf18-ebd0aae647c9", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.candidate_profile": { + "name": "candidate_profile", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_role": { + "name": "primary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "highest_education": { + "name": "highest_education", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_status": { + "name": "current_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "years_of_experience_min": { + "name": "years_of_experience_min", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "years_of_experience_max": { + "name": "years_of_experience_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "professional_bio": { + "name": "professional_bio", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portfolio_url": { + "name": "portfolio_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linkedin_url": { + "name": "linkedin_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "candidate_profile_user_id_user_id_fk": { + "name": "candidate_profile_user_id_user_id_fk", + "tableFrom": "candidate_profile", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "Users can manage their candidate profile": { + "name": "Users can manage their candidate profile", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))", + "withCheck": "(user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.candidate_profile_tag": { + "name": "candidate_profile_tag", + "schema": "", + "columns": { + "candidate_user_id": { + "name": "candidate_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "candidate_profile_tag_tagId_idx": { + "name": "candidate_profile_tag_tagId_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "candidate_profile_tag_candidate_user_id_candidate_profile_user_id_fk": { + "name": "candidate_profile_tag_candidate_user_id_candidate_profile_user_id_fk", + "tableFrom": "candidate_profile_tag", + "tableTo": "candidate_profile", + "columnsFrom": ["candidate_user_id"], + "columnsTo": ["user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "candidate_profile_tag_tag_id_tag_id_fk": { + "name": "candidate_profile_tag_tag_id_tag_id_fk", + "tableFrom": "candidate_profile_tag", + "tableTo": "tag", + "columnsFrom": ["tag_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "candidate_profile_tag_candidate_user_id_tag_id_pk": { + "name": "candidate_profile_tag_candidate_user_id_tag_id_pk", + "columns": ["candidate_user_id", "tag_id"] + } + }, + "uniqueConstraints": {}, + "policies": { + "Users can manage their profile tags": { + "name": "Users can manage their profile tags", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["app_user"], + "using": "(candidate_user_id = current_setting('app.current_user_id', true))", + "withCheck": "(candidate_user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_onboarding": { + "name": "user_onboarding", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "is_completed": { + "name": "is_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "current_step": { + "name": "current_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "draft": { + "name": "draft", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_onboarding_userId_idx": { + "name": "user_onboarding_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_onboarding_user_id_user_id_fk": { + "name": "user_onboarding_user_id_user_id_fk", + "tableFrom": "user_onboarding", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "Users can see their own onboarding": { + "name": "Users can see their own onboarding", + "as": "PERMISSIVE", + "for": "SELECT", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + }, + "Users can insert their own onboarding": { + "name": "Users can insert their own onboarding", + "as": "PERMISSIVE", + "for": "INSERT", + "to": ["app_user"], + "withCheck": "(user_id = current_setting('app.current_user_id', true))" + }, + "Users can update their own onboarding": { + "name": "Users can update their own onboarding", + "as": "PERMISSIVE", + "for": "UPDATE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))", + "withCheck": "(user_id = current_setting('app.current_user_id', true))" + }, + "Users can delete their own onboarding": { + "name": "Users can delete their own onboarding", + "as": "PERMISSIVE", + "for": "DELETE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recruiter_profile": { + "name": "recruiter_profile", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_size": { + "name": "organization_size", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "industry": { + "name": "industry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_website": { + "name": "organization_website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "llm_provider": { + "name": "llm_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_api_key": { + "name": "llm_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "recruiter_profile_user_id_user_id_fk": { + "name": "recruiter_profile_user_id_user_id_fk", + "tableFrom": "recruiter_profile", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "Users can manage their recruiter profile": { + "name": "Users can manage their recruiter profile", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))", + "withCheck": "(user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recruiter_profile_tag": { + "name": "recruiter_profile_tag", + "schema": "", + "columns": { + "recruiter_user_id": { + "name": "recruiter_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recruiter_profile_tag_tagId_idx": { + "name": "recruiter_profile_tag_tagId_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recruiter_profile_tag_recruiter_user_id_recruiter_profile_user_id_fk": { + "name": "recruiter_profile_tag_recruiter_user_id_recruiter_profile_user_id_fk", + "tableFrom": "recruiter_profile_tag", + "tableTo": "recruiter_profile", + "columnsFrom": ["recruiter_user_id"], + "columnsTo": ["user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recruiter_profile_tag_tag_id_tag_id_fk": { + "name": "recruiter_profile_tag_tag_id_tag_id_fk", + "tableFrom": "recruiter_profile_tag", + "tableTo": "tag", + "columnsFrom": ["tag_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recruiter_profile_tag_recruiter_user_id_tag_id_pk": { + "name": "recruiter_profile_tag_recruiter_user_id_tag_id_pk", + "columns": ["recruiter_user_id", "tag_id"] + } + }, + "uniqueConstraints": {}, + "policies": { + "Users can manage their recruiter tags": { + "name": "Users can manage their recruiter tags", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["app_user"], + "using": "(recruiter_user_id = current_setting('app.current_user_id', true))", + "withCheck": "(recruiter_user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag": { + "name": "tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "tag_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tag_name_idx": { + "name": "tag_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tag_type_idx": { + "name": "tag_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tag_name_unique": { + "name": "tag_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.account": { + "name": "account", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "Users can update their own accounts": { + "name": "Users can update their own accounts", + "as": "PERMISSIVE", + "for": "UPDATE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + }, + "System can insert all accounts": { + "name": "System can insert all accounts", + "as": "PERMISSIVE", + "for": "INSERT", + "to": ["app_user"], + "withCheck": "true" + }, + "System can see all accounts": { + "name": "System can see all accounts", + "as": "PERMISSIVE", + "for": "SELECT", + "to": ["app_user"], + "using": "true" + }, + "Users can delete their own accounts": { + "name": "Users can delete their own accounts", + "as": "PERMISSIVE", + "for": "DELETE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.session": { + "name": "session", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": { + "System can see all sessions": { + "name": "System can see all sessions", + "as": "PERMISSIVE", + "for": "SELECT", + "to": ["app_user"], + "using": "true" + }, + "Users can delete their own sessions": { + "name": "Users can delete their own sessions", + "as": "PERMISSIVE", + "for": "DELETE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + }, + "Users can update their own sessions": { + "name": "Users can update their own sessions", + "as": "PERMISSIVE", + "for": "UPDATE", + "to": ["app_user"], + "using": "(user_id = current_setting('app.current_user_id', true))" + }, + "System can create sessions during signup": { + "name": "System can create sessions during signup", + "as": "PERMISSIVE", + "for": "INSERT", + "to": ["app_user"], + "withCheck": "true" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user": { + "name": "user", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "auth", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "terms_accepted": { + "name": "terms_accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "terms_accepted_at": { + "name": "terms_accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terms_accepted_version": { + "name": "terms_accepted_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": { + "System can see all users": { + "name": "System can see all users", + "as": "PERMISSIVE", + "for": "SELECT", + "to": ["app_user"], + "using": "true" + }, + "System can insert new users": { + "name": "System can insert new users", + "as": "PERMISSIVE", + "for": "INSERT", + "to": ["app_user"], + "withCheck": "true" + }, + "Users can delete their own profile": { + "name": "Users can delete their own profile", + "as": "PERMISSIVE", + "for": "DELETE", + "to": ["app_user"], + "using": "(id = current_setting('app.current_user_id', true))" + }, + "Users can update their own profile": { + "name": "Users can update their own profile", + "as": "PERMISSIVE", + "for": "UPDATE", + "to": ["app_user"], + "using": "(id = current_setting('app.current_user_id', true))", + "withCheck": "(id = current_setting('app.current_user_id', true))" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verification": { + "name": "verification", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "App user can manage verification tokens": { + "name": "App user can manage verification tokens", + "as": "PERMISSIVE", + "for": "ALL", + "to": ["app_user"], + "using": "true", + "withCheck": "true" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "auth.user_role": { + "name": "user_role", + "schema": "auth", + "values": ["candidate", "recruiter", "admin"] + } + }, + "schemas": { + "auth": "auth" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index a99c7d0..043e41e 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -43,6 +43,20 @@ "when": 1770876034376, "tag": "0005_cheerful_magdalene", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1771863566762, + "tag": "0006_curious_leopardon", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1771915071593, + "tag": "0007_regular_microbe", + "breakpoints": true } ] } diff --git a/src/db/schema/app/candidate-profile.schema.ts b/src/db/schema/app/candidate-profile.schema.ts index e802cb2..3b3432a 100644 --- a/src/db/schema/app/candidate-profile.schema.ts +++ b/src/db/schema/app/candidate-profile.schema.ts @@ -22,7 +22,7 @@ export const candidateProfileTable = pgTable( yearsOfExperienceMax: integer("years_of_experience_max"), professionalBio: text("professional_bio").notNull(), - country: text("country").notNull(), + countryCode: text("country_code").notNull(), portfolioUrl: text("portfolio_url"), githubUrl: text("github_url"), linkedinUrl: text("linkedin_url"), diff --git a/src/db/schema/app/recruiter-profile.schema.ts b/src/db/schema/app/recruiter-profile.schema.ts index 0916641..b79937e 100644 --- a/src/db/schema/app/recruiter-profile.schema.ts +++ b/src/db/schema/app/recruiter-profile.schema.ts @@ -15,9 +15,9 @@ export const recruiterProfileTable = pgTable( organizationName: text("organization_name").notNull(), organizationSize: text("organization_size").notNull(), industry: text("industry").notNull(), - country: text("country").notNull(), + countryCode: text("country_code").notNull(), - companyWebsite: text("company_website"), + organizationWebsite: text("organization_website"), // LLM setup fields llmProvider: text("llm_provider").notNull(), diff --git a/src/modules/onboarding/candidate-onboarding.schema.ts b/src/modules/onboarding/candidate-onboarding.schema.ts index 9c33ac8..fffa8d5 100644 --- a/src/modules/onboarding/candidate-onboarding.schema.ts +++ b/src/modules/onboarding/candidate-onboarding.schema.ts @@ -18,7 +18,7 @@ export const candidateOnboardingSchema = z.object({ professionalBio: z.string().min(20, "Professional bio must be at least 20 characters"), // step 3 - location & presence - country: z.string().min(1, "Country is required"), + countryCode: z.string().min(1, "Country code is required"), portfolioUrl: z.url("Invalid portfolio URL").optional().or(z.literal("")), githubUrl: z.url("Invalid GitHub URL").optional().or(z.literal("")), linkedinUrl: z.url("Invalid LinkedIn profile URL").optional().or(z.literal("")), diff --git a/src/modules/onboarding/onboarding.service.ts b/src/modules/onboarding/onboarding.service.ts index 2b65627..44f06e8 100644 --- a/src/modules/onboarding/onboarding.service.ts +++ b/src/modules/onboarding/onboarding.service.ts @@ -205,7 +205,7 @@ export class OnboardingService { yearsOfExperienceMin: minYears, yearsOfExperienceMax: maxYears, professionalBio: data.professionalBio, - country: data.country, + countryCode: data.countryCode, portfolioUrl: data.portfolioUrl ?? null, githubUrl: data.githubUrl ?? null, linkedinUrl: data.linkedinUrl ?? null, @@ -282,8 +282,8 @@ export class OnboardingService { organizationName: data.organizationName, organizationSize: data.organizationSize, industry: data.industry, - country: data.country, - companyWebsite: data.companyWebsite ?? null, + countryCode: data.countryCode, + organizationWebsite: data.organizationWebsite ?? null, llmProvider: data.llmProvider, llmApiKey: CryptoJS.AES.encrypt(data.llmApiKey, env.ENCRYPTION_KEY).toString(), defaultModel: data.defaultModel ?? null, diff --git a/src/modules/onboarding/recruiter-onboarding.schema.ts b/src/modules/onboarding/recruiter-onboarding.schema.ts index 438868a..ace460a 100644 --- a/src/modules/onboarding/recruiter-onboarding.schema.ts +++ b/src/modules/onboarding/recruiter-onboarding.schema.ts @@ -8,12 +8,12 @@ export const recruiterOnboardingSchema = z.object({ .min(2, "Organization name must be at least 2 characters"), organizationSize: z.string().min(1, "Organization size is required"), industry: z.string().min(1, "Industry is required"), - country: z.string().min(1, "Country is required"), + countryCode: z.string().min(1, "Country code is required"), // Step 2 hiringDomains: z.array(z.string()).min(1, "Select at least one domain"), experienceLevelsHiring: z.array(z.string()).min(1, "Select at least one level"), - companyWebsite: z.url("Invalid website URL").optional().or(z.literal("")), + organizationWebsite: z.url("Invalid website URL").optional().or(z.literal("")), // Step 3 - LLM Setup llmProvider: z.string().min(1, "Select LLM provider"), diff --git a/src/modules/recruiter/organization/recruiter-organization.controller.ts b/src/modules/recruiter/organization/recruiter-organization.controller.ts new file mode 100644 index 0000000..13d2bdc --- /dev/null +++ b/src/modules/recruiter/organization/recruiter-organization.controller.ts @@ -0,0 +1,33 @@ +import { type NextFunction, type Request, type Response } from "express"; + +import { ApplicationError } from "../../../shared/errors/application-error.ts"; +import { getAuth } from "../../../utils/get-auth.ts"; +import { RecruiterProfileService } from "../profile/recruiter-profile.service.ts"; +import { + type RecruiterOrganizationData, + recruiterOrganizationPartialSchema, +} from "./recruiter-organization.schema.ts"; + +export const patchRecruiterOrganization = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const user = getAuth(req).user; + + if (user.role !== "recruiter") { + throw new ApplicationError("Forbidden: recruiter role required", 403); + } + + // validate incoming data (partial allowed for patch) + const data = recruiterOrganizationPartialSchema.parse(req.body); + + const service = new RecruiterProfileService(user.id); + const updated: RecruiterOrganizationData = await service.updateOrganization(data); + + res.json(updated); + } catch (error) { + next(error); + } +}; diff --git a/src/modules/recruiter/organization/recruiter-organization.routes.ts b/src/modules/recruiter/organization/recruiter-organization.routes.ts new file mode 100644 index 0000000..eb97f68 --- /dev/null +++ b/src/modules/recruiter/organization/recruiter-organization.routes.ts @@ -0,0 +1,15 @@ +import { Router } from "express"; + +import { requireAuth } from "../../../shared/middlewares/auth.middleware.ts"; +import { patchRecruiterOrganization } from "./recruiter-organization.controller.ts"; + +const recruiterOrganizationRouter = Router(); + +// patch route for updating recruiter organization information +recruiterOrganizationRouter.patch( + "/recruiter/organization", + requireAuth, + patchRecruiterOrganization +); + +export default recruiterOrganizationRouter; diff --git a/src/modules/recruiter/organization/recruiter-organization.schema.ts b/src/modules/recruiter/organization/recruiter-organization.schema.ts new file mode 100644 index 0000000..4c868aa --- /dev/null +++ b/src/modules/recruiter/organization/recruiter-organization.schema.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +// Schema for validating recruiter organization data (used when creating or updating) +export const recruiterOrganizationSchema = z.object({ + organizationName: z + .string() + .min(1, "Organization name is required") + .min(2, "Organization name must be at least 2 characters"), + organizationSize: z.string().min(1, "Organization size is required"), + industry: z.string().min(1, "Industry is required"), + countryCode: z.string().min(1, "Country code is required"), + + // hiring preferences stored as tags + hiringDomains: z.array(z.string()).min(1, "Select at least one domain"), + experienceLevels: z.array(z.string()).min(1, "Select at least one experience level"), + + organizationWebsite: z.url("Invalid website URL").optional().or(z.literal("")), + llmProvider: z.string().min(1, "Select LLM provider"), + defaultModel: z.string().optional(), +}); + +// Partial schema for patch operations +export const recruiterOrganizationPartialSchema = recruiterOrganizationSchema.partial(); + +// Types exported for use throughout the application +export type RecruiterOrganizationData = z.infer; +export type RecruiterOrganizationUpdateData = z.infer; diff --git a/src/modules/recruiter/profile/recruiter-profile.controller.ts b/src/modules/recruiter/profile/recruiter-profile.controller.ts new file mode 100644 index 0000000..e46059f --- /dev/null +++ b/src/modules/recruiter/profile/recruiter-profile.controller.ts @@ -0,0 +1,26 @@ +import { type NextFunction, type Request, type Response } from "express"; + +import { ApplicationError } from "../../../shared/errors/application-error.ts"; +import { getAuth } from "../../../utils/get-auth.ts"; +import { RecruiterProfileService } from "./recruiter-profile.service.ts"; + +export const getRecruiterProfile = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const user = getAuth(req).user; + + if (user.role !== "recruiter") { + throw new ApplicationError("Forbidden: recruiter role required", 403); + } + + const service = new RecruiterProfileService(user.id); + const profile = await service.getProfile(); + + res.json(profile); + } catch (error) { + next(error); + } +}; diff --git a/src/modules/recruiter/profile/recruiter-profile.routes.ts b/src/modules/recruiter/profile/recruiter-profile.routes.ts new file mode 100644 index 0000000..9ca1884 --- /dev/null +++ b/src/modules/recruiter/profile/recruiter-profile.routes.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; + +import { requireAuth } from "../../../shared/middlewares/auth.middleware.ts"; +import { getRecruiterProfile } from "./recruiter-profile.controller.ts"; + +const recruiterProfileRouter = Router(); + +recruiterProfileRouter.get("/recruiter/profile", requireAuth, getRecruiterProfile); + +export default recruiterProfileRouter; diff --git a/src/modules/recruiter/profile/recruiter-profile.service.ts b/src/modules/recruiter/profile/recruiter-profile.service.ts new file mode 100644 index 0000000..f09aba0 --- /dev/null +++ b/src/modules/recruiter/profile/recruiter-profile.service.ts @@ -0,0 +1,213 @@ +import { and, eq, inArray, or } from "drizzle-orm"; + +import { createUserContext } from "../../../db/create-user-context.ts"; +import { + recruiterProfileTable, + recruiterProfileTagTable, + tagTable, +} from "../../../db/schema/index.ts"; +import { ApplicationError } from "../../../shared/errors/application-error.ts"; +import type { RecruiterOrganizationUpdateData } from "../organization/recruiter-organization.schema.ts"; +import type { RecruiterProfileResponse } from "./recruiter-profile.type.ts"; + +export class RecruiterProfileService { + constructor(private readonly userId: string) {} + + private get userCtx() { + return createUserContext(this.userId); + } + + async getProfile(): Promise { + const profile = await this.userCtx.withRls((tx) => { + return tx.query.recruiterProfileTable.findFirst({ + where: eq(recruiterProfileTable.userId, this.userId), + with: { + user: { + with: { + sessions: true, + }, + }, + tags: { + with: { + tag: true, + }, + }, + }, + }); + }); + + if (!profile) { + throw new ApplicationError("Recruiter profile not found", 404); + } + + const hiringDomains = profile.tags + .filter((pt) => pt.tag.type === "domain") + .map((pt) => pt.tag.name); + + const experienceLevels = profile.tags + .filter((pt) => pt.tag.type === "experience_level") + .map((pt) => pt.tag.name); + + const userSessions = profile.user.sessions.map((s) => ({ + id: s.id, + expiresAt: s.expiresAt, + ipAddress: s.ipAddress, + userAgent: s.userAgent, + createdAt: s.createdAt, + updatedAt: s.updatedAt, + })); + + return { + user: { + id: profile.user.id, + name: profile.user.name, + email: profile.user.email, + avatarUrl: profile.user.image || null, + sessions: userSessions, + }, + organizationName: profile.organizationName, + organizationSize: profile.organizationSize, + industry: profile.industry, + countryCode: profile.countryCode, + organizationWebsite: profile.organizationWebsite, + llmProvider: profile.llmProvider, + defaultModel: profile.defaultModel, + hiringDomains, + experienceLevels, + createdAt: profile.createdAt, + updatedAt: profile.updatedAt, + }; + } + + /** + * Update only the organization-related fields from a recruiter profile. + * Accepts a partial payload so callers can patch one or more properties. + */ + async updateOrganization(data: RecruiterOrganizationUpdateData) { + // build an object containing only defined values + type UpdatePayload = Partial<{ + organizationName: string; + organizationSize: string; + industry: string; + countryCode: string; + organizationWebsite: string | null; + llmProvider: string; + defaultModel: string | null; + }>; + + const updates: UpdatePayload = {}; + if (data.organizationName !== undefined) updates.organizationName = data.organizationName; + if (data.organizationSize !== undefined) updates.organizationSize = data.organizationSize; + if (data.industry !== undefined) updates.industry = data.industry; + if (data.countryCode !== undefined) updates.countryCode = data.countryCode; + if (data.organizationWebsite !== undefined) + updates.organizationWebsite = data.organizationWebsite; + if (data.llmProvider !== undefined) updates.llmProvider = data.llmProvider; + if (data.defaultModel !== undefined) updates.defaultModel = data.defaultModel; + + // update profile fields if present + if (Object.keys(updates).length > 0) { + await this.userCtx.withRls((tx) => + tx + .update(recruiterProfileTable) + .set(updates) + .where(eq(recruiterProfileTable.userId, this.userId)) + ); + } + + // handle tag updates (domains + experience levels) + if (data.hiringDomains || data.experienceLevels) { + await this.userCtx.withRls(async (tx) => { + // determine desired lists + const domains = data.hiringDomains ?? []; + const levels = data.experienceLevels ?? []; + + // first, clear any existing associations for these tag types + const typesToRemove: string[] = []; + if (domains.length) typesToRemove.push("domain"); + if (levels.length) typesToRemove.push("experience_level"); + + if (typesToRemove.length) { + const existingTags = await tx.query.tagTable.findMany({ + where: inArray(tagTable.type, typesToRemove), + }); + const existingIds = existingTags.map((t) => t.id); + if (existingIds.length) { + await tx + .delete(recruiterProfileTagTable) + .where( + and( + eq(recruiterProfileTagTable.recruiterUserId, this.userId), + inArray(recruiterProfileTagTable.tagId, existingIds) + ) + ); + } + } + + // upsert tag names themselves so new values are available + if (domains.length || levels.length) { + await tx + .insert(tagTable) + .values([ + ...domains.map((d) => ({ name: d, type: "domain" })), + ...levels.map((l) => ({ name: l, type: "experience_level" })), + ]) + .onConflictDoNothing(); + } + + // now build and insert the new association rows in a single lookup + const toInsert: Array<{ recruiterUserId: string; tagId: number }> = []; + if (domains.length || levels.length) { + const rows = await tx.query.tagTable.findMany({ + where: or( + and(eq(tagTable.type, "domain"), inArray(tagTable.name, domains)), + and(eq(tagTable.type, "experience_level"), inArray(tagTable.name, levels)) + ), + }); + toInsert.push(...rows.map((tag) => ({ recruiterUserId: this.userId, tagId: tag.id }))); + } + if (toInsert.length) { + await tx.insert(recruiterProfileTagTable).values(toInsert); + } + }); + } + + // fetch and return fresh organization information (including tags) + const updated = await this.userCtx.withRls((tx) => + tx.query.recruiterProfileTable.findFirst({ + where: eq(recruiterProfileTable.userId, this.userId), + with: { + tags: { + with: { tag: true }, + }, + }, + }) + ); + + if (!updated) { + throw new ApplicationError("Recruiter profile not found", 404); + } + + const hiringDomains = updated.tags + .filter((pt) => pt.tag.type === "domain") + .map((pt) => pt.tag.name); + + const experienceLevels = updated.tags + .filter((pt) => pt.tag.type === "experience_level") + .map((pt) => pt.tag.name); + + return { + organizationName: updated.organizationName, + organizationSize: updated.organizationSize, + industry: updated.industry, + countryCode: updated.countryCode, + // schema allows empty string/undefined; convert null to empty string + organizationWebsite: updated.organizationWebsite ?? undefined, + llmProvider: updated.llmProvider, + // make undefined if null + defaultModel: updated.defaultModel ?? undefined, + hiringDomains, + experienceLevels, + }; + } +} diff --git a/src/modules/recruiter/profile/recruiter-profile.type.ts b/src/modules/recruiter/profile/recruiter-profile.type.ts new file mode 100644 index 0000000..2e0259b --- /dev/null +++ b/src/modules/recruiter/profile/recruiter-profile.type.ts @@ -0,0 +1,27 @@ +export type RecruiterProfileResponse = { + user: { + id: string; + email: string; + name?: string; + avatarUrl?: string | null; + sessions: Array<{ + id: string; + expiresAt: Date; + ipAddress: string | null; + userAgent: string | null; + createdAt: Date; + updatedAt: Date; + }>; + }; + organizationName: string; + organizationSize: string; + industry: string; + countryCode: string; + organizationWebsite: string | null; + llmProvider: string; + defaultModel: string | null; + hiringDomains: string[]; + experienceLevels: string[]; + createdAt: Date; + updatedAt: Date; +};