feat(api): implement basic rest api draft

This commit is contained in:
Julien Oculi 2024-07-16 15:08:11 +02:00
parent 90cfd257aa
commit e652336485
6 changed files with 155 additions and 6 deletions

View file

@ -1,3 +1,3 @@
{ {
"conventionalCommits.scopes": ["model", "chore", "db", "task"] "conventionalCommits.scopes": ["model", "chore", "db", "task", "api"]
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@cohabit/resources-manager", "name": "@cohabit/resources-manager",
"version": "0.1.2", "version": "0.2.0",
"exports": { "exports": {
"./models": "./src/models/mod.ts", "./models": "./src/models/mod.ts",
"./db": "./src/db/mod.ts", "./db": "./src/db/mod.ts",

View file

@ -1,7 +1,8 @@
import type { Db, Resource } from '../../../mod.ts' import type { Db } from '../db/mod.ts'
import type { UUID } from '../../../types.ts' import type { Resource } from '../models/mod.ts'
import type { ResourceBuilder, ResourceType } from '../types.ts' import type { UUID } from '../../types.ts'
import { respondJson } from '../utils.ts' import type { ResourceBuilder, ResourceType } from './types.ts'
import { respondJson } from './utils.ts'
export async function resourceHandler<T extends Resource>( export async function resourceHandler<T extends Resource>(
type: ResourceType<T>, type: ResourceType<T>,

81
src/api/server.ts Normal file
View file

@ -0,0 +1,81 @@
// /(users|groups|machines)/:?id
import { stripTypeS } from '../api/utils.ts'
import type { Db } from '../db/mod.ts'
import { Credential, Group, Machine, Service, User } from '../models/mod.ts'
import type { CredentialCategory } from '../models/src/credential.ts'
import { resourceHandler } from './hander.ts'
export class Api {
static init(db: Db, { base = '/' }: { base: `/${string}` }) {
return new Api({ db, base })
}
#db: Db
#base: `/${string}`
private constructor({ db, base }: { db: Db; base: `/${string}` }) {
this.#db = db
this.#base = base
}
serve() {
return Deno.serve((req) => {
const url = new URL(req.url)
if (url.pathname.startsWith(this.#base)) {
const base = new URL(this.#base, req.url)
return router(req, base, this.#db)
}
return new Response(null, {
status: 301,
statusText: 'Not allowed base url',
})
})
}
}
function router(req: Request, base: URL, db: Db): Promise<Response> {
const pattern = new URLPattern({ pathname: '/:type/:id?' }, base.href)
const match = pattern.exec(req.url)
if (!match) {
return Promise.resolve(
new Response(null, {
status: 400,
statusText: 'Bad request',
}),
)
}
const { type, id } = match.pathname.groups as {
type: string
id: string | undefined
}
switch (type) {
case 'credentials':
return resourceHandler<Credential<CredentialCategory>>(
stripTypeS(type),
Credential,
db,
req,
id,
)
case 'groups':
return resourceHandler<Group>(stripTypeS(type), Group, db, req, id)
case 'machines':
return resourceHandler<Machine>(stripTypeS(type), Machine, db, req, id)
case 'services':
return resourceHandler<Service>(stripTypeS(type), Service, db, req, id)
case 'users':
return resourceHandler<User>(stripTypeS(type), User, db, req, id)
default:
return Promise.resolve(
new Response(null, {
status: 400,
statusText: 'Bad request',
}),
)
}
}

29
src/api/types.ts Normal file
View file

@ -0,0 +1,29 @@
import type {
Credential,
Group,
Machine,
Resource,
Service,
User,
} from '../../src/models/mod.ts'
import type { JsonValue } from '@std/json'
import type { CredentialCategory } from '../models/src/credential.ts'
export type ResourceBuilder<T extends Resource> = T extends
Credential<CredentialCategory> ? typeof Credential
: T extends Group ? typeof Group
: T extends Machine ? typeof Machine
: T extends Service ? typeof Service
: T extends User ? typeof User
: never
export type ResourceType<T extends Resource> = T extends
Credential<CredentialCategory> ? 'credential'
: T extends Group ? 'group'
: T extends Machine ? 'machine'
: T extends Service ? 'service'
: T extends User ? 'user'
: never
export type JsonObject = Record<string, JsonValue>
export type JsonStringifiable = { toJSON(): JsonObject } | JsonObject

38
src/api/utils.ts Normal file
View file

@ -0,0 +1,38 @@
import type { JsonStringifiable } from '../api/types.ts'
export function respondJson<T extends JsonStringifiable | Error>(
valueOrError: T,
): Response {
if (valueOrError instanceof Error) {
const { name, cause, message } = valueOrError
return new Response(
JSON.stringify({
result: 'error',
error: { name, cause, message },
}),
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
status: 500,
statusText: valueOrError.message,
},
)
}
return new Response(
JSON.stringify({
result: 'ok',
value: valueOrError,
}),
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
},
)
}
export function stripTypeS<T extends string>(typeS: `${T}s`): T {
return typeS.slice(0, -1) as T
}