WIP saurpc integration
This commit is contained in:
parent
861c4a4035
commit
7825dda25e
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -15,6 +15,8 @@ node_modules/
|
||||||
*.db
|
*.db
|
||||||
*.db-wal
|
*.db-wal
|
||||||
*.db-shm
|
*.db-shm
|
||||||
|
*-wal
|
||||||
|
*-shm
|
||||||
|
|
||||||
/static/uploads
|
/static/uploads
|
||||||
|
|
||||||
|
|
9
TablesForModels.ts
Normal file
9
TablesForModels.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Display, Playlist, Song, Team, User } from '@lyrics/models.ts'
|
||||||
|
|
||||||
|
export const TablesForModels = {
|
||||||
|
'song': Song,
|
||||||
|
'playlist': Playlist,
|
||||||
|
'display': Display,
|
||||||
|
'user': User,
|
||||||
|
'team': Team,
|
||||||
|
}
|
6
api.ts
6
api.ts
|
@ -2,6 +2,12 @@ import { FreshContext, Handlers } from '$fresh/server.ts'
|
||||||
import { db, TablesForModels } from '@lyrics/db.ts'
|
import { db, TablesForModels } from '@lyrics/db.ts'
|
||||||
import { ulid } from 'https://deno.land/std@0.209.0/ulid/mod.ts'
|
import { ulid } from 'https://deno.land/std@0.209.0/ulid/mod.ts'
|
||||||
|
|
||||||
|
export type Method = 'GET' | 'PUT' | 'DELETE'
|
||||||
|
|
||||||
|
export function isMethod(value: string): value is Method {
|
||||||
|
return (value in TablesForModels)
|
||||||
|
}
|
||||||
|
|
||||||
export function crudHandlerFor<
|
export function crudHandlerFor<
|
||||||
T extends { id: string },
|
T extends { id: string },
|
||||||
M extends { parse: (a: unknown) => T },
|
M extends { parse: (a: unknown) => T },
|
||||||
|
|
13
db.ts
13
db.ts
|
@ -1,4 +1,4 @@
|
||||||
import { Display, Playlist, Song } from '@lyrics/models.ts'
|
import { AuthUser, Display, Playlist, Song, Team } from '@lyrics/models.ts'
|
||||||
|
|
||||||
export const kv = await Deno.openKv('lyrics')
|
export const kv = await Deno.openKv('lyrics')
|
||||||
|
|
||||||
|
@ -6,12 +6,14 @@ export const TablesForModels = {
|
||||||
'song': Song,
|
'song': Song,
|
||||||
'playlist': Playlist,
|
'playlist': Playlist,
|
||||||
'display': Display,
|
'display': Display,
|
||||||
|
'user': AuthUser,
|
||||||
|
'team': Team,
|
||||||
}
|
}
|
||||||
export type Table = keyof typeof TablesForModels
|
export type Table = keyof typeof TablesForModels
|
||||||
|
|
||||||
function crudFor<
|
function crudFor<
|
||||||
P extends keyof typeof TablesForModels,
|
|
||||||
T extends { id: string },
|
T extends { id: string },
|
||||||
|
P extends keyof typeof TablesForModels,
|
||||||
M extends { parse: (a: unknown) => T },
|
M extends { parse: (a: unknown) => T },
|
||||||
>(m: M, p: P) {
|
>(m: M, p: P) {
|
||||||
return {
|
return {
|
||||||
|
@ -42,6 +44,11 @@ export function isTable(value: string): value is Table {
|
||||||
|
|
||||||
export const db = {
|
export const db = {
|
||||||
song: crudFor(Song, 'song'),
|
song: crudFor(Song, 'song'),
|
||||||
playlist: crudFor(Playlist, 'playlist'),
|
playlist: crudFor(
|
||||||
|
Playlist,
|
||||||
|
'playlist',
|
||||||
|
),
|
||||||
display: crudFor(Display, 'display'),
|
display: crudFor(Display, 'display'),
|
||||||
|
user: crudFor(AuthUser, 'user'),
|
||||||
|
team: crudFor(Team, 'team'),
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,16 +21,18 @@
|
||||||
"**/_fresh/*"
|
"**/_fresh/*"
|
||||||
],
|
],
|
||||||
"imports": {
|
"imports": {
|
||||||
|
"zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
|
||||||
|
"saurpc": "https://deno.land/x/saurpc@0.5.0/mod.ts",
|
||||||
|
"bcrypt": "https://deno.land/x/bcrypt@v0.4.1/mod.ts",
|
||||||
"$fresh/": "https://deno.land/x/fresh@1.6.5/",
|
"$fresh/": "https://deno.land/x/fresh@1.6.5/",
|
||||||
"preact": "https://esm.sh/preact@10.19.2",
|
"preact": "https://esm.sh/preact@10.19.2",
|
||||||
"preact/": "https://esm.sh/preact@10.19.2/",
|
"preact/": "https://esm.sh/preact@10.19.2/",
|
||||||
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.1",
|
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.1",
|
||||||
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0",
|
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0",
|
||||||
"tailwindcss": "npm:tailwindcss@3.4.1",
|
"tailwindcss": "npm:tailwindcss@3.4.1",
|
||||||
"tailwindcss/": "npm:/tailwindcss@3.4.1/",
|
|
||||||
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
|
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
|
||||||
"@lyrics/": "./",
|
"@lyrics/": "./",
|
||||||
"$std/": "https://deno.land/std@0.211.0/"
|
"$std/": "https://deno.land/std@0.219.0/"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
|
@ -2,16 +2,16 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1708118438,
|
"lastModified": 1710085324,
|
||||||
"narHash": "sha256-kk9/0nuVgA220FcqH/D2xaN6uGyHp/zoxPNUmPCMmEE=",
|
"narHash": "sha256-OV+dGVjOFuGrdutv+7SFNf+8ArJE3QuUKPKeRziZVVY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "5863c27340ba4de8f83e7e3c023b9599c3cb3c80",
|
"rev": "91686dd2ff9e091f5010241de9d1de0a9388c7c8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-unstable",
|
"ref": "master",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/master";
|
||||||
outputs = {
|
outputs = {
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import * as $_app from './routes/_app.tsx'
|
||||||
import * as $api_db from './routes/api/db.ts'
|
import * as $api_db from './routes/api/db.ts'
|
||||||
import * as $api_db_table_ from './routes/api/db/[table].ts'
|
import * as $api_db_table_ from './routes/api/db/[table].ts'
|
||||||
import * as $api_joke from './routes/api/joke.ts'
|
import * as $api_joke from './routes/api/joke.ts'
|
||||||
|
import * as $api_rpc from './routes/api/rpc.ts'
|
||||||
import * as $greet_name_ from './routes/greet/[name].tsx'
|
import * as $greet_name_ from './routes/greet/[name].tsx'
|
||||||
import * as $index from './routes/index.tsx'
|
import * as $index from './routes/index.tsx'
|
||||||
import * as $Counter from './islands/Counter.tsx'
|
import * as $Counter from './islands/Counter.tsx'
|
||||||
|
@ -19,6 +20,7 @@ const manifest = {
|
||||||
'./routes/api/db.ts': $api_db,
|
'./routes/api/db.ts': $api_db,
|
||||||
'./routes/api/db/[table].ts': $api_db_table_,
|
'./routes/api/db/[table].ts': $api_db_table_,
|
||||||
'./routes/api/joke.ts': $api_joke,
|
'./routes/api/joke.ts': $api_joke,
|
||||||
|
'./routes/api/rpc.ts': $api_rpc,
|
||||||
'./routes/greet/[name].tsx': $greet_name_,
|
'./routes/greet/[name].tsx': $greet_name_,
|
||||||
'./routes/index.tsx': $index,
|
'./routes/index.tsx': $index,
|
||||||
},
|
},
|
||||||
|
|
53
models.ts
53
models.ts
|
@ -1,10 +1,49 @@
|
||||||
import { z } from 'https://deno.land/x/zod@v3.22.4/mod.ts'
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
// shared
|
||||||
export const Identifiable = z.object({
|
export const Identifiable = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().ulid(),
|
||||||
})
|
})
|
||||||
export type TIdentifiable = z.infer<typeof Identifiable>
|
export type TIdentifiable = z.infer<typeof Identifiable>
|
||||||
|
|
||||||
|
export const Creatable = z.object({
|
||||||
|
createdAt: z.date(),
|
||||||
|
})
|
||||||
|
export type TCreatable = z.infer<typeof Creatable>
|
||||||
|
|
||||||
|
export const Updatable = z.object({
|
||||||
|
updatedAt: z.date(),
|
||||||
|
})
|
||||||
|
export type TUpdatable = z.infer<typeof Updatable>
|
||||||
|
|
||||||
|
// lyrics
|
||||||
|
export const Role = z.enum([
|
||||||
|
'admin', // full permissions
|
||||||
|
'editor', // edit songs, verses, displays, playlists, etc.
|
||||||
|
'viewer', // cannot change anything, but can view everything
|
||||||
|
])
|
||||||
|
export type TRole = z.infer<typeof Role>
|
||||||
|
|
||||||
|
export const Team = Identifiable.merge(Creatable).merge(z.object({
|
||||||
|
name: z.string(),
|
||||||
|
members: z.record(z.string().ulid(), z.set(Role)),
|
||||||
|
}))
|
||||||
|
export type TTeam = z.infer<typeof Team>
|
||||||
|
|
||||||
|
export const User = Identifiable.merge(Creatable).merge(z.object({
|
||||||
|
username: z.string(),
|
||||||
|
}))
|
||||||
|
export type TUser = z.infer<typeof User>
|
||||||
|
|
||||||
|
export const AuthUser = User.merge(z.object({
|
||||||
|
passwordDigest: z.string(),
|
||||||
|
}))
|
||||||
|
export type TAuthUser = z.infer<typeof AuthUser>
|
||||||
|
|
||||||
|
export function toUser({ passwordDigest: _, ...user }: TAuthUser): TUser {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
export const Verse = z.object({
|
export const Verse = z.object({
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
})
|
})
|
||||||
|
@ -16,25 +55,27 @@ export const Map = z.object({
|
||||||
export type TMap = z.infer<typeof Map>
|
export type TMap = z.infer<typeof Map>
|
||||||
|
|
||||||
export const Song = Identifiable.merge(z.object({
|
export const Song = Identifiable.merge(z.object({
|
||||||
title: z.string(),
|
name: z.string(),
|
||||||
verses: z.record(z.string(), Verse),
|
verses: z.record(z.string(), Verse),
|
||||||
maps: z.record(z.string(), Map),
|
maps: z.record(z.string(), Map),
|
||||||
}))
|
}))
|
||||||
export type TSong = z.infer<typeof Song>
|
export type TSong = z.infer<typeof Song>
|
||||||
|
|
||||||
export const PlaylistEntry = z.object({
|
export const PlaylistEntry = z.object({
|
||||||
songKey: z.string().uuid(),
|
songId: z.string().ulid(),
|
||||||
mapKey: z.string(),
|
mapKey: z.string(),
|
||||||
})
|
})
|
||||||
export type TPlaylistEntry = z.infer<typeof PlaylistEntry>
|
export type TPlaylistEntry = z.infer<typeof PlaylistEntry>
|
||||||
|
|
||||||
export const Playlist = Identifiable.merge(z.object({
|
export const Playlist = Identifiable.merge(z.object({
|
||||||
|
name: z.string(),
|
||||||
entries: z.array(PlaylistEntry),
|
entries: z.array(PlaylistEntry),
|
||||||
}))
|
}))
|
||||||
export type TPlaylist = z.infer<typeof Playlist>
|
export type TPlaylist = z.infer<typeof Playlist>
|
||||||
|
|
||||||
export const Display = Identifiable.merge(z.object({
|
export const Display = Identifiable.merge(z.object({
|
||||||
playlistKey: z.string().uuid(),
|
name: z.string(),
|
||||||
songKeyIndex: z.number(),
|
playlistId: z.string().ulid(),
|
||||||
|
songIndex: z.number(),
|
||||||
}))
|
}))
|
||||||
export type TDisplay = z.infer<typeof Display>
|
export type TDisplay = z.infer<typeof Display>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { FreshContext, Handlers } from '$fresh/server.ts'
|
import { FreshContext } from '$fresh/server.ts'
|
||||||
import { knownMethods } from '$fresh/server/router.ts'
|
|
||||||
import { isTable } from '@lyrics/db.ts'
|
import { isTable } from '@lyrics/db.ts'
|
||||||
import { crudHandlerFor } from '@lyrics/api.ts'
|
import { crudHandlerFor } from '@lyrics/api.ts'
|
||||||
import { Display, Playlist, Song } from '@lyrics/models.ts'
|
import { Display, Playlist, Song } from '@lyrics/models.ts'
|
||||||
|
@ -10,10 +9,6 @@ export const subHandlers = {
|
||||||
display: crudHandlerFor(Display, 'display'),
|
display: crudHandlerFor(Display, 'display'),
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMethod(value: string): value is Table {
|
|
||||||
return (value in [])
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handler = (req: Request, ctx: FreshContext): Response => {
|
export const handler = (req: Request, ctx: FreshContext): Response => {
|
||||||
const table = ctx.params.table
|
const table = ctx.params.table
|
||||||
req.method
|
req.method
|
||||||
|
|
51
routes/api/rpc.ts
Normal file
51
routes/api/rpc.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// import { FreshContext } from '$fresh/server.ts'
|
||||||
|
import { TablesForModels } from '@lyrics/db.ts'
|
||||||
|
import { handleRpcRequest, ProcedureServerError } from 'saurpc'
|
||||||
|
import { rpcs } from '@lyrics/rpc.ts'
|
||||||
|
|
||||||
|
export type Method = 'GET' | 'PUT' | 'DELETE'
|
||||||
|
|
||||||
|
export function isMethod(value: string): value is Method {
|
||||||
|
return (value in TablesForModels)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toErrorResponse(e: ProcedureServerError) {
|
||||||
|
const { type, message } = e
|
||||||
|
const headers = { 'content-type': 'application/json' }
|
||||||
|
switch (type) {
|
||||||
|
case 'rpc_not_found':
|
||||||
|
console.info('RPC Not Found:', e)
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ type, message }),
|
||||||
|
{ status: 404, headers },
|
||||||
|
)
|
||||||
|
case 'invalid_request':
|
||||||
|
console.info('Invalid RPC:', e)
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ type, message }),
|
||||||
|
{ status: 400, headers },
|
||||||
|
)
|
||||||
|
case 'exception_caught':
|
||||||
|
console.error('RPC Exception:', e)
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ type }),
|
||||||
|
{ status: 500, headers },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handler = async (
|
||||||
|
req: Request,
|
||||||
|
// _ctx: FreshContext,
|
||||||
|
): Promise<Response> => {
|
||||||
|
try {
|
||||||
|
return await handleRpcRequest(req, rpcs)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ProcedureServerError) {
|
||||||
|
return toErrorResponse(e)
|
||||||
|
} else {
|
||||||
|
console.error('Error handling RPC request:', e)
|
||||||
|
return new Response('unknown error occurred', { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
rpc.ts
Normal file
27
rpc.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { hash } from 'bcrypt'
|
||||||
|
// import { User } from './models.ts'
|
||||||
|
import { TAuthUser, toUser, TUser } from '@lyrics/models.ts'
|
||||||
|
import { ulid } from '$std/ulid/mod.ts'
|
||||||
|
import { db } from '@lyrics/db.ts'
|
||||||
|
|
||||||
|
export const rpcs = {
|
||||||
|
'ping': (): 'pong' => {
|
||||||
|
console.log('received ping')
|
||||||
|
return 'pong'
|
||||||
|
},
|
||||||
|
'getVersion': () => '0.1.0',
|
||||||
|
async registerUser(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<TUser> {
|
||||||
|
const user: TAuthUser = {
|
||||||
|
id: ulid(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
username: username,
|
||||||
|
passwordDigest: await hash(password),
|
||||||
|
}
|
||||||
|
const result = await db.user.save(user)
|
||||||
|
if (!result.ok) throw new Error('failed to save user')
|
||||||
|
return toUser(user)
|
||||||
|
},
|
||||||
|
}
|
Loading…
Reference in a new issue