feat - new schemas

This commit is contained in:
Juan 2025-08-13 16:31:22 +02:00
parent 970c8b33db
commit 96ebac5547
11 changed files with 652 additions and 47 deletions

View File

@ -1,44 +1,131 @@
import { sql } from "drizzle-orm"
import {
doublePrecision,
foreignKey,
integer,
pgPolicy,
pgTable,
serial,
text,
uuid,
varchar
} from "drizzle-orm/pg-core"
import { authenticatedRole, authUsers } from "drizzle-orm/supabase"
export const users = pgTable("demo", {
id: serial("id").primaryKey(),
import { sql } from 'drizzle-orm'; fullName: text("full_name"),
import { foreignKey, pgPolicy, pgTable, serial, text, uuid, varchar } from 'drizzle-orm/pg-core'; phone: varchar("phone", { length: 256 })
import { authenticatedRole, authUsers } from 'drizzle-orm/supabase'; })
export const users = pgTable('demo', {
id: serial('id').primaryKey(),
fullName: text('full_name'),
phone: varchar('phone', { length: 256 })
});
export const profiles = pgTable( export const profiles = pgTable(
'profiles', "profiles",
{ {
id: uuid('id').notNull().primaryKey(), id: uuid("id").notNull().primaryKey(),
firstName: text('first_name'), firstName: text("first_name"),
lastName: text('last_name'), lastName: text("last_name")
}, },
(table) => [ (table) => [
foreignKey({ foreignKey({
columns: [table.id], columns: [table.id],
foreignColumns: [authUsers.id], foreignColumns: [authUsers.id],
name: 'profiles_id_fkey', name: "profiles_id_fkey"
}).onDelete('cascade'), }).onDelete("cascade"),
pgPolicy('select-own-profile', { pgPolicy("select-own-profile", {
for: 'select', for: "select",
to: authenticatedRole, to: authenticatedRole,
using: sql`${table.id} = auth.uid()`, using: sql`${table.id} = auth.uid()`
}), }),
pgPolicy('update-own-profile', { pgPolicy("update-own-profile", {
for: 'update', for: "update",
to: authenticatedRole, to: authenticatedRole,
using: sql`${table.id} = auth.uid()`, using: sql`${table.id} = auth.uid()`,
withCheck: sql`${table.id} = auth.uid()`, withCheck: sql`${table.id} = auth.uid()`
}), }),
pgPolicy('insert-profile', { pgPolicy("insert-profile", {
for: 'insert', for: "insert",
to: authenticatedRole, to: authenticatedRole,
withCheck: sql`${table.id} = auth.uid()`, withCheck: sql`${table.id} = auth.uid()`
}) })
] ]
).enableRLS(); ).enableRLS()
// === Catálogo de certificaciones ===
export const certifications = pgTable(
"certifications",
{
id: serial("id").primaryKey(),
name: text("name").notNull()
},
() => [
// Política: todos los usuarios autenticados pueden leer, nadie puede escribir
pgPolicy("select-certifications", {
for: "select",
to: authenticatedRole,
using: sql`true`
})
]
).enableRLS()
// === Catálogo de modelos de drones ===
export const droneModels = pgTable(
"drone_models",
{
id: serial("id").primaryKey(),
name: text("name").notNull()
},
() => [
pgPolicy("select-drone-models", {
for: "select",
to: authenticatedRole,
using: sql`true`
})
]
).enableRLS()
// === Tabla principal de pilotos ===
export const pilots = pgTable(
"pilots",
{
id: uuid("id").notNull().primaryKey(), // Igual que auth.uid()
name: text("name"),
location: text("location"),
latitude: doublePrecision("latitude"),
longitude: doublePrecision("longitude"),
company: text("company"),
position: text("position"),
description: text("description"),
differentiation: text("differentiation"),
coverageAreas: text("coverage_areas"),
specializationAreas: text("specialization_areas"),
certificationIds: integer("certification_ids").array(), // IDs de tabla certifications
droneModelIds: integer("drone_model_ids").array(), // IDs de tabla drone_models
email: text("email")
},
(table) => [
// Relación con la tabla auth.users
foreignKey({
columns: [table.id],
foreignColumns: [authUsers.id],
name: "pilots_id_fkey"
}).onDelete("cascade"),
// === RLS ===
pgPolicy("select-own-pilot", {
for: "select",
to: authenticatedRole,
using: sql`${table.id} = auth.uid()`
}),
pgPolicy("update-own-pilot", {
for: "update",
to: authenticatedRole,
using: sql`${table.id} = auth.uid()`,
withCheck: sql`${table.id} = auth.uid()`
}),
pgPolicy("insert-pilot", {
for: "insert",
to: authenticatedRole,
withCheck: sql`${table.id} = auth.uid()`
})
]
).enableRLS()

View File

@ -0,0 +1,36 @@
CREATE TABLE "certifications" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "certifications" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "drone_models" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "drone_models" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "pilots" (
"id" uuid PRIMARY KEY NOT NULL,
"name" text,
"location" text,
"latitude" double precision,
"longitude" double precision,
"company" text,
"position" text,
"description" text,
"differentiation" text,
"coverage_areas" text,
"specialization_areas" text,
"certification_ids" integer[],
"drone_model_ids" integer[],
"email" text
);
--> statement-breakpoint
ALTER TABLE "pilots" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
ALTER TABLE "pilots" ADD CONSTRAINT "pilots_id_fkey" FOREIGN KEY ("id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE POLICY "select-certifications" ON "certifications" AS PERMISSIVE FOR SELECT TO "authenticated" USING (true);--> statement-breakpoint
CREATE POLICY "select-drone-models" ON "drone_models" AS PERMISSIVE FOR SELECT TO "authenticated" USING (true);--> statement-breakpoint
CREATE POLICY "select-own-pilot" ON "pilots" AS PERMISSIVE FOR SELECT TO "authenticated" USING ("pilots"."id" = auth.uid());--> statement-breakpoint
CREATE POLICY "update-own-pilot" ON "pilots" AS PERMISSIVE FOR UPDATE TO "authenticated" USING ("pilots"."id" = auth.uid()) WITH CHECK ("pilots"."id" = auth.uid());--> statement-breakpoint
CREATE POLICY "insert-pilot" ON "pilots" AS PERMISSIVE FOR INSERT TO "authenticated" WITH CHECK ("pilots"."id" = auth.uid());

View File

@ -0,0 +1,336 @@
{
"id": "462e99b2-ba6b-4c91-9f6b-891795695955",
"prevId": "7d0d4272-65ba-45cf-9dd3-a5e2008d3744",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.certifications": {
"name": "certifications",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {
"select-certifications": {
"name": "select-certifications",
"as": "PERMISSIVE",
"for": "SELECT",
"to": [
"authenticated"
],
"using": "true"
}
},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.drone_models": {
"name": "drone_models",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {
"select-drone-models": {
"name": "select-drone-models",
"as": "PERMISSIVE",
"for": "SELECT",
"to": [
"authenticated"
],
"using": "true"
}
},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.pilots": {
"name": "pilots",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"location": {
"name": "location",
"type": "text",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"company": {
"name": "company",
"type": "text",
"primaryKey": false,
"notNull": false
},
"position": {
"name": "position",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"differentiation": {
"name": "differentiation",
"type": "text",
"primaryKey": false,
"notNull": false
},
"coverage_areas": {
"name": "coverage_areas",
"type": "text",
"primaryKey": false,
"notNull": false
},
"specialization_areas": {
"name": "specialization_areas",
"type": "text",
"primaryKey": false,
"notNull": false
},
"certification_ids": {
"name": "certification_ids",
"type": "integer[]",
"primaryKey": false,
"notNull": false
},
"drone_model_ids": {
"name": "drone_model_ids",
"type": "integer[]",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"pilots_id_fkey": {
"name": "pilots_id_fkey",
"tableFrom": "pilots",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {
"select-own-pilot": {
"name": "select-own-pilot",
"as": "PERMISSIVE",
"for": "SELECT",
"to": [
"authenticated"
],
"using": "\"pilots\".\"id\" = auth.uid()"
},
"update-own-pilot": {
"name": "update-own-pilot",
"as": "PERMISSIVE",
"for": "UPDATE",
"to": [
"authenticated"
],
"using": "\"pilots\".\"id\" = auth.uid()",
"withCheck": "\"pilots\".\"id\" = auth.uid()"
},
"insert-pilot": {
"name": "insert-pilot",
"as": "PERMISSIVE",
"for": "INSERT",
"to": [
"authenticated"
],
"withCheck": "\"pilots\".\"id\" = auth.uid()"
}
},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.profiles": {
"name": "profiles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"first_name": {
"name": "first_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"last_name": {
"name": "last_name",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"profiles_id_fkey": {
"name": "profiles_id_fkey",
"tableFrom": "profiles",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {
"select-own-profile": {
"name": "select-own-profile",
"as": "PERMISSIVE",
"for": "SELECT",
"to": [
"authenticated"
],
"using": "\"profiles\".\"id\" = auth.uid()"
},
"update-own-profile": {
"name": "update-own-profile",
"as": "PERMISSIVE",
"for": "UPDATE",
"to": [
"authenticated"
],
"using": "\"profiles\".\"id\" = auth.uid()",
"withCheck": "\"profiles\".\"id\" = auth.uid()"
},
"insert-profile": {
"name": "insert-profile",
"as": "PERMISSIVE",
"for": "INSERT",
"to": [
"authenticated"
],
"withCheck": "\"profiles\".\"id\" = auth.uid()"
}
},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.demo": {
"name": "demo",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"full_name": {
"name": "full_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"phone": {
"name": "phone",
"type": "varchar(256)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -15,6 +15,13 @@
"when": 1755013739316, "when": 1755013739316,
"tag": "0001_nice_gargoyle", "tag": "0001_nice_gargoyle",
"breakpoints": true "breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1755075449620,
"tag": "0002_bouncy_apocalypse",
"breakpoints": true
} }
] ]
} }

View File

@ -0,0 +1,39 @@
import { useMutation } from "@tanstack/react-query"
import { toast } from "sonner"
import type z from "zod"
import { createProfile } from "@/lib/server/user"
import { profileFormSchema } from "@/lib/validation/user"
import { useValidation } from "../useValidation"
type TProfileForm = z.infer<typeof profileFormSchema>
export const useProfile = () => {
const { validate, errors } = useValidation({
defaultSchema: profileFormSchema
})
const signup = useMutation({
mutationKey: ["create-profile"],
mutationFn: async (data: TProfileForm) => createProfile({ data }),
onSuccess: () => {
toast.success("Your profile is created..", {
id: "create-profile"
})
}
})
const validateSignup = (formData: TProfileForm) => {
const isValid = validate({ formData })
if (!isValid) {
toast.error("Don't create", {
id: "create-profile"
})
}
signup.mutate(formData)
}
return {
profile: validateSignup,
errors,
isPending: signup.isPending
}
}

View File

@ -1,9 +1,9 @@
import { redirect } from "@tanstack/react-router" import { redirect } from "@tanstack/react-router"
import { createServerFn } from "@tanstack/react-start" import { createServerFn } from "@tanstack/react-start"
import { db } from "@/integrations/drizzle" import { db } from "@/integrations/drizzle"
import { users } from "@/integrations/drizzle/db/schema" import { profiles, users } from "@/integrations/drizzle/db/schema"
import { getSupabaseServerClient } from "@/integrations/supabase/supabase" import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
import { loginFormSchema, signupFormSchema } from "../validation/user" import { loginFormSchema, profileFormSchema, signupFormSchema } from "../validation/user"
export const getUser = createServerFn().handler(async () => { export const getUser = createServerFn().handler(async () => {
const supabase = getSupabaseServerClient() const supabase = getSupabaseServerClient()
@ -89,4 +89,22 @@ export const signupUser = createServerFn({ method: "POST" })
export const getAllUsers = createServerFn().handler(async () => { export const getAllUsers = createServerFn().handler(async () => {
const response = await db.select().from(users) const response = await db.select().from(users)
return response return response
}) })
export const createProfile = createServerFn({ method: "POST" })
.validator(profileFormSchema)
.handler(async ({ data }) => {
await db.insert(profiles).values(data).returning();
})
export const getProfile = createServerFn().handler(async (data) => {
const { id } = data;
const response = await db
.select()
.from(profiles)
.where(eq(profiles.id, id))
.limit(1);
return response[0] ?? null;
});

View File

@ -10,3 +10,9 @@ export const signupFormSchema = z.object({
password: z.string().min(6, "Password must be at least 6 characters long"), password: z.string().min(6, "Password must be at least 6 characters long"),
redirectUrl: z.string().optional() redirectUrl: z.string().optional()
}) })
export const profileFormSchema= z.object({
id: z.uuid(),
firstName: z.string(),
lastName: z.string().optional()
})

View File

@ -14,6 +14,7 @@ import { Route as LogoutRouteImport } from './routes/logout'
import { Route as LoginRouteImport } from './routes/login' import { Route as LoginRouteImport } from './routes/login'
import { Route as AuthedRouteImport } from './routes/_authed' import { Route as AuthedRouteImport } from './routes/_authed'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as AuthedRegisterRouteImport } from './routes/_authed/register'
import { Route as AuthedPostRouteImport } from './routes/_authed/post' import { Route as AuthedPostRouteImport } from './routes/_authed/post'
const SignupRoute = SignupRouteImport.update({ const SignupRoute = SignupRouteImport.update({
@ -40,6 +41,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AuthedRegisterRoute = AuthedRegisterRouteImport.update({
id: '/register',
path: '/register',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedPostRoute = AuthedPostRouteImport.update({ const AuthedPostRoute = AuthedPostRouteImport.update({
id: '/post', id: '/post',
path: '/post', path: '/post',
@ -52,6 +58,7 @@ export interface FileRoutesByFullPath {
'/logout': typeof LogoutRoute '/logout': typeof LogoutRoute
'/signup': typeof SignupRoute '/signup': typeof SignupRoute
'/post': typeof AuthedPostRoute '/post': typeof AuthedPostRoute
'/register': typeof AuthedRegisterRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
@ -59,6 +66,7 @@ export interface FileRoutesByTo {
'/logout': typeof LogoutRoute '/logout': typeof LogoutRoute
'/signup': typeof SignupRoute '/signup': typeof SignupRoute
'/post': typeof AuthedPostRoute '/post': typeof AuthedPostRoute
'/register': typeof AuthedRegisterRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
@ -68,12 +76,13 @@ export interface FileRoutesById {
'/logout': typeof LogoutRoute '/logout': typeof LogoutRoute
'/signup': typeof SignupRoute '/signup': typeof SignupRoute
'/_authed/post': typeof AuthedPostRoute '/_authed/post': typeof AuthedPostRoute
'/_authed/register': typeof AuthedRegisterRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/login' | '/logout' | '/signup' | '/post' fullPaths: '/' | '/login' | '/logout' | '/signup' | '/post' | '/register'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/login' | '/logout' | '/signup' | '/post' to: '/' | '/login' | '/logout' | '/signup' | '/post' | '/register'
id: id:
| '__root__' | '__root__'
| '/' | '/'
@ -82,6 +91,7 @@ export interface FileRouteTypes {
| '/logout' | '/logout'
| '/signup' | '/signup'
| '/_authed/post' | '/_authed/post'
| '/_authed/register'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
@ -129,6 +139,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/_authed/register': {
id: '/_authed/register'
path: '/register'
fullPath: '/register'
preLoaderRoute: typeof AuthedRegisterRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/post': { '/_authed/post': {
id: '/_authed/post' id: '/_authed/post'
path: '/post' path: '/post'
@ -141,10 +158,12 @@ declare module '@tanstack/react-router' {
interface AuthedRouteChildren { interface AuthedRouteChildren {
AuthedPostRoute: typeof AuthedPostRoute AuthedPostRoute: typeof AuthedPostRoute
AuthedRegisterRoute: typeof AuthedRegisterRoute
} }
const AuthedRouteChildren: AuthedRouteChildren = { const AuthedRouteChildren: AuthedRouteChildren = {
AuthedPostRoute: AuthedPostRoute, AuthedPostRoute: AuthedPostRoute,
AuthedRegisterRoute: AuthedRegisterRoute,
} }
const AuthedRouteWithChildren = const AuthedRouteWithChildren =

View File

@ -1,4 +1,5 @@
import { createFileRoute, redirect } from "@tanstack/react-router" import { createFileRoute, redirect } from "@tanstack/react-router"
import { getProfile } from "@/lib/server/user"
export const Route = createFileRoute("/_authed")({ export const Route = createFileRoute("/_authed")({
beforeLoad: ({ context }) => { beforeLoad: ({ context }) => {
@ -7,6 +8,11 @@ export const Route = createFileRoute("/_authed")({
// TODO: Redirect to login page // TODO: Redirect to login page
} }
}, },
loader: (context) => {
console.log(context);
const profile = getProfile()
console.log(profile)
},
errorComponent: ({ error }) => { errorComponent: ({ error }) => {
if (error.message === "Not authenticated") { if (error.message === "Not authenticated") {
return ( return (

View File

@ -0,0 +1,43 @@
import { Button, Form, Input, Textarea } from "@heroui/react"
import { createFileRoute } from "@tanstack/react-router"
import type { FormEvent } from "react"
import { useProfile } from "@/lib/hooks/user/useCreateUser"
export const Route = createFileRoute("/_authed/register")({
component: RouteComponent
})
function RouteComponent() {
const { errors, isPending, profile } = useProfile()
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
profile({
id: "e6472b9d-01a9-4e2e-8bdc-0ddaa9baf5d8",
firstName: formData.get("firstName") as string,
lastName: formData.get("lastName") as string
})
}
return (
<div>
<Form
className="grid gap-2 max-w-sm w-full"
onSubmit={handleSubmit}
validationErrors={errors}
>
<Input name="name" label="Nombre completo" isRequired />
<Input name="location" label="Lugar" />
<Input name="company" label="Empresa" />
<Input name="role" label="Cargo" />
<Textarea name="description" label="Descripción" />
<Textarea name="differentiator" label="¿Qué te hace diferente?" />
<Input name="coverage_areas" label="Areas de cobertura" />
<Input name="services" label="Servicios" />
projects text[] (proyectos destacados)
contact text (número de teléfono u otro medio)
email text
<Button isLoading={isPending} type="submit">Crear usuario</Button>
</Form>
</div>
)
}

View File

@ -1,9 +1,17 @@
import { Button, Form, Input } from "@heroui/react" import { Button, Form, Input } from "@heroui/react"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute, redirect } from "@tanstack/react-router"
import type { FormEvent } from "react" import type { FormEvent } from "react"
import { useLogin } from "@/lib/hooks/user/useLogin" import { useLogin } from "@/lib/hooks/user/useLogin"
export const Route = createFileRoute("/login")({ export const Route = createFileRoute("/login")({
beforeLoad: ({ context }) => {
if (!context?.error) {
throw redirect({
to: "/post"
})
// TODO: Redirect to login page
}
},
component: LoginComp component: LoginComp
}) })