Merge branch 'main' into issue/51-chats-de-ia-en-segundo-plano-para-asignaturas
CI / test (pull_request) Successful in 40s
CI / test (pull_request) Successful in 40s
This commit is contained in:
@@ -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
|
||||||
@@ -4,6 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- 'supabase/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- 'supabase/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -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,3 +1,4 @@
|
|||||||
|
// TESTING
|
||||||
export type Json =
|
export type Json =
|
||||||
| string
|
| string
|
||||||
| number
|
| number
|
||||||
|
|||||||
@@ -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
@@ -1,6 +1,6 @@
|
|||||||
SET session_replication_role = replica;
|
SET session_replication_role = replica;
|
||||||
|
|
||||||
--
|
-- 1
|
||||||
-- PostgreSQL database dump
|
-- PostgreSQL database dump
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type Json =
|
export type Json =
|
||||||
| string
|
| string
|
||||||
| number
|
| number
|
||||||
| boolean
|
| boolean
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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",
|
|
||||||
|
const openLibraryBaseUrl = "https://openlibrary.org/search.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: {
|
headers: {
|
||||||
Accept: "application/json",
|
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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user