Skip to content

Commit

Permalink
Merge pull request #17 from ghost-in-the-machine-llc/db-migration
Browse files Browse the repository at this point in the history
Db migration
  • Loading branch information
martypdx committed Nov 6, 2023
2 parents bcdd3cb + ddabbb9 commit 8b511eb
Show file tree
Hide file tree
Showing 37 changed files with 1,686 additions and 231 deletions.
2 changes: 2 additions & 0 deletions .env.example
@@ -1,3 +1,5 @@
# General supabase access token (all projects)
SUPABASE_ACCESS_TOKEN=
# Supabase Project
SUPABASE_DB_PASSWORD=
SUPABASE_PROJECT_ID=
11 changes: 9 additions & 2 deletions .github/workflows/deploy.yml
Expand Up @@ -32,6 +32,13 @@ jobs:
- uses: supabase/setup-cli@v1
with:
version: latest


- name: Link to environment
run: supabase link --project-ref $PROJECT_ID

- name: Deploy functions
run: supabase functions deploy --project-ref $PROJECT_ID
run: supabase functions deploy

- name: Push db changes
run: supabase db push

2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -4,3 +4,5 @@ node_modules
*.env
# Local Netlify folder
.netlify
# saved postman responses
response
5 changes: 5 additions & 0 deletions .vscode/settings.json
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"djwt"
]
}
9 changes: 8 additions & 1 deletion deno.lock
@@ -1,5 +1,12 @@
{
"version": "2",
"version": "3",
"redirects": {
"https://deno.land/std/http/http_status.ts": "https://deno.land/std@0.205.0/http/http_status.ts",
"https://deno.land/std/http/status.ts": "https://deno.land/std@0.205.0/http/status.ts",
"https://esm.sh/@supabase/supabase-js@2": "https://esm.sh/@supabase/supabase-js@2.38.4",
"https://esm.sh/@supabase/supabase-js@2/": "https://esm.sh/@supabase/supabase-js@2.38.4/",
"https://esm.sh/@supabase/supabase-js@2/dist/module/index.d.ts": "https://esm.sh/v133/@supabase/supabase-js@2.38.4/dist/module/index.d.ts"
},
"remote": {
"https://deno.land/std@0.127.0/_util/assert.ts": "6396c1bd0361c4939e7f32f9b03efffcd04b640a1b206ed67058553d6cb59cc4",
"https://deno.land/std@0.127.0/_util/os.ts": "49b92edea1e82ba295ec946de8ffd956ed123e2948d9bd1d3e901b04e4307617",
Expand Down
9 changes: 9 additions & 0 deletions import-map.json
@@ -0,0 +1,9 @@
{
"imports": {
"@supabase": "https://esm.sh/@supabase/supabase-js@2/",
"@supabase/types": "https://esm.sh/@supabase/supabase-js@2/dist/module/index.d.ts",
"@supabase/gotrue-helpers": "https://esm.sh/v133/@supabase/gotrue-js@2.57.0/dist/module/lib/helpers.js",
"http/status": "https://deno.land/std/http/status.ts",
"@x/djwt": "https://deno.land/x/djwt@v3.0.0/mod.ts"
}
}
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -7,7 +7,9 @@
"start": "supabase start; supabase functions serve",
"lint": "npm run lint:js && npm run lint:ts",
"lint:js": "npm run lint -w www",
"lint:ts": "deno lint"
"lint:ts": "deno lint",
"db:update-ts": "supabase gen types typescript --local > ./supabase/functions/schema.gen.ts",
"db:new-migration": "supabase db diff --local --schema public | supabase migration new "
},
"keywords": [],
"author": "",
Expand Down
2 changes: 2 additions & 0 deletions spiritwave.ai.code-workspace
Expand Up @@ -29,6 +29,7 @@
"denoland",
"Hyperlegible",
"nosniff",
"openai",
"OPENAI",
"qunit",
"selkie",
Expand All @@ -51,6 +52,7 @@
"deno.enablePaths": [
"supabase/functions"
],
"deno.importMap": "import-map.json",
"typescript.validate.enable": false,
"typescript.tsserver.experimental.enableProjectDiagnostics": false,
"[typescript]": {
Expand Down
5 changes: 2 additions & 3 deletions supabase/config.toml
Expand Up @@ -71,7 +71,7 @@ site_url = "http://localhost:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://localhost:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
jwt_expiry = 604800
# If disabled, the refresh token will never expire.
enable_refresh_token_rotation = true
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
Expand Down Expand Up @@ -134,5 +134,4 @@ vector_port = 54328
backend = "postgres"

[functions]
greeting.verify_jwt = false
invocation.verify_jwt = false
play.import_map = "../import-map.json"
8 changes: 8 additions & 0 deletions supabase/functions/.vscode/settings.json
@@ -0,0 +1,8 @@
{
"cSpell.words": [
"nosniff",
"openai",
"PGRST",
"Postgrest"
]
}
167 changes: 167 additions & 0 deletions supabase/functions/_lib/HealingSessionManager.ts
@@ -0,0 +1,167 @@
import type {
PostgrestMaybeSingleResponse,
PostgrestSingleResponse,
SupabaseClient,
} from '@supabase/types';
import type { Database } from '../schema.gen.ts';
import type {
Healer,
Service,
SessionStatus,
Step,
} from '../database.types.ts';
import type { Message } from './openai.ts';

import { createServiceClient, handleResponse } from './supabase.ts';
import { getUserPayload } from './jwt.ts';

interface SessionInfo {
id: number;
step_id: number;
status: SessionStatus;
}

interface Session {
id: number;
step_id: number;
healer: Healer;
service: Service;
}

interface NewMoment {
step_id: number;
messages: Message[];
response: string;
}

export enum Status {
Created = 'created',
Active = 'active',
Done = 'done',
}

export class HealingSessionManager {
// #userClient: SupabaseClient<Database>;
#serviceClient: SupabaseClient<Database>;
#uid: string;
// This is "healing session", not a server session
#sessionId: number;

constructor(userToken: string, sessionId: number) {
const payload = getUserPayload(userToken);
this.#uid = payload.sub;
this.#sessionId = sessionId;

// this.#userClient = createClient(userToken);
this.#serviceClient = createServiceClient();
}

async getOpenSessionInfo(): Promise<SessionInfo> {
const res: PostgrestMaybeSingleResponse<SessionInfo> = await this
.#serviceClient
.from('session')
.select(`
id,
step_id,
status
`)
// eventually add service_id and healer_id
.eq('id', this.#sessionId)
.eq('uid', this.#uid)
.maybeSingle();

return await handleResponse(res);
}

async getFullSessionInfo(): Promise<Session> {
const res: PostgrestSingleResponse<Session> = await this
.#serviceClient
.from('session')
.select(`
id,
healer(*),
service(*),
step_id
`)
// eventually add service_id and healer_id
.eq('id', this.#sessionId)
.eq('uid', this.#uid)
.neq('status', Status.Done)
.single();

return await handleResponse(res);
}

async getStepAfter(stepId: number | null): Promise<Step> {
let query = this.#serviceClient
.from('step')
.select();
query = stepId
? query.eq('prior_id', stepId)
: query.is('prior_id', null);

const res: PostgrestMaybeSingleResponse<Step> = await query
.maybeSingle();

return handleResponse(res);
}

async #updateSession(update: object): Promise<void> {
const { error } = await this.#serviceClient
.from('session')
.update(update)
.eq('id', this.#sessionId);

if (error) throw error;
}

updateSessionStep(stepId: number): Promise<void> {
return this.#updateSession({ step_id: stepId, status: Status.Active });
}

updateSessionStatus(status: SessionStatus): Promise<void> {
return this.#updateSession({ status });
}

async getPriorMessages(stepId: number | null): Promise<Message[]> {
if (!stepId) return [];

const res = await this.#serviceClient
.from('moment')
.select('*')
.match({
session_id: this.#sessionId,
step_id: stepId,
uid: this.#uid,
})
.single();

const data = handleResponse(res)!;
return JSON.parse(<string> data.messages).messages;
}

async saveMoment(moment: NewMoment): Promise<void> {
// PG doesn't deal with whole arrays in json columns,
// so we make it an object... :shrug:
const messages = {
messages: [
...moment.messages,
{ role: 'assistant', content: moment.response },
],
};

const { error } = await this.#serviceClient
.from('moment')
.insert({
...moment,
// parse the response string as JSON to decode
// encoded characters like \", \', \n, etc.
response: JSON.parse(JSON.stringify(moment.response)),
messages: JSON.stringify(messages),
uid: this.#uid,
session_id: this.#sessionId,
});

if (error) throw error;
}
}
2 changes: 1 addition & 1 deletion supabase/functions/_lib/cors.ts
@@ -1,5 +1,5 @@
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Origin': '*', //TODO: allow to be set from env config
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type',
};
Expand Down
14 changes: 14 additions & 0 deletions supabase/functions/_lib/http.ts
@@ -0,0 +1,14 @@
import { Status, STATUS_TEXT } from 'http/status';

export class HttpError extends Error {
statusCode = Status.InternalServerError;
statusText = STATUS_TEXT[Status.InternalServerError];

constructor(statusCode: Status, message: string, statusText?: string) {
super(message);
this.statusCode = statusCode;
this.statusText = statusText || STATUS_TEXT[statusCode];
}
}

export class NoDataError extends Error {}
17 changes: 17 additions & 0 deletions supabase/functions/_lib/jwt.ts
@@ -0,0 +1,17 @@
import { decodeJWTPayload } from '@supabase/gotrue-helpers';

// const encoder = new TextEncoder();
// const secret = Deno.env.get('JWT_SECRET');
// const keyBuffer = encoder.encode(secret);

// const keyPromise = crypto.subtle.importKey(
// 'raw',
// keyBuffer,
// { name: 'HMAC', hash: 'SHA-256' },
// true,
// ['sign', 'verify'],
// );

export function getUserPayload(token: string) {
return decodeJWTPayload(token);
}
57 changes: 20 additions & 37 deletions supabase/functions/_lib/openai.ts
@@ -1,17 +1,18 @@
import {
getAllContent,
OpenAIContentStream,
streamToConsole,
} from './streams.ts';
import { corsHeaders } from './cors.ts';
import { Status } from 'http/status';
import { HttpError } from './http.ts';
import { OpenAIContentStream } from './streams.ts';

const API_KEY = Deno.env.get('OPENAI_API_KEY');
const COMPLETIONS_URL = 'https://api.openai.com/v1/chat/completions';

export interface Message {
role: string;
content: string;
}

export async function streamCompletion(
messages: { role: string; content: string }[],
): Promise<Response> {
// body is a ReadableStream when opt { stream: true }
messages: Message[],
): Promise<{ status: Status; stream: ReadableStream }> {
const res = await fetch(
COMPLETIONS_URL,
{
Expand All @@ -34,35 +35,17 @@ export async function streamCompletion(

if (!ok) {
const text = await res.text();
return new Response(text, {
headers: {
...corsHeaders,
'content-type': 'application/json',
},
status: status,
});
}

let stream = null, response = null;
if (body) {
[stream, response] = body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new OpenAIContentStream())
.tee();
let message = text;
try {
message = JSON.parse(text);
} catch (_) { /* no-op */ }

stream = stream.pipeThrough(new TextEncoderStream());
response
.pipeThrough(getAllContent())
// This will be a save to db of content when relevant
.pipeTo(streamToConsole());
throw new HttpError(status, message);
}

return new Response(stream, {
headers: {
...corsHeaders,
'content-type': 'text/event-stream; charset=utf-8',
'x-content-type-options': 'nosniff',
},
status: status,
});
const stream = body!
.pipeThrough(new TextDecoderStream())
.pipeThrough(new OpenAIContentStream());

return { status, stream };
}

0 comments on commit 8b511eb

Please sign in to comment.