Merge branch 'main' into issue/51-chats-de-ia-en-segundo-plano-para-asignaturas
CI / test (pull_request) Successful in 40s

This commit is contained in:
2026-03-10 22:13:46 +00:00
12 changed files with 466 additions and 138 deletions
+31
View File
@@ -0,0 +1,31 @@
name: CI
on:
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: supabase/setup-cli@v1
with:
version: latest
- name: Start Supabase local development setup
run: supabase db start
- name: Reset database to ensure a clean state
run: supabase db reset --no-seed
- name: Verify generated types are checked in
run: |
supabase gen types typescript --local > supabase/functions/_shared/database.types.ts
if ! git diff --ignore-space-at-eol --exit-code --quiet supabase/functions/_shared/database.types.ts; then
echo "Detected uncommitted changes after build. See diff below:"
git diff
exit 1
fi
+3 -1
View File
@@ -4,6 +4,8 @@ on:
push: push:
branches: branches:
- main - main
paths:
- 'supabase/**'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -22,4 +24,4 @@ jobs:
version: latest version: latest
- name: Deploy edge function - name: Deploy edge function
run: npx supabase@beta functions deploy --project-ref $SUPABASE_PROJECT_ID --use-api run: npx supabase@beta functions deploy --project-ref $SUPABASE_PROJECT_ID --use-api
+2
View File
@@ -4,6 +4,8 @@ on:
push: push:
branches: branches:
- main - main
paths:
- 'supabase/**'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
+54
View File
@@ -0,0 +1,54 @@
name: Update database types
on:
push:
branches:
- main
paths:
- supabase/migrations/*.sql
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
PROJECT_REF: ${{ secrets.SUPABASE_PROJECT_ID }}
DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: supabase/setup-cli@v1
with:
version: latest
- name: Generate database types
run: |
supabase gen types typescript --local > supabase/functions/_shared/database.types.ts
- name: Check for changes
id: git_status
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "changed=true" >> $GITEA_OUTPUT
else
echo "changed=false" >> $GITEA_OUTPUT
fi
- name: Commit changes
if: steps.git_status.outputs.changed == 'true'
run: |
git config user.name "supabot"
git config user.email "supabot@example.com"
git add supabase/functions/_shared/database.types.ts
git commit -m "chore: update database types"
- name: Push changes
if: steps.git_status.outputs.changed == 'true'
run: |
git push
+1
View File
@@ -1,3 +1,4 @@
// TESTING
export type Json = export type Json =
| string | string
| number | number
+1 -1
View File
@@ -57,7 +57,7 @@ uri = "pg-functions://postgres/public/custom_access_token_hook"
[functions.openai-webhook-responses] [functions.openai-webhook-responses]
enabled = true enabled = true
verify_jwt = true verify_jwt = false
import_map = "./functions/openai-webhook-responses/deno.json" import_map = "./functions/openai-webhook-responses/deno.json"
# Uncomment to specify a custom file path to the entrypoint. # Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx # Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
+1 -1
View File
@@ -1,6 +1,6 @@
SET session_replication_role = replica; SET session_replication_role = replica;
-- -- 1
-- PostgreSQL database dump -- PostgreSQL database dump
-- --
+1 -1
View File
@@ -1,4 +1,4 @@
export type Json = export type Json =
| string | string
| number | number
| boolean | boolean
+2 -2
View File
@@ -1,6 +1,6 @@
// supabase/functions/_shared/openai-service.ts // supabase/functions/_shared/openai-service.ts
/// <reference lib="deno.window" /> /// <reference lib="deno.window" />
import OpenAI from "npm:openai@6.16.0"; import OpenAI from "npm:openai@6.16.0"
import type * as OpenAITypes from "npm:openai@6.16.0"; import type * as OpenAITypes from "npm:openai@6.16.0";
// Use non-streaming params to ensure `responses.create` returns a typed Response // Use non-streaming params to ensure `responses.create` returns a typed Response
export type StructuredResponseOptions = export type StructuredResponseOptions =
@@ -86,7 +86,7 @@ export class OpenAIService {
newOptions.input = arr; newOptions.input = arr;
} }
// Narrow to non-streaming response // Narrow to non-streaming response
const openaiRaw = (await this.openai.responses.create( const openaiRaw = (await this.openai.responses.create(
newOptions as OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming, newOptions as OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming,
)) as OpenAITypes.OpenAI.Responses.Response; )) as OpenAITypes.OpenAI.Responses.Response;
+146 -12
View File
@@ -6,6 +6,11 @@ import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts";
console.log("Starting buscar-bibliografia function"); console.log("Starting buscar-bibliografia function");
type GoogleBooksVolume = Record<string, unknown>; type GoogleBooksVolume = Record<string, unknown>;
type OpenLibraryDoc = Record<string, unknown>;
type EndpointResult =
| { endpoint: "google"; item: GoogleBooksVolume }
| { endpoint: "open_library"; item: OpenLibraryDoc };
interface GoogleBooksVolumesListResponse { interface GoogleBooksVolumesListResponse {
kind?: string; kind?: string;
@@ -13,17 +18,54 @@ interface GoogleBooksVolumesListResponse {
items?: GoogleBooksVolume[]; items?: GoogleBooksVolume[];
} }
interface OpenLibrarySearchResponse {
num_found?: number;
start?: number;
docs?: OpenLibraryDoc[];
}
const ISO6391 = z.string().regex(
/^[a-z]{2}$/i,
"Debe ser ISO 639-1 (2 letras)",
);
const ISO6392 = z.string().regex(
/^[a-z]{3}$/i,
"Debe ser ISO 639-2 (3 letras)",
);
const SearchTermsSchema = z const SearchTermsSchema = z
.object({ .object({
q: z.string().min(1, "q es requerido"), q: z.string().min(1, "q es requerido"),
maxResults: z.number().int().min(0).max(40),
orderBy: z.enum(["newest", "relevance"]).optional(),
}) })
.passthrough(); .strict();
const GoogleParamsSchema = z
.object({
orderBy: z.enum(["newest", "relevance"]).optional(),
langRestrict: ISO6391.optional(),
startIndex: z.number().int().min(0).optional(),
})
.passthrough()
.optional()
.default({});
const OpenLibraryParamsSchema = z
.object({
language: ISO6392.optional(),
page: z.number().int().min(1).optional(),
sort: z.string().optional(),
})
.passthrough()
.optional()
.default({});
const BodySchema = z const BodySchema = z
.object({ .object({
searchTerms: SearchTermsSchema, searchTerms: SearchTermsSchema,
// Parámetros por endpoint
google: GoogleParamsSchema,
openLibrary: OpenLibraryParamsSchema,
}) })
.strict(); .strict();
@@ -57,6 +99,40 @@ function buildUrlWithSearchTerms(
return url.toString(); return url.toString();
} }
function pickSerializableParams(
input: Record<string, unknown>,
): Record<
string,
string | number | boolean | Array<string | number | boolean>
> {
const out: Record<
string,
string | number | boolean | Array<string | number | boolean>
> = {};
for (const [key, value] of Object.entries(input)) {
if (value === undefined || value === null) continue;
if (Array.isArray(value)) {
const arr = value
.filter((v) => v !== undefined && v !== null)
.filter((v) =>
["string", "number", "boolean"].includes(typeof v)
) as Array<
string | number | boolean
>;
if (arr.length) out[key] = arr;
continue;
}
if (["string", "number", "boolean"].includes(typeof value)) {
out[key] = value as string | number | boolean;
}
}
return out;
}
Deno.serve(async (req: Request): Promise<Response> => { Deno.serve(async (req: Request): Promise<Response> => {
const url = new URL(req.url); const url = new URL(req.url);
const functionName = url.pathname.split("/").pop(); const functionName = url.pathname.split("/").pop();
@@ -117,17 +193,45 @@ Deno.serve(async (req: Request): Promise<Response> => {
} }
const baseUrl = "https://www.googleapis.com/books/v1/volumes"; const baseUrl = "https://www.googleapis.com/books/v1/volumes";
const requestUrl = buildUrlWithSearchTerms(baseUrl, { const googleParams = pickSerializableParams({
...body.google,
...body.searchTerms, ...body.searchTerms,
key: GOOGLE_API_KEY, key: GOOGLE_API_KEY,
maxResults: 20,
startIndex: (body.google as Record<string, unknown>)?.startIndex ?? 0,
}); });
// No mandar parámetros exclusivos de Open Library a Google
delete (googleParams as Record<string, unknown>)["page"];
delete (googleParams as Record<string, unknown>)["language"];
const googleResp = await fetch(requestUrl, { const googleUrl = buildUrlWithSearchTerms(baseUrl, googleParams);
method: "GET",
headers: { const openLibraryBaseUrl = "https://openlibrary.org/search.json";
Accept: "application/json", const openLibraryParams = pickSerializableParams({
}, ...body.openLibrary,
q: body.searchTerms.q,
limit: 20,
page: (body.openLibrary as Record<string, unknown>)?.page ?? 1,
}); });
// No mandar parámetros exclusivos de Google a Open Library
delete (openLibraryParams as Record<string, unknown>)["langRestrict"];
delete (openLibraryParams as Record<string, unknown>)["orderBy"];
delete (openLibraryParams as Record<string, unknown>)["startIndex"];
const openLibraryUrl = buildUrlWithSearchTerms(
openLibraryBaseUrl,
openLibraryParams,
);
const [googleResp, openLibraryResp] = await Promise.all([
fetch(googleUrl, { headers: { Accept: "application/json" } }),
fetch(openLibraryUrl, {
headers: {
Accept: "application/json",
"User-Agent": "Acad-IA info.ingenieria.lci@gmail.com",
},
}),
]);
if (!googleResp.ok) { if (!googleResp.ok) {
const text = await googleResp.text().catch(() => ""); const text = await googleResp.text().catch(() => "");
@@ -143,10 +247,40 @@ Deno.serve(async (req: Request): Promise<Response> => {
); );
} }
const data = (await googleResp.json()) as GoogleBooksVolumesListResponse; if (!openLibraryResp.ok) {
const items = Array.isArray(data?.items) ? data.items : []; const text = await openLibraryResp.text().catch(() => "");
throw new HttpError(
502,
"Error al consultar Open Library.",
"OPEN_LIBRARY_REQUEST_FAILED",
{
status: openLibraryResp.status,
statusText: openLibraryResp.statusText,
body: text || null,
},
);
}
return sendSuccess(items); const googleData =
(await googleResp.json()) as GoogleBooksVolumesListResponse;
const openLibraryData =
(await openLibraryResp.json()) as OpenLibrarySearchResponse;
const googleItems = Array.isArray(googleData?.items)
? googleData.items.slice(0, 20)
: [];
const openLibraryDocs = Array.isArray(openLibraryData?.docs)
? openLibraryData.docs.slice(0, 20)
: [];
const results: EndpointResult[] = [
...googleItems.map((item) => ({ endpoint: "google", item } as const)),
...openLibraryDocs.map((
item,
) => ({ endpoint: "open_library", item } as const)),
];
return sendSuccess(results);
} catch (error) { } catch (error) {
if (error instanceof HttpError) { if (error instanceof HttpError) {
console.error( console.error(
@@ -1,43 +1,9 @@
drop view if exists "public"."plantilla_asignatura";
alter table "public"."asignaturas" drop column "criterios_de_evaluacion";
create or replace view "public"."plantilla_asignatura" as SELECT asignaturas.id AS asignatura_id,
struct.id AS estructura_id,
struct.template_id
FROM (public.asignaturas
JOIN public.estructuras_asignatura struct ON ((asignaturas.estructura_id = struct.id)));
grant delete on table "public"."asignatura_mensajes_ia" to "postgres";
grant insert on table "public"."asignatura_mensajes_ia" to "postgres";
grant references on table "public"."asignatura_mensajes_ia" to "postgres";
grant select on table "public"."asignatura_mensajes_ia" to "postgres";
grant trigger on table "public"."asignatura_mensajes_ia" to "postgres";
grant truncate on table "public"."asignatura_mensajes_ia" to "postgres";
grant update on table "public"."asignatura_mensajes_ia" to "postgres";
grant delete on table "public"."plan_mensajes_ia" to "postgres";
grant insert on table "public"."plan_mensajes_ia" to "postgres";
grant references on table "public"."plan_mensajes_ia" to "postgres";
grant select on table "public"."plan_mensajes_ia" to "postgres";
grant trigger on table "public"."plan_mensajes_ia" to "postgres";
grant truncate on table "public"."plan_mensajes_ia" to "postgres";
grant update on table "public"."plan_mensajes_ia" to "postgres";
alter publication supabase_realtime add table asignatura_mensajes_ia; alter publication supabase_realtime add table asignatura_mensajes_ia;
alter publication supabase_realtime add table plan_mensajes_ia; alter publication supabase_realtime add table plan_mensajes_ia;
alter publication supabase_realtime add table asignaturas;
alter publication supabase_realtime add table planes_estudio;
-- Comentario para probar el trigger 3
+217 -79
View File
File diff suppressed because one or more lines are too long