Compare commits

..

No commits in common. "main" and "0.1.0" have entirely different histories.
main ... 0.1.0

21 changed files with 246 additions and 579 deletions

View file

@ -1 +0,0 @@
DB_PATH = './db.sqlite'

View file

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

View file

@ -1,3 +1,3 @@
# KM - Coh@bit resources manager # Coh@bit ressources
Système de gestion des ressources de cohabit. Server de gestion des ressources de cohabit.

View file

@ -1,13 +1,12 @@
{ {
"name": "@cohabit/resources-manager", "name": "@cohabit/ressources",
"version": "0.2.1", "version": "0.1.0",
"exports": { "exports": {
"./models": "./src/models/mod.ts", ".": "./mod.ts",
"./db": "./src/db/mod.ts", "./models": "./src/models/mod.ts"
"./types": "./types.ts"
}, },
"tasks": { "tasks": {
"start": "deno run ./mod.ts" "start": "deno run --unstable-kv ./mod.ts"
}, },
"fmt": { "fmt": {
"singleQuote": true, "singleQuote": true,
@ -15,11 +14,16 @@
"semiColons": false "semiColons": false
}, },
"imports": { "imports": {
"@/": "./",
"@models": "./src/models/mod.ts",
"@models/": "./src/models/src/",
"@api": "./src/api/server.ts",
"@api/": "./src/api/",
"@db": "./src/db/mod.ts",
"@db/": "./src/db/",
"@std/json": "jsr:@std/json@^0.223.0" "@std/json": "jsr:@std/json@^0.223.0"
}, },
"exclude": [ "exclude": [
"./docs", "./docs"
"./tmp" ]
],
"unstable": ["kv"]
} }

View file

@ -4,7 +4,6 @@
"specifiers": { "specifiers": {
"jsr:@std/json@^0.223.0": "jsr:@std/json@0.223.0", "jsr:@std/json@^0.223.0": "jsr:@std/json@0.223.0",
"jsr:@std/streams@^0.223.0": "jsr:@std/streams@0.223.0", "jsr:@std/streams@^0.223.0": "jsr:@std/streams@0.223.0",
"npm:iterator-polyfill": "npm:iterator-polyfill@1.0.9",
"npm:superjson@1.13.3": "npm:superjson@1.13.3" "npm:superjson@1.13.3": "npm:superjson@1.13.3"
}, },
"jsr": { "jsr": {
@ -29,10 +28,6 @@
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"dependencies": {} "dependencies": {}
}, },
"iterator-polyfill@1.0.9": {
"integrity": "sha512-YKBrosdKd9nqEaJwpYZjlnax8WtLwCbTo7gnUXrQ3ARA92vD2W8hdOWCRzvotHByDRCvPrFvEFF4M3V53lwAHQ==",
"dependencies": {}
},
"superjson@1.13.3": { "superjson@1.13.3": {
"integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==",
"dependencies": { "dependencies": {

2
mod.ts Normal file
View file

@ -0,0 +1,2 @@
export * from '@models'
export { Db } from '@db'

View file

@ -1,56 +0,0 @@
import type { Db } from '../db/mod.ts'
import type { Resource } from '../models/mod.ts'
import type { UUID } from '../../types.ts'
import type { ResourceBuilder, ResourceType } from './types.ts'
import { respondJson } from './utils.ts'
export async function resourceHandler<T extends Resource>(
type: ResourceType<T>,
Builder: ResourceBuilder<T>,
db: Db,
req: Request,
id?: string,
): Promise<Response> {
if (req.method === 'POST') {
try {
const json = await req.json() as T
try {
//@ts-expect-error Extends from Resource
const resource = Builder.create(json)
//@ts-expect-error not a generic
const result = await db.resource[type].set(resource)
if (result.ok) {
//@ts-expect-error generic to fix
return respondJson(resource)
} else {
return respondJson(new Error(`can't insert ${resource} in db`))
}
} catch (error) {
return respondJson(error)
}
} catch (error) {
return respondJson(error)
}
}
if (req.method === 'GET') {
if (id === undefined) {
return respondJson(new Error('missing "id" for "GET"'))
}
try {
const resource = await db.resource[type].get({ uuid: id as UUID })
//@ts-expect-error generic to fix
return respondJson(resource)
} catch {
return respondJson(
new Error(`can't find any ${type} with the current uuid ${id}`),
)
}
}
if (req.method === 'PATCH') {
throw new Error('not implemented')
}
if (req.method === 'DELETE') {
throw new Error('not implemented')
}
return respondJson(new Error(`method "${req.method}" is not allowed`))
}

View file

@ -1,81 +0,0 @@
// /(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',
}),
)
}
}

View file

@ -1,29 +0,0 @@
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

View file

@ -1,38 +0,0 @@
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
}

View file

@ -3,18 +3,18 @@ import {
Group, Group,
Machine, Machine,
type Ref, type Ref,
type Resource, type Ressource,
Service, Service,
User, User,
} from '../models/mod.ts' } from '@models'
import type { CredentialCategory } from '../models/src/credential.ts' import type { CredentialCategory } from '@models/credential.ts'
//!TODO link resources (get, list) //!TODO link ressources (get, list)
//!TODO Purge unused resources (delete) //!TODO Purge unused ressources (delete)
export class Db { export class Db {
#kv: Deno.Kv #kv: Deno.Kv
static async init(path?: string): Promise<Db> { static async init(path?: string) {
const kv = await Deno.openKv(path ?? Deno.env.get('DB_PATH')) const kv = await Deno.openKv(path ?? Deno.env.get('DB_PATH'))
return new Db(kv) return new Db(kv)
} }
@ -23,123 +23,124 @@ export class Db {
this.#kv = kv this.#kv = kv
} }
get storage(): Deno.Kv { get storage() {
return this.#kv return this.#kv
} }
get prefix(): { resource: 'resource' } { get prefix() {
return { return {
resource: 'resource', ressource: 'ressource',
} }
} }
get resource(): ResourceAccessors { get ressource() {
return { return {
credential: this.#resourceStorage<Credential<CredentialCategory>>( credential: this.#ressourceStorage<Credential<CredentialCategory>>(
'credential', 'credential',
Credential, Credential,
), ),
group: this.#resourceStorage<Group>('group', Group), group: this.#ressourceStorage<Group>('group', Group),
machine: this.#resourceStorage<Machine>('machine', Machine), machine: this.#ressourceStorage<Machine>('machine', Machine),
service: this.#resourceStorage<Service>('service', Service), service: this.#ressourceStorage<Service>('service', Service),
user: this.#resourceStorage<User>('user', User), user: this.#ressourceStorage<User>('user', User),
} }
} }
#resourceStorage<T extends Resource>( #ressourceStorage<T extends Ressource>(
type: ResourceType<T>, type: RessourceType<T>,
Builder: ResourceBuilder<T>, Builder: RessourceBuilder<T>,
): ResourceStorage<T> { ) {
return { return {
get: (resource: Pick<T, 'uuid'>) => this.#get<T>(type, Builder, resource), get: (ressource: Pick<T, 'uuid'>) =>
set: (resources: T[]) => this.#set<T>(type, resources), this.#get<T>(type, Builder, ressource),
delete: (resources: Pick<T, 'uuid'>[]) => set: (ressources: T[]) => this.#set<T>(type, ressources),
this.#delete<T>(type, resources), delete: (ressources: Pick<T, 'uuid'>[]) =>
this.#delete<T>(type, ressources),
list: ( list: (
filter: (resource: T) => boolean | Promise<boolean> = () => true, filter: (ressource: T) => boolean | Promise<boolean> = () => true,
) => this.#list<T>(type, Builder, filter), ) => this.#list<T>(type, Builder, filter),
listRef: async ( listRef: async (
filter: (resource: T) => boolean | Promise<boolean> = () => true, filter: (ressource: T) => boolean | Promise<boolean> = () => true,
) => { ) => {
const resources: Ref<T>[] = [] const ressources: Ref<T>[] = []
for await (const resource of this.#list<T>(type, Builder, filter)) { for await (const ressource of this.#list<T>(type, Builder, filter)) {
resources.push(resource.toRef() as Ref<T>) ressources.push(ressource.toRef() as Ref<T>)
} }
return resources return ressources
}, },
} }
} }
async #get<T extends Resource>( async #get<T extends Ressource>(
type: ResourceType<T>, type: RessourceType<T>,
Builder: ResourceBuilder<T>, Builder: RessourceBuilder<T>,
entry: Pick<T, 'uuid'>, entry: Pick<T, 'uuid'>,
): Promise<T> { ): Promise<T> {
const json = await this.#kv.get<ResourceJson<T>>([ const json = await this.#kv.get<RessourceJson<T>>([
this.prefix.resource, this.prefix.ressource,
type, type,
entry.uuid, entry.uuid,
]) ])
if (json.value) { if (json.value) {
//@ts-expect-error Type union of Resource types for Builder //@ts-expect-error Type union of Ressource types for Builder
return Builder.fromJSON(json.value) return Builder.fromJSON(json.value)
} }
throw new ReferenceError( throw new ReferenceError(
`no resource.${type} was found with uuid: "${entry.uuid}"`, `no ressource.${type} was found with uuid: "${entry.uuid}"`,
) )
} }
#set<T extends Resource>(type: ResourceType<T>, entries: T[]) { #set<T extends Ressource>(type: RessourceType<T>, entries: T[]) {
const atomic = this.#kv.atomic() const atomic = this.#kv.atomic()
for (const entry of entries) { for (const entry of entries) {
//! TODO check if `refs` exists //! TODO check if `refs` exists
const key = [this.prefix.resource, type, entry.uuid] const key = [this.prefix.ressource, type, entry.uuid]
atomic.set(key, entry.toJSON()) atomic.set(key, entry.toJSON())
} }
return atomic.commit() return atomic.commit()
} }
#delete<T extends Resource>( #delete<T extends Ressource>(
type: ResourceType<T>, type: RessourceType<T>,
entries: Pick<T, 'uuid'>[], entries: Pick<T, 'uuid'>[],
): Promise<Deno.KvCommitResult | Deno.KvCommitError> { ): Promise<Deno.KvCommitResult | Deno.KvCommitError> {
const atomic = this.#kv.atomic() const atomic = this.#kv.atomic()
for (const entry of entries) { for (const entry of entries) {
//! TODO check if `refs` exists //! TODO check if `refs` exists
atomic.delete([this.prefix.resource, type, entry.uuid]) atomic.delete([this.prefix.ressource, type, entry.uuid])
} }
return atomic.commit() return atomic.commit()
} }
async *#list<T extends Resource>( async *#list<T extends Ressource>(
type: ResourceType<T>, type: RessourceType<T>,
Builder: ResourceBuilder<T>, Builder: RessourceBuilder<T>,
filter: (entry: T) => boolean | Promise<boolean>, filter: (entry: T) => boolean | Promise<boolean>,
): AsyncGenerator<T, void, void> { ): AsyncGenerator<T, void, void> {
const list = this.#kv.list<ResourceJson<T>>({ const list = this.#kv.list<RessourceJson<T>>({
prefix: [this.prefix.resource, type], prefix: [this.prefix.ressource, type],
}) })
for await (const entry of list) { for await (const entry of list) {
const value = entry.value const value = entry.value
//@ts-expect-error Type union of Resource types for Builder //@ts-expect-error Type union of Ressource types for Builder
const resource = Builder.fromJSON(value) as T const ressource = Builder.fromJSON(value) as T
if (await filter(resource)) { if (await filter(ressource)) {
yield resource yield ressource
} }
} }
} }
} }
type ResourceType<T extends Resource> = T extends Credential<CredentialCategory> type RessourceType<T extends Ressource> = T extends
? 'credential' Credential<CredentialCategory> ? 'credential'
: T extends Group ? 'group' : T extends Group ? 'group'
: T extends Machine ? 'machine' : T extends Machine ? 'machine'
: T extends Service ? 'service' : T extends Service ? 'service'
: T extends User ? 'user' : T extends User ? 'user'
: never : never
type ResourceBuilder<T extends Resource> = T extends type RessourceBuilder<T extends Ressource> = T extends
Credential<CredentialCategory> ? typeof Credential Credential<CredentialCategory> ? typeof Credential
: T extends Group ? typeof Group : T extends Group ? typeof Group
: T extends Machine ? typeof Machine : T extends Machine ? typeof Machine
@ -147,26 +148,4 @@ type ResourceBuilder<T extends Resource> = T extends
: T extends User ? typeof User : T extends User ? typeof User
: never : never
type ResourceJson<T extends Resource> = ReturnType<T['toJSON']> type RessourceJson<T extends Ressource> = ReturnType<T['toJSON']>
type ResourceStorage<T extends Resource> = {
get: (resource: Pick<T, 'uuid'>) => Promise<T>
set: (resources: T[]) => Promise<Deno.KvCommitResult | Deno.KvCommitError>
delete: (
resources: Pick<T, 'uuid'>[],
) => Promise<Deno.KvCommitResult | Deno.KvCommitError>
list: (
filter?: (resource: T) => boolean | Promise<boolean>,
) => AsyncGenerator<T, void, void>
listRef: (
filter?: (resource: T) => boolean | Promise<boolean>,
) => Promise<Ref<T>[]>
}
type ResourceAccessors = {
credential: ResourceStorage<Credential<CredentialCategory>>
group: ResourceStorage<Group>
machine: ResourceStorage<Machine>
service: ResourceStorage<Service>
user: ResourceStorage<User>
}

View file

@ -1,13 +1,11 @@
export { export {
Credential, Ref,
type CredentialCategory, type RefResolver,
type CredentialStore, type RefString,
type Passkey, } from '@/src/models/utils/ref.ts'
} from './src/credential.ts' export { Credential } from '@models/credential.ts'
export { Group, type GroupPermissions } from './src/group.ts' export { Group } from '@models/group.ts'
export { Machine, type MachineStatus } from './src/machine.ts' export { Machine } from '@models/machine.ts'
export type { Resource, ResourceJson, ResourceRefJson } from './src/resource.ts' export type { Ressource } from '@models/ressource.ts'
export { Service, type ServiceCategory } from './src/service.ts' export { Service } from '@models/service.ts'
export { User } from './src/user.ts' export { User } from '@models/user.ts'
export { Avatar } from './utils/avatar.ts'
export { Ref, type RefResolver, type RefString } from './utils/ref.ts'

View file

@ -1,9 +1,9 @@
import type { Base64String, ToJson, UUID } from '../../../types.ts' import type { Base64String, ToJson, UUID } from '@/types.ts'
import { Avatar } from '../utils/avatar.ts' import { Ressource } from '@models/ressource.ts'
import type { Ref } from '../utils/ref.ts' import type { Ref } from '@models'
import { Resource, type ResourceJson } from './resource.ts' import { Avatar } from '@/src/models/utils/avatar.ts'
export class Credential<T extends CredentialCategory> extends Resource { export class Credential<T extends CredentialCategory> extends Ressource {
static fromJSON<T extends CredentialCategory>( static fromJSON<T extends CredentialCategory>(
json: ToJson<Credential<T>>, json: ToJson<Credential<T>>,
): Credential<T> { ): Credential<T> {
@ -36,19 +36,19 @@ export class Credential<T extends CredentialCategory> extends Resource {
}: { }: {
name: Credential<T>['name'] name: Credential<T>['name']
category: T category: T
store: CredentialStore<T>['store'] store: Credential<T>['store']
avatar?: Credential<T>['avatar'] avatar?: Credential<T>['avatar']
}): Credential<T> { }): Credential<T> {
return this.create({ category, store, name, avatar }) return this.create({ category, store, name, avatar })
} }
#category: T #category: T
#store: Readonly<CredentialStore<T>['store']> #store: Readonly<CredentialStore<T>>
private constructor( private constructor(
{ category, store, ...props }: { category, store, ...props }:
& Pick<Credential<T>, 'category' | 'store'> & Pick<Credential<T>, 'category' | 'store'>
& Pick<Resource, 'name' | 'uuid' | 'avatar' | 'createdAt' | 'updatedAt'>, & Pick<Ressource, 'name' | 'uuid' | 'avatar' | 'createdAt' | 'updatedAt'>,
) { ) {
super(props) super(props)
@ -65,31 +65,23 @@ export class Credential<T extends CredentialCategory> extends Resource {
return 'credential' return 'credential'
} }
get category(): T { get category() {
return this.#category return this.#category
} }
get store(): Readonly<CredentialStore<T>['store']> { get store() {
return this.#store return this.#store
} }
update( update<T extends CredentialCategory>(
props: Partial<Omit<Credential<T>, 'type' | 'uuid' | 'createdAt'>>, props: Partial<Omit<Credential<T>, 'type' | 'uuid' | 'createdAt'>>,
): Credential<T> { ): Credential<T> {
const { updatedAt } = super.update(props) const { updatedAt } = super.update(props)
return new Credential<T>({ const credential = new Credential<T>({ ...this, ...props, updatedAt })
uuid: this.uuid, return credential
name: this.name,
avatar: this.avatar,
createdAt: this.createdAt,
category: this.category,
store: this.store,
...props,
updatedAt,
})
} }
toJSON(): ResourceJson<Credential<T>, 'category' | 'store'> { toJSON() {
return { return {
...super.toJSON(), ...super.toJSON(),
type: this.type, type: this.type,
@ -107,10 +99,10 @@ export class Credential<T extends CredentialCategory> extends Resource {
} }
} }
export interface Credential<T extends CredentialCategory> extends Resource { export interface Credential<T extends CredentialCategory> extends Ressource {
type: 'credential' type: 'credential'
category: T category: T
store: Readonly<CredentialStore<T>['store']> store: Readonly<CredentialStore<T>>
} }
export type CredentialCategory = 'password' | 'ssh' | 'passkey' export type CredentialCategory = 'password' | 'ssh' | 'passkey'

View file

@ -1,13 +1,9 @@
import type { Posix, ToJson, UUID } from '../../../types.ts' import type { Posix, ToJson, UUID } from '@/types.ts'
import { Avatar } from '../utils/avatar.ts' import { Ressource } from '@models/ressource.ts'
import { Ref } from '../utils/ref.ts' import { Ref } from '@models'
import { import { Avatar } from '@/src/models/utils/avatar.ts'
Resource,
type ResourceJson,
type ResourceRefJson,
} from './resource.ts'
export class Group extends Resource { export class Group extends Ressource {
static fromJSON( static fromJSON(
{ posix, groups, ...json }: ToJson<Group>, { posix, groups, ...json }: ToJson<Group>,
): Group { ): Group {
@ -54,13 +50,19 @@ export class Group extends Resource {
} }
#posix?: Posix #posix?: Posix
#permissions: Readonly<GroupPermissions> #permissions: Readonly<{
[serviceOrMachine: UUID]: {
read: boolean
write: boolean
execute: boolean
}
}>
#groups: readonly Ref<Group>[] #groups: readonly Ref<Group>[]
private constructor( private constructor(
{ posix, permissions, groups, ...props }: { posix, permissions, groups, ...props }:
& Pick<Group, 'posix' | 'permissions' | 'groups'> & Pick<Group, 'posix' | 'permissions' | 'groups'>
& Pick<Resource, 'name' | 'avatar' | 'uuid' | 'createdAt' | 'updatedAt'>, & Pick<Ressource, 'name' | 'avatar' | 'uuid' | 'createdAt' | 'updatedAt'>,
) { ) {
super(props) super(props)
@ -91,37 +93,25 @@ export class Group extends Resource {
return 'group' return 'group'
} }
get posix(): Posix | undefined { get posix() {
return this.#posix return this.#posix
} }
get permissions(): Readonly<GroupPermissions> { get permissions() {
return this.#permissions return this.#permissions
} }
get groups(): readonly Ref<Group>[] { get groups() {
return this.#groups return this.#groups
} }
update(props: Partial<Omit<Group, 'type' | 'uuid' | 'createdAt'>>): Group { update(props: Partial<Omit<Group, 'type' | 'uuid' | 'createdAt'>>): Group {
const { updatedAt } = super.update(props) const { updatedAt } = super.update(props)
return new Group({ const group = new Group({ ...this, ...props, updatedAt })
uuid: this.uuid, return group
name: this.name,
avatar: this.avatar,
createdAt: this.createdAt,
posix: this.posix,
permissions: this.permissions,
groups: this.groups,
...props,
updatedAt,
})
} }
toJSON(): toJSON() {
& ResourceJson<Group, 'permissions'>
& { posix: Posix | null }
& ResourceRefJson<Group, Group, 'groups'> {
return { return {
...super.toJSON(), ...super.toJSON(),
type: this.type, type: this.type,
@ -140,17 +130,15 @@ export class Group extends Resource {
} }
} }
export interface Group extends Resource { export interface Group extends Ressource {
type: 'group' type: 'group'
posix: Posix | undefined posix: Posix | undefined
groups: readonly Ref<Group>[] groups: readonly Ref<Group>[]
permissions: Readonly<GroupPermissions> permissions: Readonly<{
} [serviceOrMachine: UUID]: {
read: boolean
export type GroupPermissions = { write: boolean
[serviceOrMachine: UUID]: { execute: boolean
read: boolean }
write: boolean }>
execute: boolean
}
} }

View file

@ -1,14 +1,10 @@
import type { ToJson, UrlString } from '../../../types.ts' import { Ref } from '@/src/models/utils/ref.ts'
import { Avatar } from '../utils/avatar.ts' import type { ToJson, UrlString } from '@/types.ts'
import { Ref } from '../utils/ref.ts' import type { Group } from '@models/group.ts'
import type { Group } from './group.ts' import { Ressource } from '@models/ressource.ts'
import { import { Avatar } from '@/src/models/utils/avatar.ts'
Resource,
type ResourceJson,
type ResourceRefJson,
} from './resource.ts'
export class Machine extends Resource { export class Machine extends Ressource {
static fromJSON( static fromJSON(
{ url, ...json }: ToJson<Machine>, { url, ...json }: ToJson<Machine>,
): Machine { ): Machine {
@ -18,7 +14,7 @@ export class Machine extends Resource {
} }
static create( static create(
{ tags, url, status, groups, avatar, name }: Pick< { tags, url, status, groups, avatar }: Pick<
Machine, Machine,
'name' | 'tags' | 'url' | 'status' | 'avatar' | 'groups' 'name' | 'tags' | 'url' | 'status' | 'avatar' | 'groups'
>, >,
@ -57,13 +53,19 @@ export class Machine extends Resource {
#tags: readonly string[] #tags: readonly string[]
#url: UrlString #url: UrlString
#status: MachineStatus #status:
| 'ready'
| 'busy'
| 'unavailable'
| 'discontinued'
| 'error'
| 'unknown'
#groups: readonly Ref<Group>[] #groups: readonly Ref<Group>[]
private constructor( private constructor(
{ tags, url, status, groups, ...props }: { tags, url, status, groups, ...props }:
& Pick<Machine, 'tags' | 'url' | 'status' | 'avatar' | 'groups'> & Pick<Machine, 'tags' | 'url' | 'status' | 'avatar' | 'groups'>
& Pick<Resource, 'name' | 'uuid' | 'avatar' | 'createdAt' | 'updatedAt'>, & Pick<Ressource, 'name' | 'uuid' | 'avatar' | 'createdAt' | 'updatedAt'>,
) { ) {
super(props) super(props)
@ -81,16 +83,16 @@ export class Machine extends Resource {
get type(): 'machine' { get type(): 'machine' {
return 'machine' return 'machine'
} }
get tags(): readonly string[] { get tags() {
return this.#tags return this.#tags
} }
get url(): UrlString { get url() {
return this.#url return this.#url
} }
get status(): MachineStatus { get status() {
return this.#status return this.#status
} }
get groups(): readonly Ref<Group>[] { get groups() {
return this.#groups return this.#groups
} }
@ -98,23 +100,11 @@ export class Machine extends Resource {
props: Partial<Omit<Machine, 'type' | 'uuid' | 'createdAt'>>, props: Partial<Omit<Machine, 'type' | 'uuid' | 'createdAt'>>,
): Machine { ): Machine {
const { updatedAt } = super.update(props) const { updatedAt } = super.update(props)
return new Machine({ const machine = new Machine({ ...this, ...props, updatedAt })
uuid: this.uuid, return machine
name: this.name,
avatar: this.avatar,
createdAt: this.createdAt,
tags: this.tags,
url: this.url,
status: this.status,
groups: this.groups,
...props,
updatedAt,
})
} }
toJSON(): toJSON() {
& ResourceJson<Machine, 'tags' | 'url' | 'status'>
& ResourceRefJson<Machine, Group, 'groups'> {
return { return {
...super.toJSON(), ...super.toJSON(),
type: this.type, type: this.type,
@ -134,18 +124,16 @@ export class Machine extends Resource {
} }
} }
export interface Machine extends Resource { export interface Machine extends Ressource {
type: 'machine' type: 'machine'
tags: readonly string[] tags: readonly string[]
url: UrlString url: UrlString
status: MachineStatus status:
| 'ready'
| 'busy'
| 'unavailable'
| 'discontinued'
| 'error'
| 'unknown'
groups: readonly Ref<Group>[] groups: readonly Ref<Group>[]
} }
export type MachineStatus =
| 'ready'
| 'busy'
| 'unavailable'
| 'discontinued'
| 'error'
| 'unknown'

View file

@ -1,12 +1,12 @@
import type { DateString, ToJson, UrlString, UUID } from '../../../types.ts' import { Ref } from '@/src/models/utils/ref.ts'
import { regex } from '../../../utils.ts' import type { DateString, ToJson, UrlString, UUID } from '@/types.ts'
import { Ref } from '../utils/ref.ts' import { regex } from '@/utils.ts'
export class Resource { export class Ressource {
static fromJSON( static fromJSON(
json: ToJson<Resource>, json: ToJson<Ressource>,
): Resource { ): Ressource {
return new Resource({ return new Ressource({
name: json.name, name: json.name,
avatar: json.avatar, avatar: json.avatar,
uuid: json.uuid, uuid: json.uuid,
@ -16,16 +16,16 @@ export class Resource {
} }
static create( static create(
{ name, avatar }: Pick<Resource, 'name' | 'avatar'>, { name, avatar }: Pick<Ressource, 'name' | 'avatar'>,
): Resource { ): Ressource {
const uuid = crypto.randomUUID() as UUID const uuid = crypto.randomUUID() as UUID
const createdAt = new Date().toISOString() as DateString const createdAt = new Date().toISOString() as DateString
const updatedAt = createdAt const updatedAt = createdAt
return new Resource({ name, avatar, uuid, createdAt, updatedAt }) return new Ressource({ name, avatar, uuid, createdAt, updatedAt })
} }
static load(_options: never): Resource { static load(_options: never): Ressource {
throw new Error('not implemented') throw new Error('not implemented')
} }
@ -37,7 +37,7 @@ export class Resource {
protected constructor( protected constructor(
{ name, avatar, uuid, createdAt, updatedAt }: Pick< { name, avatar, uuid, createdAt, updatedAt }: Pick<
Resource, Ressource,
'name' | 'avatar' | 'uuid' | 'createdAt' | 'updatedAt' 'name' | 'avatar' | 'uuid' | 'createdAt' | 'updatedAt'
>, >,
) { ) {
@ -83,22 +83,14 @@ export class Resource {
} }
update( update(
props: Partial<Omit<Resource, 'type' | 'uuid' | 'createdAt'>>, props: Partial<Omit<Ressource, 'type' | 'uuid' | 'createdAt'>>,
): Resource { ): Ressource {
return new Resource({ const updated = new Ressource({ ...this, ...props })
name: this.name, updated.#updatedAt = new Date().toISOString() as DateString
avatar: this.avatar, return updated
uuid: this.uuid,
createdAt: this.createdAt,
updatedAt: new Date().toISOString() as DateString,
...props,
})
} }
toJSON(): ResourceJson< toJSON() {
Resource,
'type' | 'uuid' | 'name' | 'avatar' | 'createdAt' | 'updatedAt'
> {
return { return {
type: this.type, type: this.type,
uuid: this.uuid, uuid: this.uuid,
@ -113,12 +105,12 @@ export class Resource {
return `Ressource (${JSON.stringify(this)})` return `Ressource (${JSON.stringify(this)})`
} }
toRef(): Ref<Resource> { toRef(): Ref<Ressource> {
return Ref.fromResource(this) return Ref.fromRessource(this)
} }
} }
export interface Resource { export interface Ressource {
type: 'user' | 'machine' | 'service' | 'group' | 'credential' type: 'user' | 'machine' | 'service' | 'group' | 'credential'
uuid: UUID uuid: UUID
name: string name: string
@ -126,24 +118,3 @@ export interface Resource {
createdAt: DateString createdAt: DateString
updatedAt: DateString updatedAt: DateString
} }
export type ResourceJson<T extends Resource, K extends keyof T> = Readonly<
Pick<
T,
| 'type'
| 'uuid'
| 'name'
| 'avatar'
| 'createdAt'
| 'updatedAt'
| K
>
>
export type ResourceRefJson<
T extends Resource,
Ref extends Resource,
K extends keyof T,
> = {
[k in K]: Readonly<ReturnType<ReturnType<Ref['toRef']>['toString']>[]>
}

View file

@ -1,14 +1,10 @@
import type { ToJson, UrlString } from '../../../types.ts' import { Ref } from '@/src/models/utils/ref.ts'
import { Avatar } from '../utils/avatar.ts' import type { ToJson, UrlString } from '@/types.ts'
import { Ref } from '../utils/ref.ts' import type { Group } from '@models/group.ts'
import type { Group } from './group.ts' import { Ressource } from '@models/ressource.ts'
import { import { Avatar } from '@/src/models/utils/avatar.ts'
Resource,
type ResourceJson,
type ResourceRefJson,
} from './resource.ts'
export class Service extends Resource { export class Service extends Ressource {
static fromJSON( static fromJSON(
{ groups, ...json }: ToJson<Service>, { groups, ...json }: ToJson<Service>,
): Service { ): Service {
@ -56,7 +52,7 @@ export class Service extends Resource {
return this.create({ name, avatar, tags, url, category, groups }) return this.create({ name, avatar, tags, url, category, groups })
} }
#category: ServiceCategory #category: 'web' | 'fs' | 'git'
#url: UrlString #url: UrlString
#groups: readonly Ref<Group>[] #groups: readonly Ref<Group>[]
#tags: readonly string[] #tags: readonly string[]
@ -66,7 +62,7 @@ export class Service extends Resource {
tags, tags,
url, url,
groups, groups,
...resource ...ressource
}: Pick< }: Pick<
Service, Service,
| 'uuid' | 'uuid'
@ -79,7 +75,7 @@ export class Service extends Resource {
| 'groups' | 'groups'
| 'avatar' | 'avatar'
>) { >) {
super(resource) super(ressource)
if (!['web', 'fs', 'git'].includes(category)) { if (!['web', 'fs', 'git'].includes(category)) {
throw new TypeError( throw new TypeError(
@ -96,19 +92,19 @@ export class Service extends Resource {
return 'service' return 'service'
} }
get category(): ServiceCategory { get category() {
return this.#category return this.#category
} }
get tags(): readonly string[] { get tags() {
return this.#tags return this.#tags
} }
get url(): UrlString { get url() {
return this.#url return this.#url
} }
get groups(): readonly Ref<Group>[] { get groups() {
return this.#groups return this.#groups
} }
@ -116,23 +112,11 @@ export class Service extends Resource {
props: Partial<Omit<Service, 'type' | 'uuid' | 'createdAt'>>, props: Partial<Omit<Service, 'type' | 'uuid' | 'createdAt'>>,
): Service { ): Service {
const { updatedAt } = super.update(props) const { updatedAt } = super.update(props)
return new Service({ const service = new Service({ ...this, ...props, updatedAt })
uuid: this.uuid, return service
name: this.name,
createdAt: this.createdAt,
avatar: this.avatar,
category: this.category,
url: this.url,
tags: this.tags,
groups: this.groups,
...props,
updatedAt,
})
} }
toJSON(): toJSON() {
& ResourceJson<Service, 'category' | 'tags' | 'url'>
& ResourceRefJson<Service, Group, 'groups'> {
return { return {
...super.toJSON(), ...super.toJSON(),
type: this.type, type: this.type,
@ -152,12 +136,10 @@ export class Service extends Resource {
} }
} }
export interface Service extends Resource { export interface Service extends Ressource {
type: 'service' type: 'service'
category: ServiceCategory category: 'web' | 'fs' | 'git'
tags: readonly string[] tags: readonly string[]
url: UrlString url: UrlString
groups: readonly Ref<Group>[] groups: readonly Ref<Group>[]
} }
export type ServiceCategory = 'web' | 'fs' | 'git'

View file

@ -1,16 +1,12 @@
import type { Login, MailAddress, Posix, ToJson } from '../../../types.ts' import { Ref } from '@/src/models/utils/ref.ts'
import { regex, toLogin } from '../../../utils.ts' import type { Login, MailAddress, Posix, ToJson } from '@/types.ts'
import { Avatar } from '../utils/avatar.ts' import { regex, toLogin } from '@/utils.ts'
import { Ref } from '../utils/ref.ts' import type { Credential, CredentialCategory } from '@models/credential.ts'
import type { Credential, CredentialCategory } from './credential.ts' import type { Group } from '@models/group.ts'
import type { Group } from './group.ts' import { Ressource } from '@models/ressource.ts'
import { import { Avatar } from '@/src/models/utils/avatar.ts'
Resource,
type ResourceJson,
type ResourceRefJson,
} from './resource.ts'
export class User extends Resource { export class User extends Ressource {
static fromJSON(json: ToJson<User>): User { static fromJSON(json: ToJson<User>): User {
if (json.type !== 'user') { if (json.type !== 'user') {
throw new TypeError(`type is "${json.type}" but "user" is required`) throw new TypeError(`type is "${json.type}" but "user" is required`)
@ -116,7 +112,7 @@ export class User extends Resource {
groups, groups,
posix, posix,
credentials, credentials,
...resource ...ressource
}: Pick< }: Pick<
User, User,
| 'uuid' | 'uuid'
@ -131,7 +127,7 @@ export class User extends Resource {
| 'avatar' | 'avatar'
| 'credentials' | 'credentials'
>) { >) {
super(resource) super(ressource)
this.#lastname = lastname this.#lastname = lastname
this.#firstname = firstname this.#firstname = firstname
if (!regex.mailAddress) { if (!regex.mailAddress) {
@ -154,54 +150,37 @@ export class User extends Resource {
get type(): 'user' { get type(): 'user' {
return 'user' return 'user'
} }
get firstname(): string { get firstname() {
return this.#firstname return this.#firstname
} }
get lastname(): string { get lastname() {
return this.#lastname return this.#lastname
} }
get login(): Login { get login() {
return this.#login return this.#login
} }
get mail(): MailAddress { get mail() {
return this.#mail return this.#mail
} }
get groups(): readonly Ref<Group>[] { get groups() {
return this.#groups return this.#groups
} }
get posix(): Posix | undefined { get posix() {
return this.#posix return this.#posix
} }
get credentials(): readonly Ref<Credential<CredentialCategory>>[] { get credentials() {
return this.#credentials return this.#credentials
} }
update(props: Partial<Omit<User, 'type' | 'uuid' | 'createdAt'>>): User { update(props: Partial<Omit<User, 'type' | 'uuid' | 'createdAt'>>): User {
const { updatedAt } = super.update(props) const { updatedAt } = super.update(props)
return new User({ const user = new User({ ...this, ...props, updatedAt })
uuid: this.uuid, return user
name: this.name,
avatar: this.avatar,
createdAt: this.createdAt,
lastname: this.lastname,
firstname: this.firstname,
mail: this.mail,
groups: this.groups,
posix: this.posix,
credentials: this.credentials,
...props,
updatedAt,
})
} }
toJSON(): toJSON() {
& ResourceJson<User, 'lastname' | 'firstname' | 'login' | 'mail'>
& { posix: Posix | null }
& ResourceRefJson<User, Group, 'groups'>
& ResourceRefJson<User, Credential<CredentialCategory>, 'credentials'> {
return { return {
...super.toJSON(), ...super.toJSON(),
type: this.type,
lastname: this.lastname, lastname: this.lastname,
firstname: this.firstname, firstname: this.firstname,
login: this.login, login: this.login,
@ -222,7 +201,7 @@ export class User extends Resource {
} }
} }
export interface User extends Resource { export interface User extends Ressource {
type: 'user' type: 'user'
lastname: string lastname: string
firstname: string firstname: string

View file

@ -1,4 +1,4 @@
import type { UrlString } from '../../../types.ts' import type { UrlString } from '@/types.ts'
export class Avatar { export class Avatar {
static fromEmoji(emoji: string): UrlString { static fromEmoji(emoji: string): UrlString {

View file

@ -1,77 +1,71 @@
import type { UUID } from '../../../types.ts' import type { Db } from '@/mod.ts'
import type { Db } from '../../db/mod.ts' import type { UUID } from '@/types.ts'
import { import {
Credential, Credential,
Group, Group,
Machine, Machine,
type Resource, type Ressource,
Service, Service,
User, User,
} from '../mod.ts' } from '@models'
export type RefString<T extends Resource> = export type RefString<T extends Ressource> =
| `@ref/${T['type']}#${UUID}` | `@ref/${T['type']}#${UUID}`
| Ref<T> | Ref<T>
export type RefResolver<T extends Resource> = ( export type RefResolver<T extends Ressource> = (
ref: RefString<T>, ref: RefString<T>,
) => T | Promise<T> ) => T | Promise<T>
export class Ref<T extends Resource> extends String { export class Ref<T extends Ressource> extends String {
static #toString<T extends Resource>( static #toString<T extends Ressource>(
{ uuid, type }: { uuid: UUID; type: T['type'] }, { uuid, type }: { uuid: UUID; type: T['type'] },
) { ) {
return `@ref/${type}#${uuid}` as const return `@ref/${type}#${uuid}` as const
} }
static parse<T extends Resource>( static parse<T extends Ressource>(string: RefString<T>) {
string: RefString<T>,
): { type: T['type']; uuid: UUID } {
const [_, value] = string.split('/') const [_, value] = string.split('/')
const [type, uuid] = value.split('#') as [T['type'], UUID] const [type, uuid] = value.split('#') as [T['type'], UUID]
return { type, uuid } as const return { type, uuid } as const
} }
static fromResource<T extends Resource>(resource: T): Ref<T> { static fromRessource<T extends Ressource>(ressource: T): Ref<T> {
return new Ref<T>(resource) return new Ref<T>(ressource)
} }
static fromString<T extends Resource>(string: RefString<T>): Ref<T> { static fromString<T extends Ressource>(string: RefString<T>) {
return new Ref<T>(Ref.parse(string)) return new Ref<T>(Ref.parse(string))
} }
static dbResolver( static dbResolver(db: Db) {
db: Db, return <T extends Ressource>(ref: RefString<T>): Promise<T> => {
): <T extends Resource>(ref: RefString<T>) => Promise<T> {
return <T extends Resource>(ref: RefString<T>): Promise<T> => {
const { type, uuid } = Ref.parse(ref) const { type, uuid } = Ref.parse(ref)
//@ts-expect-error force type casting to fix //@ts-expect-error force type casting to fix
return db.resource[type].get({ uuid }) return db.ressource[type].get({ uuid })
} }
} }
static restResolver( static restResolver(endpoint: string | URL) {
endpoint: string | URL, return async <T extends Ressource>(ref: RefString<T>) => {
): <T extends Resource>(ref: RefString<T>) => Promise<T> {
return async <T extends Resource>(ref: RefString<T>): Promise<T> => {
const { type, uuid } = Ref.parse(ref) const { type, uuid } = Ref.parse(ref)
const url = new URL(`${type}s/${uuid}`, endpoint) const url = new URL(`${type}s/${uuid}`, endpoint)
const response = await fetch(url) const response = await fetch(url)
const json = await response.json() const json = await response.json()
if (type === 'user') { if (type === 'user') {
return User.fromJSON(json) as unknown as T return User.fromJSON(json)
} }
if (type === 'machine') { if (type === 'machine') {
return Machine.fromJSON(json) as unknown as T return Machine.fromJSON(json)
} }
if (type === 'service') { if (type === 'service') {
return Service.fromJSON(json) as unknown as T return Service.fromJSON(json)
} }
if (type === 'group') { if (type === 'group') {
return Group.fromJSON(json) as unknown as T return Group.fromJSON(json)
} }
if (type === 'credential') { if (type === 'credential') {
return Credential.fromJSON(json) as unknown as T return Credential.fromJSON(json)
} }
throw new TypeError(`unknown ref type "${type}"`) throw new TypeError(`unknown ref type "${type}"`)
@ -87,23 +81,23 @@ export class Ref<T extends Resource> extends String {
#type: T['type'] #type: T['type']
#uuid: UUID #uuid: UUID
get type(): T['type'] { get type() {
return this.#type return this.#type
} }
get uuid(): UUID { get uuid() {
return this.#uuid return this.#uuid
} }
ref(resolver: RefResolver<T>): T | Promise<T> { ref(resolver: RefResolver<T>) {
return resolver(this.toString()) return resolver(this.toString())
} }
toString(): `@ref/${T['type']}#${UUID}` { toString() {
return Ref.#toString<T>({ uuid: this.uuid, type: this.type }) return Ref.#toString<T>({ uuid: this.uuid, type: this.type })
} }
toJSON(): `@ref/${T['type']}#${UUID}` { toJSON() {
return this.toString() return this.toString()
} }
} }

View file

@ -1,4 +1,4 @@
import type { Login } from './types.ts' import type { Login } from '@/types.ts'
export function toLogin({ export function toLogin({
firstname, firstname,