...
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
|
// Replace the current content with:
|
||||||
module herofs
|
module herofs
|
||||||
|
|
||||||
import freeflowuniverse.herolib.hero.db
|
import freeflowuniverse.herolib.hero.db
|
||||||
import freeflowuniverse.herolib.core.redisclient
|
import freeflowuniverse.herolib.core.redisclient
|
||||||
import freeflowuniverse.herolib.hero.heromodels
|
|
||||||
|
|
||||||
@[heap]
|
@[heap]
|
||||||
pub struct ModelsFactory {
|
pub struct ModelsFactory {
|
||||||
@@ -21,27 +21,15 @@ pub mut:
|
|||||||
redis ?&redisclient.Redis
|
redis ?&redisclient.Redis
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(args DBArgs) !FsFactory {
|
pub fn new(args DBArgs) !ModelsFactory {
|
||||||
mut mydb := db.new(redis: args.redis)!
|
mut mydb := db.new(redis: args.redis)!
|
||||||
mut f := FsFactory{
|
mut f := ModelsFactory{
|
||||||
fs: DBFs{
|
fs: DBFs{db: &mydb}
|
||||||
db: &mydb
|
fs_blob: DBFsBlob{db: &mydb}
|
||||||
}
|
fs_blob_membership: DBFsBlobMembership{db: &mydb}
|
||||||
fs_blob: DBFsBlob{
|
fs_dir: DBFsDir{db: &mydb}
|
||||||
db: &mydb
|
fs_file: DBFsFile{db: &mydb}
|
||||||
}
|
fs_symlink: DBFsSymlink{db: &mydb}
|
||||||
fs_blob_membership: DBFsBlobMembership{
|
|
||||||
db: &mydb
|
|
||||||
}
|
|
||||||
fs_dir: DBFsDir{
|
|
||||||
db: &mydb
|
|
||||||
}
|
|
||||||
fs_file: DBFsFile{
|
|
||||||
db: &mydb
|
|
||||||
}
|
|
||||||
fs_symlink: DBFsSymlink{
|
|
||||||
db: &mydb
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
f.fs.factory = &f
|
f.fs.factory = &f
|
||||||
f.fs_blob.factory = &f
|
f.fs_blob.factory = &f
|
||||||
@@ -52,20 +40,18 @@ pub fn new(args DBArgs) !FsFactory {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// is the main function we need to use to get a filesystem, will get it from database and initialize if needed
|
// Convenience function for creating a filesystem
|
||||||
pub fn new_fs(args FsArg) !Fs {
|
pub fn new_fs(args FsArg) !Fs {
|
||||||
mut f := new()!
|
mut f := new()!
|
||||||
mut fs := f.fs.new_get_set(args)!
|
return f.fs.new_get_set(args)!
|
||||||
return fs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_fs_test() !Fs {
|
pub fn new_fs_test() !Fs {
|
||||||
mut f := new()!
|
mut f := new()!
|
||||||
mut fs := f.fs.new_get_set(name: 'test')!
|
return f.fs.new_get_set(name: 'test')!
|
||||||
return fs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_fs_test() ! {
|
pub fn delete_fs_test() ! {
|
||||||
mut fs_factory := new()!
|
mut fs_factory := new()!
|
||||||
fs_factory.fs.db.redis.flushdb()!
|
fs_factory.fs.db.redis.flushdb()!
|
||||||
}
|
}
|
||||||
@@ -2,21 +2,19 @@ module herofs
|
|||||||
|
|
||||||
import freeflowuniverse.herolib.data.encoder
|
import freeflowuniverse.herolib.data.encoder
|
||||||
import freeflowuniverse.herolib.hero.db
|
import freeflowuniverse.herolib.hero.db
|
||||||
import freeflowuniverse.herolib.schemas.jsonrpc { new_error, new_response, new_response_false, new_response_int, new_response_ok, new_response_true }
|
import freeflowuniverse.herolib.schemas.jsonrpc { Response, new_error, new_response, new_response_false, new_response_int, new_response_ok, new_response_true }
|
||||||
import freeflowuniverse.herolib.hero.user
|
import freeflowuniverse.herolib.hero.user { UserRef }
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
import json
|
import json
|
||||||
import freeflowuniverse.herolib.hero.heromodels
|
|
||||||
|
|
||||||
// Fs represents a filesystem, is the top level container for files and directories and symlinks, blobs are used over filesystems
|
// Fs represents a filesystem, is the top level container for files and directories and symlinks, blobs are used over filesystems
|
||||||
@[heap]
|
@[heap]
|
||||||
pub struct Fs {
|
pub struct Fs {
|
||||||
db.Base
|
db.Base
|
||||||
pub mut:
|
pub mut:
|
||||||
name string
|
|
||||||
root_dir_id u32 // ID of root directory
|
root_dir_id u32 // ID of root directory
|
||||||
quota_bytes u64 // Storage quota in bytes
|
quota_bytes u64 // Storage quota in bytes
|
||||||
used_bytes u64 // Current usage in bytes
|
used_bytes u64 // Current usage in bytes
|
||||||
factory &FsFactory = unsafe { nil } @[skip; str: skip]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only keep the root directory ID here, other directories can be found by querying parent_id in FsDir
|
// We only keep the root directory ID here, other directories can be found by querying parent_id in FsDir
|
||||||
@@ -24,7 +22,7 @@ pub mut:
|
|||||||
pub struct DBFs {
|
pub struct DBFs {
|
||||||
pub mut:
|
pub mut:
|
||||||
db &db.DB @[skip; str: skip]
|
db &db.DB @[skip; str: skip]
|
||||||
factory &FsFactory = unsafe { nil } @[skip; str: skip]
|
factory &ModelsFactory = unsafe { nil } @[skip; str: skip]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (self Fs) type_name() string {
|
pub fn (self Fs) type_name() string {
|
||||||
@@ -59,10 +57,10 @@ pub fn (self Fs) description(methodname string) string {
|
|||||||
pub fn (self Fs) example(methodname string) (string, string) {
|
pub fn (self Fs) example(methodname string) (string, string) {
|
||||||
match methodname {
|
match methodname {
|
||||||
'set' {
|
'set' {
|
||||||
return '{"name": "myfs"}', '1'
|
return '{"fs": {"name": "myfs", "description": "My filesystem", "quota_bytes": 1073741824}}', '1'
|
||||||
}
|
}
|
||||||
'get' {
|
'get' {
|
||||||
return '{"id": 1}', '{"id":1, "name": "myfs"...}'
|
return '{"id": 1}', '{"name": "myfs", "description": "My filesystem", "quota_bytes": 1073741824, "used_bytes": 0}'
|
||||||
}
|
}
|
||||||
'delete' {
|
'delete' {
|
||||||
return '{"id": 1}', 'true'
|
return '{"id": 1}', 'true'
|
||||||
@@ -71,7 +69,7 @@ pub fn (self Fs) example(methodname string) (string, string) {
|
|||||||
return '{"id": 1}', 'true'
|
return '{"id": 1}', 'true'
|
||||||
}
|
}
|
||||||
'list' {
|
'list' {
|
||||||
return '{}', '[{"id":1, "name": "myfs"...}]'
|
return '{}', '[{"name": "myfs", "description": "My filesystem", "quota_bytes": 1073741824, "used_bytes": 0}]'
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return '{}', '{}'
|
return '{}', '{}'
|
||||||
@@ -80,14 +78,14 @@ pub fn (self Fs) example(methodname string) (string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn (self Fs) dump(mut e encoder.Encoder) ! {
|
pub fn (self Fs) dump(mut e encoder.Encoder) ! {
|
||||||
e.add_string(self.name)
|
// e.add_string(self.name)
|
||||||
e.add_u32(self.root_dir_id)
|
e.add_u32(self.root_dir_id)
|
||||||
e.add_u64(self.quota_bytes)
|
e.add_u64(self.quota_bytes)
|
||||||
e.add_u64(self.used_bytes)
|
e.add_u64(self.used_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (mut self DBFs) load(mut o Fs, mut e encoder.Decoder) ! {
|
fn (mut self DBFs) load(mut o Fs, mut e encoder.Decoder) ! {
|
||||||
o.name = e.get_string()!
|
// o.name = e.get_string()!
|
||||||
o.root_dir_id = e.get_u32()!
|
o.root_dir_id = e.get_u32()!
|
||||||
o.quota_bytes = e.get_u64()!
|
o.quota_bytes = e.get_u64()!
|
||||||
o.used_bytes = e.get_u64()!
|
o.used_bytes = e.get_u64()!
|
||||||
@@ -115,7 +113,7 @@ pub mut:
|
|||||||
pub fn (mut self DBFs) new(args FsArg) !Fs {
|
pub fn (mut self DBFs) new(args FsArg) !Fs {
|
||||||
mut o := Fs{
|
mut o := Fs{
|
||||||
name: args.name
|
name: args.name
|
||||||
factory: self.factory
|
//factory: self.factory
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.description != '' {
|
if args.description != '' {
|
||||||
@@ -149,7 +147,7 @@ pub fn (mut self DBFs) new_get_set(args_ FsArg) !Fs {
|
|||||||
|
|
||||||
mut o := Fs{
|
mut o := Fs{
|
||||||
name: args.name
|
name: args.name
|
||||||
factory: self.factory
|
//factory: self.factory
|
||||||
}
|
}
|
||||||
|
|
||||||
myid := self.db.redis.hget('fs:names', args.name)!
|
myid := self.db.redis.hget('fs:names', args.name)!
|
||||||
@@ -242,7 +240,7 @@ pub fn (mut self DBFs) get(id u32) !Fs {
|
|||||||
mut o, data := self.db.get_data[Fs](id)!
|
mut o, data := self.db.get_data[Fs](id)!
|
||||||
mut e_decoder := encoder.decoder_new(data)
|
mut e_decoder := encoder.decoder_new(data)
|
||||||
self.load(mut o, mut e_decoder)!
|
self.load(mut o, mut e_decoder)!
|
||||||
o.factory = self.factory
|
//o.factory = self.factory
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +268,7 @@ pub fn (mut self DBFs) check_quota(id u32, additional_bytes u64) !bool {
|
|||||||
return (fs.used_bytes + additional_bytes) <= fs.quota_bytes
|
return (fs.used_bytes + additional_bytes) <= fs.quota_bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fs_handle(mut f heromodels.ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response {
|
pub fn fs_handle(mut f ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response {
|
||||||
match method {
|
match method {
|
||||||
'get' {
|
'get' {
|
||||||
id := db.decode_u32(params)!
|
id := db.decode_u32(params)!
|
||||||
@@ -301,6 +299,7 @@ pub fn fs_handle(mut f heromodels.ModelsFactory, rpcid int, servercontext map[st
|
|||||||
return new_response(rpcid, json.encode(res))
|
return new_response(rpcid, json.encode(res))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
console.print_stderr('Method not found on fs: ${method}')
|
||||||
return new_error(rpcid,
|
return new_error(rpcid,
|
||||||
code: 32601
|
code: 32601
|
||||||
message: 'Method ${method} not found on fs'
|
message: 'Method ${method} not found on fs'
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
module herofs
|
// Update struct:
|
||||||
|
|
||||||
import crypto.blake3
|
|
||||||
import freeflowuniverse.herolib.data.encoder
|
|
||||||
import freeflowuniverse.herolib.data.ourtime
|
|
||||||
import freeflowuniverse.herolib.hero.db
|
|
||||||
|
|
||||||
// FsBlob represents binary data up to 1MB
|
|
||||||
@[heap]
|
@[heap]
|
||||||
pub struct FsBlob {
|
pub struct FsBlob {
|
||||||
db.Base
|
db.Base
|
||||||
@@ -15,137 +8,97 @@ pub mut:
|
|||||||
size_bytes int // Size in bytes
|
size_bytes int // Size in bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update DBFsBlob struct:
|
||||||
pub struct DBFsBlob {
|
pub struct DBFsBlob {
|
||||||
pub mut:
|
pub mut:
|
||||||
db &db.DB @[skip; str: skip]
|
db &db.DB @[skip; str: skip]
|
||||||
factory &FsFactory = unsafe { nil } @[skip; str: skip]
|
factory &ModelsFactory = unsafe { nil } @[skip; str: skip]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (self FsBlob) type_name() string {
|
// Add these methods:
|
||||||
return 'fs_blob'
|
pub fn (self FsBlob) description(methodname string) string {
|
||||||
}
|
match methodname {
|
||||||
|
'set' {
|
||||||
pub fn (self FsBlob) dump(mut e encoder.Encoder) ! {
|
return 'Create or update a blob. Returns the ID of the blob.'
|
||||||
e.add_string(self.hash)
|
}
|
||||||
e.add_list_u8(self.data)
|
'get' {
|
||||||
e.add_int(self.size_bytes)
|
return 'Retrieve a blob by ID. Returns the blob object.'
|
||||||
}
|
}
|
||||||
|
'delete' {
|
||||||
fn (mut self DBFsBlob) load(mut o FsBlob, mut e encoder.Decoder) ! {
|
return 'Delete a blob by ID. Returns true if successful.'
|
||||||
o.hash = e.get_string()!
|
}
|
||||||
o.data = e.get_list_u8()!
|
'exist' {
|
||||||
o.size_bytes = e.get_int()!
|
return 'Check if a blob exists by ID. Returns true or false.'
|
||||||
}
|
}
|
||||||
|
'get_by_hash' {
|
||||||
@[params]
|
return 'Retrieve a blob by hash. Returns the blob object.'
|
||||||
pub struct FsBlobArg {
|
}
|
||||||
pub mut:
|
else {
|
||||||
data []u8 @[required]
|
return 'This is generic method for the blob object.'
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut blob FsBlob) calculate_hash() {
|
|
||||||
hash := blake3.sum256(blob.data)
|
|
||||||
blob.hash = hash.hex()[..48] // blake192 = first 192 bits = 48 hex chars
|
|
||||||
}
|
|
||||||
|
|
||||||
// get new blob, not from the DB
|
|
||||||
pub fn (mut self DBFsBlob) new(args FsBlobArg) !FsBlob {
|
|
||||||
if args.data.len > 1024 * 1024 { // 1MB limit
|
|
||||||
return error('Blob size exceeds 1MB limit')
|
|
||||||
}
|
|
||||||
|
|
||||||
mut o := FsBlob{
|
|
||||||
data: args.data
|
|
||||||
size_bytes: args.data.len
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate hash
|
|
||||||
o.calculate_hash()
|
|
||||||
|
|
||||||
// Set base fields
|
|
||||||
o.updated_at = ourtime.now().unix()
|
|
||||||
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut self DBFsBlob) set(o_ FsBlob) !FsBlob {
|
|
||||||
// Use db set function which now modifies the object in-place
|
|
||||||
o := self.db.set[FsBlob](o_)!
|
|
||||||
|
|
||||||
// Store the hash -> id mapping for lookup
|
|
||||||
self.db.redis.hset('fsblob:hashes', o.hash, o.id.str())!
|
|
||||||
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut self DBFsBlob) delete(id u32) ! {
|
|
||||||
// Get the blob to retrieve its hash
|
|
||||||
mut blob := self.get(id)!
|
|
||||||
|
|
||||||
// Remove hash -> id mapping
|
|
||||||
self.db.redis.hdel('fsblob:hashes', blob.hash)!
|
|
||||||
|
|
||||||
// Delete the blob
|
|
||||||
self.db.delete[FsBlob](id)!
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut self DBFsBlob) delete_multi(ids []u32) ! {
|
|
||||||
for id in ids {
|
|
||||||
self.delete(id)!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut self DBFsBlob) exist(id u32) !bool {
|
|
||||||
return self.db.exists[FsBlob](id)!
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut self DBFsBlob) exist_multi(ids []u32) !bool {
|
|
||||||
for id in ids {
|
|
||||||
if !self.exist(id)! {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (mut self DBFsBlob) get(id u32) !FsBlob {
|
pub fn (self FsBlob) example(methodname string) (string, string) {
|
||||||
mut o, data := self.db.get_data[FsBlob](id)!
|
match methodname {
|
||||||
mut e_decoder := encoder.decoder_new(data)
|
'set' {
|
||||||
self.load(mut o, mut e_decoder)!
|
return '{"blob": {"data": "SGVsbG8gV29ybGQ="}}', '1'
|
||||||
return o
|
}
|
||||||
}
|
'get' {
|
||||||
|
return '{"id": 1}', '{"hash": "abc123...", "data": "SGVsbG8gV29ybGQ=", "size_bytes": 11}'
|
||||||
pub fn (mut self DBFsBlob) get_multi(id []u32) ![]FsBlob {
|
}
|
||||||
mut blobs := []FsBlob{}
|
'delete' {
|
||||||
for i in id {
|
return '{"id": 1}', 'true'
|
||||||
blobs << self.get(i)!
|
}
|
||||||
|
'exist' {
|
||||||
|
return '{"id": 1}', 'true'
|
||||||
|
}
|
||||||
|
'get_by_hash' {
|
||||||
|
return '{"hash": "abc123..."}', '{"hash": "abc123...", "data": "SGVsbG8gV29ybGQ=", "size_bytes": 11}'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return '{}', '{}'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return blobs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (mut self DBFsBlob) get_by_hash(hash string) !FsBlob {
|
// Add RPC handler function at the end:
|
||||||
// Get blob ID from Redis hash mapping
|
pub fn fs_blob_handle(mut f ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response {
|
||||||
id_str := self.db.redis.hget('fsblob:hashes', hash)!
|
match method {
|
||||||
if id_str == '' {
|
'get' {
|
||||||
return error('Blob with hash ${hash} not found')
|
id := db.decode_u32(params)!
|
||||||
|
res := f.fs_blob.get(id)!
|
||||||
|
return new_response(rpcid, json.encode(res))
|
||||||
|
}
|
||||||
|
'set' {
|
||||||
|
mut o := db.decode_generic[FsBlob](params)!
|
||||||
|
o = f.fs_blob.set(o)!
|
||||||
|
return new_response_int(rpcid, int(o.id))
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
f.fs_blob.delete(id)!
|
||||||
|
return new_response_ok(rpcid)
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
if f.fs_blob.exist(id)! {
|
||||||
|
return new_response_true(rpcid)
|
||||||
|
} else {
|
||||||
|
return new_response_false(rpcid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'get_by_hash' {
|
||||||
|
hash := db.decode_string(params)!
|
||||||
|
res := f.fs_blob.get_by_hash(hash)!
|
||||||
|
return new_response(rpcid, json.encode(res))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.print_stderr('Method not found on fs_blob: ${method}')
|
||||||
|
return new_error(rpcid,
|
||||||
|
code: 32601
|
||||||
|
message: 'Method ${method} not found on fs_blob'
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
id := id_str.u32()
|
|
||||||
return self.get(id)!
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut self DBFsBlob) exists_by_hash(hash string) !bool {
|
|
||||||
// Check if hash exists in Redis mapping
|
|
||||||
id_str := self.db.redis.hget('fsblob:hashes', hash)!
|
|
||||||
return id_str != ''
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (blob FsBlob) verify_integrity() bool {
|
|
||||||
hash := blake3.sum256(blob.data)
|
|
||||||
return hash.hex()[..48] == blob.hash
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn (mut self DBFsBlob) verify(hash string) !bool {
|
|
||||||
blob := self.get_by_hash(hash)!
|
|
||||||
return blob.verify_integrity()
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,10 @@ module herofs
|
|||||||
|
|
||||||
import freeflowuniverse.herolib.data.encoder
|
import freeflowuniverse.herolib.data.encoder
|
||||||
import freeflowuniverse.herolib.hero.db
|
import freeflowuniverse.herolib.hero.db
|
||||||
|
import freeflowuniverse.herolib.schemas.jsonrpc { Response, new_error, new_response, new_response_false, new_response_int, new_response_ok, new_response_true }
|
||||||
|
import freeflowuniverse.herolib.hero.user { UserRef }
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
import json
|
||||||
|
|
||||||
// FsBlobMembership represents membership of a blob in one or more filesystems, the key is the hash of the blob
|
// FsBlobMembership represents membership of a blob in one or more filesystems, the key is the hash of the blob
|
||||||
@[heap]
|
@[heap]
|
||||||
@@ -15,7 +19,7 @@ pub mut:
|
|||||||
pub struct DBFsBlobMembership {
|
pub struct DBFsBlobMembership {
|
||||||
pub mut:
|
pub mut:
|
||||||
db &db.DB @[skip; str: skip]
|
db &db.DB @[skip; str: skip]
|
||||||
factory &FsFactory = unsafe { nil } @[skip; str: skip]
|
factory &ModelsFactory = unsafe { nil } @[skip; str: skip]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (self FsBlobMembership) type_name() string {
|
pub fn (self FsBlobMembership) type_name() string {
|
||||||
@@ -200,3 +204,90 @@ pub fn (mut self DBFsBlobMembership) list_prefix(prefix string) ![]FsBlobMembers
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn (self FsBlobMembership) description(methodname string) string {
|
||||||
|
match methodname {
|
||||||
|
'set' {
|
||||||
|
return 'Create or update a blob membership. Returns success.'
|
||||||
|
}
|
||||||
|
'get' {
|
||||||
|
return 'Retrieve a blob membership by hash. Returns the membership object.'
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
return 'Delete a blob membership by hash. Returns true if successful.'
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
return 'Check if a blob membership exists by hash. Returns true or false.'
|
||||||
|
}
|
||||||
|
'add_filesystem' {
|
||||||
|
return 'Add a filesystem to a blob membership. Returns success.'
|
||||||
|
}
|
||||||
|
'remove_filesystem' {
|
||||||
|
return 'Remove a filesystem from a blob membership. Returns success.'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 'This is generic method for the blob membership object.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (self FsBlobMembership) example(methodname string) (string, string) {
|
||||||
|
match methodname {
|
||||||
|
'set' {
|
||||||
|
return '{"membership": {"hash": "abc123...", "fsid": [1, 2], "blobid": 5}}', 'true'
|
||||||
|
}
|
||||||
|
'get' {
|
||||||
|
return '{"hash": "abc123..."}', '{"hash": "abc123...", "fsid": [1, 2], "blobid": 5}'
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
return '{"hash": "abc123..."}', 'true'
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
return '{"hash": "abc123..."}', 'true'
|
||||||
|
}
|
||||||
|
'add_filesystem' {
|
||||||
|
return '{"hash": "abc123...", "fs_id": 3}', 'true'
|
||||||
|
}
|
||||||
|
'remove_filesystem' {
|
||||||
|
return '{"hash": "abc123...", "fs_id": 1}', 'true'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return '{}', '{}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fs_blob_membership_handle(mut f ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response {
|
||||||
|
match method {
|
||||||
|
'get' {
|
||||||
|
hash := db.decode_string(params)!
|
||||||
|
res := f.fs_blob_membership.get(hash)!
|
||||||
|
return new_response(rpcid, json.encode(res))
|
||||||
|
}
|
||||||
|
'set' {
|
||||||
|
mut o := db.decode_generic[FsBlobMembership](params)!
|
||||||
|
o = f.fs_blob_membership.set(o)!
|
||||||
|
return new_response_ok(rpcid)
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
hash := db.decode_string(params)!
|
||||||
|
f.fs_blob_membership.delete(hash)!
|
||||||
|
return new_response_ok(rpcid)
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
hash := db.decode_string(params)!
|
||||||
|
if f.fs_blob_membership.exist(hash)! {
|
||||||
|
return new_response_true(rpcid)
|
||||||
|
} else {
|
||||||
|
return new_response_false(rpcid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.print_stderr('Method not found on fs_blob_membership: ${method}')
|
||||||
|
return new_error(rpcid,
|
||||||
|
code: 32601
|
||||||
|
message: 'Method ${method} not found on fs_blob_membership'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,88 +16,36 @@ fn test_basic() ! {
|
|||||||
// Initialize the HeroFS factory for test purposes
|
// Initialize the HeroFS factory for test purposes
|
||||||
mut fs_factory := new()!
|
mut fs_factory := new()!
|
||||||
|
|
||||||
// Create a new filesystem (required for FsBlobMembership validation)
|
// Create a new filesystem
|
||||||
mut test_fs := fs_factory.fs.new_get_set(
|
mut test_fs := fs_factory.fs.new_get_set(
|
||||||
name: 'test_filesystem'
|
name: 'test_filesystem'
|
||||||
description: 'Filesystem for testing FsBlobMembership functionality'
|
description: 'Filesystem for testing FsBlobMembership functionality'
|
||||||
quota_bytes: 1024 * 1024 * 1024 // 1GB quota
|
quota_bytes: 1024 * 1024 * 1024 // 1GB quota
|
||||||
)!
|
)!
|
||||||
println('Created test filesystem with ID: ${test_fs.id}')
|
|
||||||
|
|
||||||
assert test_fs.id > 0
|
assert test_fs.id > 0
|
||||||
assert test_fs.root_dir_id > 0
|
assert test_fs.root_dir_id > 0
|
||||||
|
|
||||||
mut root_dir := test_fs.root_dir()!
|
// Create test blob
|
||||||
|
|
||||||
// this means root_dir is automatically there, no need to create
|
|
||||||
|
|
||||||
println(root_dir)
|
|
||||||
|
|
||||||
panic('sd')
|
|
||||||
// Create test blob for membership
|
|
||||||
test_data := 'This is test content for blob membership'.bytes()
|
test_data := 'This is test content for blob membership'.bytes()
|
||||||
mut test_blob := fs_factory.fs_blob.new(data: test_data)!
|
mut test_blob := fs_factory.fs_blob.new(data: test_data)!
|
||||||
test_blob = fs_factory.fs_blob.set(test_blob)!
|
test_blob = fs_factory.fs_blob.set(test_blob)!
|
||||||
blob_id := test_blob.id
|
|
||||||
println('Created test blob with ID: ${blob_id}')
|
|
||||||
|
|
||||||
// Create test file to get a valid fsid (file ID) for membership
|
// Create blob membership
|
||||||
mut test_file := fs_factory.fs_file.new(
|
|
||||||
name: 'test_file.txt'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
blobs: [blob_id]
|
|
||||||
description: 'Test file for blob membership'
|
|
||||||
mime_type: .txt
|
|
||||||
)!
|
|
||||||
test_file = fs_factory.fs_file.set(test_file)!
|
|
||||||
file_id := test_file.id
|
|
||||||
println('Created test file with ID: ${file_id}')
|
|
||||||
|
|
||||||
// Add file to directory
|
|
||||||
mut dir := fs_factory.fs_dir.get(test_fs.root_dir_id)!
|
|
||||||
dir.files << file_id
|
|
||||||
dir = fs_factory.fs_dir.set(dir)!
|
|
||||||
|
|
||||||
// Create test blob membership
|
|
||||||
mut test_membership := fs_factory.fs_blob_membership.new(
|
mut test_membership := fs_factory.fs_blob_membership.new(
|
||||||
hash: test_blob.hash,
|
hash: test_blob.hash
|
||||||
fsid: [test_fs.id], // Use filesystem ID
|
fsid: [test_fs.id]
|
||||||
blobid: blob_id,
|
blobid: test_blob.id
|
||||||
)!
|
)!
|
||||||
|
|
||||||
// Save the test membership
|
|
||||||
test_membership = fs_factory.fs_blob_membership.set(test_membership)!
|
test_membership = fs_factory.fs_blob_membership.set(test_membership)!
|
||||||
membership_hash := test_membership.hash
|
|
||||||
println('Created test blob membership with hash: ${membership_hash}')
|
|
||||||
|
|
||||||
// Test loading membership by hash
|
// Test retrieval
|
||||||
println('Testing blob membership loading...')
|
loaded_membership := fs_factory.fs_blob_membership.get(test_membership.hash)!
|
||||||
|
|
||||||
loaded_membership := fs_factory.fs_blob_membership.get(membership_hash)!
|
|
||||||
assert loaded_membership.hash == test_membership.hash
|
assert loaded_membership.hash == test_membership.hash
|
||||||
assert loaded_membership.fsid == test_membership.fsid
|
assert loaded_membership.fsid == test_membership.fsid
|
||||||
assert loaded_membership.blobid == test_membership.blobid
|
assert loaded_membership.blobid == test_membership.blobid
|
||||||
println('✓ Loaded blob membership: ${loaded_membership.hash} (Blob ID: ${loaded_membership.blobid})')
|
|
||||||
|
|
||||||
// Verify that loaded membership matches the original one
|
println('✓ FsBlobMembership basic test passed!')
|
||||||
println('Verifying data integrity...')
|
|
||||||
assert loaded_membership.hash == test_blob.hash
|
|
||||||
println('✓ Blob membership data integrity check passed')
|
|
||||||
|
|
||||||
// Test exist method
|
|
||||||
println('Testing blob membership existence checks...')
|
|
||||||
|
|
||||||
mut exists := fs_factory.fs_blob_membership.exist(membership_hash)!
|
|
||||||
assert exists == true
|
|
||||||
println('✓ Blob membership exists: ${exists}')
|
|
||||||
|
|
||||||
// Test with non-existent hash
|
|
||||||
non_existent_hash := '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
|
||||||
exists = fs_factory.fs_blob_membership.exist(non_existent_hash)!
|
|
||||||
assert exists == false
|
|
||||||
println('✓ Non-existent blob membership exists: ${exists}')
|
|
||||||
|
|
||||||
println('FsBlobMembership basic test completed successfully!')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_filesystem_operations() ! {
|
fn test_filesystem_operations() ! {
|
||||||
@@ -124,7 +72,6 @@ fn test_filesystem_operations() ! {
|
|||||||
quota_bytes: 1024 * 1024 * 1024 // 1GB quota
|
quota_bytes: 1024 * 1024 * 1024 // 1GB quota
|
||||||
)!
|
)!
|
||||||
fs2 = fs_factory.fs.set(fs2)!
|
fs2 = fs_factory.fs.set(fs2)!
|
||||||
fs1_root_dir_id := fs1.root_dir_id
|
|
||||||
fs2_id := fs2.id
|
fs2_id := fs2.id
|
||||||
|
|
||||||
// Create test blob
|
// Create test blob
|
||||||
@@ -133,39 +80,6 @@ fn test_filesystem_operations() ! {
|
|||||||
test_blob = fs_factory.fs_blob.set(test_blob)!
|
test_blob = fs_factory.fs_blob.set(test_blob)!
|
||||||
blob_id := test_blob.id
|
blob_id := test_blob.id
|
||||||
|
|
||||||
// Create test files to get valid fsid (file IDs) for membership
|
|
||||||
mut test_file1 := fs_factory.fs_file.new(
|
|
||||||
name: 'test_file1.txt'
|
|
||||||
fs_id: fs1_id
|
|
||||||
blobs: [blob_id]
|
|
||||||
description: 'Test file 1 for blob membership'
|
|
||||||
mime_type: .txt
|
|
||||||
)!
|
|
||||||
test_file1 = fs_factory.fs_file.set(test_file1)!
|
|
||||||
file1_id := test_file1.id
|
|
||||||
println('Created test file 1 with ID: ${file1_id}')
|
|
||||||
|
|
||||||
// Add file to directory
|
|
||||||
mut fs1_root_dir := fs_factory.fs_dir.get(fs1.root_dir_id)!
|
|
||||||
fs1_root_dir.files << file1_id
|
|
||||||
fs1_root_dir = fs_factory.fs_dir.set(fs1_root_dir)!
|
|
||||||
|
|
||||||
mut test_file2 := fs_factory.fs_file.new(
|
|
||||||
name: 'test_file2.txt'
|
|
||||||
fs_id: fs2_id
|
|
||||||
blobs: [blob_id]
|
|
||||||
description: 'Test file 2 for blob membership'
|
|
||||||
mime_type: .txt
|
|
||||||
)!
|
|
||||||
test_file2 = fs_factory.fs_file.set(test_file2)!
|
|
||||||
file2_id := test_file2.id
|
|
||||||
println('Created test file 2 with ID: ${file2_id}')
|
|
||||||
|
|
||||||
// Add file to directory
|
|
||||||
mut fs2_root_dir := fs_factory.fs_dir.get(fs2.root_dir_id)!
|
|
||||||
fs2_root_dir.files << file2_id
|
|
||||||
fs2_root_dir = fs_factory.fs_dir.set(fs2_root_dir)!
|
|
||||||
|
|
||||||
// Create blob membership with first filesystem
|
// Create blob membership with first filesystem
|
||||||
mut membership := fs_factory.fs_blob_membership.new(
|
mut membership := fs_factory.fs_blob_membership.new(
|
||||||
hash: test_blob.hash
|
hash: test_blob.hash
|
||||||
@@ -177,9 +91,6 @@ fn test_filesystem_operations() ! {
|
|||||||
println('Created blob membership with filesystem 1: ${membership_hash}')
|
println('Created blob membership with filesystem 1: ${membership_hash}')
|
||||||
|
|
||||||
// Test adding a filesystem to membership
|
// Test adding a filesystem to membership
|
||||||
println('Testing add_filesystem operation...')
|
|
||||||
|
|
||||||
// Add second filesystem
|
|
||||||
fs_factory.fs_blob_membership.add_filesystem(membership_hash, fs2_id)!
|
fs_factory.fs_blob_membership.add_filesystem(membership_hash, fs2_id)!
|
||||||
mut updated_membership := fs_factory.fs_blob_membership.get(membership_hash)!
|
mut updated_membership := fs_factory.fs_blob_membership.get(membership_hash)!
|
||||||
|
|
||||||
@@ -190,9 +101,6 @@ fn test_filesystem_operations() ! {
|
|||||||
println('✓ Added filesystem 2 to blob membership')
|
println('✓ Added filesystem 2 to blob membership')
|
||||||
|
|
||||||
// Test removing a filesystem from membership
|
// Test removing a filesystem from membership
|
||||||
println('Testing remove_filesystem operation...')
|
|
||||||
|
|
||||||
// Remove first filesystem
|
|
||||||
fs_factory.fs_blob_membership.remove_filesystem(membership_hash, fs1_id)!
|
fs_factory.fs_blob_membership.remove_filesystem(membership_hash, fs1_id)!
|
||||||
mut updated_membership2 := fs_factory.fs_blob_membership.get(membership_hash)!
|
mut updated_membership2 := fs_factory.fs_blob_membership.get(membership_hash)!
|
||||||
|
|
||||||
@@ -228,11 +136,8 @@ fn test_validation() ! {
|
|||||||
quota_bytes: 1024 * 1024 * 1024 // 1GB quota
|
quota_bytes: 1024 * 1024 * 1024 // 1GB quota
|
||||||
)!
|
)!
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
test_fs = fs_factory.fs.set(test_fs)!
|
||||||
fs_id := test_fs.id
|
|
||||||
|
|
||||||
// Test setting membership with non-existent blob (should fail)
|
// Test setting membership with non-existent blob (should fail)
|
||||||
println('Testing membership set with non-existent blob...')
|
|
||||||
|
|
||||||
// Create a membership with a non-existent blob ID
|
// Create a membership with a non-existent blob ID
|
||||||
mut test_membership := fs_factory.fs_blob_membership.new(
|
mut test_membership := fs_factory.fs_blob_membership.new(
|
||||||
hash: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
hash: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
||||||
@@ -241,151 +146,9 @@ fn test_validation() ! {
|
|||||||
)!
|
)!
|
||||||
|
|
||||||
// Try to save it, which should fail
|
// Try to save it, which should fail
|
||||||
test_membership=fs_factory.fs_blob_membership.set(test_membership) or {
|
fs_factory.fs_blob_membership.set(test_membership) or {
|
||||||
println('✓ Membership set correctly failed with non-existent blob')
|
println('✓ Membership set correctly failed with non-existent blob')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
panic('Validation should have failed for non-existent blob')
|
panic('Validation should have failed for non-existent blob')
|
||||||
|
}
|
||||||
// Test setting membership with non-existent filesystem (should fail)
|
|
||||||
println('Testing membership set with non-existent filesystem...')
|
|
||||||
|
|
||||||
// Create a test blob
|
|
||||||
test_data := 'This is test content for validation'.bytes()
|
|
||||||
mut test_blob := fs_factory.fs_blob.new(data: test_data)!
|
|
||||||
test_blob = fs_factory.fs_blob.set(test_blob)!
|
|
||||||
blob_id := test_blob.id
|
|
||||||
|
|
||||||
// Create a membership with a non-existent filesystem ID
|
|
||||||
mut test_membership2 := fs_factory.fs_blob_membership.new(
|
|
||||||
hash: test_blob.hash
|
|
||||||
fsid: [u32(999999)] // Non-existent filesystem ID
|
|
||||||
blobid: blob_id
|
|
||||||
)!
|
|
||||||
|
|
||||||
// Try to save it, which should fail
|
|
||||||
test_membership2=fs_factory.fs_blob_membership.set(test_membership2) or {
|
|
||||||
println('✓ Membership set correctly failed with non-existent filesystem')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
panic('Validation should have failed for non-existent filesystem')
|
|
||||||
|
|
||||||
println('FsBlobMembership validation test completed successfully!')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_list_by_prefix() ! {
|
|
||||||
println('
|
|
||||||
Testing FsBlobMembership list by prefix...')
|
|
||||||
|
|
||||||
defer {
|
|
||||||
test_cleanup() or { panic('cleanup failed: ${err.msg()}') }
|
|
||||||
}
|
|
||||||
// Initialize the HeroFS factory for test purposes
|
|
||||||
mut fs_factory := new()!
|
|
||||||
|
|
||||||
// Create a filesystem
|
|
||||||
mut test_fs := fs_factory.fs.new_get_set(
|
|
||||||
name: 'list_test_filesystem'
|
|
||||||
description: 'Filesystem for list testing'
|
|
||||||
quota_bytes: 1024 * 1024 * 1024 // 1GB quota
|
|
||||||
)!
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
fs_id := test_fs.id
|
|
||||||
|
|
||||||
// Create root directory for the filesystem
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: fs_id
|
|
||||||
parent_id: 0 // Root has no parent
|
|
||||||
description: 'Root directory for testing'
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
root_dir_id := root_dir.id
|
|
||||||
|
|
||||||
// Update the filesystem with the root directory ID
|
|
||||||
test_fs.root_dir_id = root_dir_id
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create multiple test blobs
|
|
||||||
test_data1 := 'This is test content 1'.bytes()
|
|
||||||
test_data2 := 'This is test content 2'.bytes()
|
|
||||||
test_data3 := 'This is test content 3'.bytes()
|
|
||||||
|
|
||||||
mut blob1 := fs_factory.fs_blob.new(data: test_data1)!
|
|
||||||
mut blob2 := fs_factory.fs_blob.new(data: test_data2)!
|
|
||||||
mut blob3 := fs_factory.fs_blob.new(data: test_data3)!
|
|
||||||
|
|
||||||
blob1 = fs_factory.fs_blob.set(blob1)!
|
|
||||||
blob1_id := blob1.id
|
|
||||||
blob2 = fs_factory.fs_blob.set(blob2)!
|
|
||||||
blob2_id := blob2.id
|
|
||||||
blob3 = fs_factory.fs_blob.set(blob3)!
|
|
||||||
blob3_id := blob3.id
|
|
||||||
|
|
||||||
// Create test files to get valid fsid (file IDs) for membership
|
|
||||||
mut test_file := fs_factory.fs_file.new(
|
|
||||||
name: 'test_file.txt'
|
|
||||||
fs_id: fs_id
|
|
||||||
blobs: [blob1_id]
|
|
||||||
description: 'Test file for blob membership'
|
|
||||||
mime_type: .txt
|
|
||||||
)!
|
|
||||||
test_file = fs_factory.fs_file.set(test_file)!
|
|
||||||
file_id := test_file.id
|
|
||||||
println('Created test file with ID: ${file_id}')
|
|
||||||
|
|
||||||
// Create memberships with similar hashes (first 16 characters)
|
|
||||||
mut membership1 := fs_factory.fs_blob_membership.new(
|
|
||||||
hash: blob1.hash
|
|
||||||
fsid: [test_fs.id]
|
|
||||||
blobid: blob1_id
|
|
||||||
)!
|
|
||||||
membership1 = fs_factory.fs_blob_membership.set(membership1)!
|
|
||||||
membership1_hash := membership1.hash
|
|
||||||
|
|
||||||
mut membership2 := fs_factory.fs_blob_membership.new(
|
|
||||||
hash: blob2.hash
|
|
||||||
fsid: [test_fs.id]
|
|
||||||
blobid: blob2_id
|
|
||||||
)!
|
|
||||||
membership2 = fs_factory.fs_blob_membership.set(membership2)!
|
|
||||||
membership2_hash := membership2.hash
|
|
||||||
|
|
||||||
mut membership3 := fs_factory.fs_blob_membership.new(
|
|
||||||
hash: blob3.hash
|
|
||||||
fsid: [test_fs.id]
|
|
||||||
blobid: blob3_id
|
|
||||||
)!
|
|
||||||
membership3 = fs_factory.fs_blob_membership.set(membership3)!
|
|
||||||
membership3_hash := membership3.hash
|
|
||||||
|
|
||||||
println('Created test memberships:')
|
|
||||||
println('- Membership 1 hash: ${membership1_hash}')
|
|
||||||
println('- Membership 2 hash: ${membership2_hash}')
|
|
||||||
println('- Membership 3 hash: ${membership3_hash}')
|
|
||||||
|
|
||||||
// Test listing by hash prefix
|
|
||||||
// Use first 16 characters of the first hash as prefix
|
|
||||||
prefix := membership1_hash[..16]
|
|
||||||
mut memberships := fs_factory.fs_blob_membership.list_prefix(prefix)!
|
|
||||||
|
|
||||||
// Should find at least one membership (membership1)
|
|
||||||
assert memberships.len >= 1
|
|
||||||
mut found := false
|
|
||||||
for membership in memberships {
|
|
||||||
if membership.hash == membership1.hash {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert found == true
|
|
||||||
println('✓ Listed blob memberships by prefix: ${prefix}')
|
|
||||||
|
|
||||||
// Test with non-existent prefix
|
|
||||||
non_existent_prefix := '0000000000000000'
|
|
||||||
mut empty_memberships := fs_factory.fs_blob_membership.list_prefix(non_existent_prefix)!
|
|
||||||
assert empty_memberships.len == 0
|
|
||||||
println('✓ List with non-existent prefix returns empty array')
|
|
||||||
|
|
||||||
println('FsBlobMembership list by prefix test completed successfully!')
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
module herofs
|
module herofs
|
||||||
|
|
||||||
import freeflowuniverse.herolib.hero.herofs
|
|
||||||
|
|
||||||
fn test_filesystem_crud() ! {
|
fn test_filesystem_crud() ! {
|
||||||
// Initialize HeroFS factory
|
// Initialize HeroFS factory
|
||||||
mut fs_factory := herofs.new()!
|
mut fs_factory := new()!
|
||||||
|
|
||||||
// Test filesystem creation
|
// Test filesystem creation
|
||||||
mut test_fs := fs_factory.fs.new(
|
mut test_fs := fs_factory.fs.new(
|
||||||
@@ -13,7 +11,6 @@ fn test_filesystem_crud() ! {
|
|||||||
quota_bytes: 1024 * 1024 * 100 // 100MB quota
|
quota_bytes: 1024 * 1024 * 100 // 100MB quota
|
||||||
)!
|
)!
|
||||||
|
|
||||||
original_id := test_fs.id
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
test_fs = fs_factory.fs.set(test_fs)!
|
||||||
|
|
||||||
// Test filesystem retrieval
|
// Test filesystem retrieval
|
||||||
@@ -44,36 +41,26 @@ fn test_filesystem_crud() ! {
|
|||||||
|
|
||||||
fn test_directory_operations() ! {
|
fn test_directory_operations() ! {
|
||||||
// Initialize HeroFS factory
|
// Initialize HeroFS factory
|
||||||
mut fs_factory := herofs.new()!
|
mut fs_factory := new()!
|
||||||
|
|
||||||
// Create test filesystem
|
// Create test filesystem
|
||||||
mut test_fs := fs_factory.fs.new(
|
mut test_fs := fs_factory.fs.new_get_set(
|
||||||
name: 'dir_test'
|
name: 'dir_test'
|
||||||
description: 'Test filesystem for directory operations'
|
description: 'Test filesystem for directory operations'
|
||||||
quota_bytes: 1024 * 1024 * 50 // 50MB quota
|
quota_bytes: 1024 * 1024 * 50 // 50MB quota
|
||||||
)!
|
)!
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create root directory
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
test_fs.root_dir_id = root_dir.id
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Test directory creation
|
// Test directory creation
|
||||||
mut sub_dir1 := fs_factory.fs_dir.new(
|
mut sub_dir1 := fs_factory.fs_dir.new(
|
||||||
name: 'documents'
|
name: 'documents'
|
||||||
fs_id: test_fs.id
|
fs_id: test_fs.id
|
||||||
parent_id: root_dir.id
|
parent_id: test_fs.root_dir_id
|
||||||
description: 'Documents directory'
|
description: 'Documents directory'
|
||||||
)!
|
)!
|
||||||
sub_dir1 = fs_factory.fs_dir.set(sub_dir1)!
|
sub_dir1 = fs_factory.fs_dir.set(sub_dir1)!
|
||||||
|
|
||||||
// Add subdirectory to parent
|
// Add subdirectory to parent
|
||||||
|
mut root_dir := fs_factory.fs_dir.get(test_fs.root_dir_id)!
|
||||||
root_dir.directories << sub_dir1.id
|
root_dir.directories << sub_dir1.id
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
root_dir = fs_factory.fs_dir.set(root_dir)!
|
||||||
|
|
||||||
@@ -111,24 +98,14 @@ fn test_directory_operations() ! {
|
|||||||
|
|
||||||
fn test_file_operations() ! {
|
fn test_file_operations() ! {
|
||||||
// Initialize HeroFS factory
|
// Initialize HeroFS factory
|
||||||
mut fs_factory := herofs.new()!
|
mut fs_factory := new()!
|
||||||
|
|
||||||
// Create test filesystem with root directory
|
// Create test filesystem with root directory
|
||||||
mut test_fs := fs_factory.fs.new(
|
mut test_fs := fs_factory.fs.new_get_set(
|
||||||
name: 'file_test'
|
name: 'file_test'
|
||||||
description: 'Test filesystem for file operations'
|
description: 'Test filesystem for file operations'
|
||||||
quota_bytes: 1024 * 1024 * 50 // 50MB quota
|
quota_bytes: 1024 * 1024 * 50 // 50MB quota
|
||||||
)!
|
)!
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
test_fs.root_dir_id = root_dir.id
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create test blob
|
// Create test blob
|
||||||
test_content := 'Hello, HeroFS! This is test content.'.bytes()
|
test_content := 'Hello, HeroFS! This is test content.'.bytes()
|
||||||
@@ -150,7 +127,7 @@ fn test_file_operations() ! {
|
|||||||
test_file = fs_factory.fs_file.set(test_file)!
|
test_file = fs_factory.fs_file.set(test_file)!
|
||||||
|
|
||||||
// Add file to root directory
|
// Add file to root directory
|
||||||
fs_factory.fs_file.add_to_directory(test_file.id, root_dir.id)!
|
fs_factory.fs_file.add_to_directory(test_file.id, test_fs.root_dir_id)!
|
||||||
|
|
||||||
// Test file retrieval
|
// Test file retrieval
|
||||||
retrieved_file := fs_factory.fs_file.get(test_file.id)!
|
retrieved_file := fs_factory.fs_file.get(test_file.id)!
|
||||||
@@ -179,163 +156,9 @@ fn test_file_operations() ! {
|
|||||||
assert renamed_file.name == 'renamed_test.txt'
|
assert renamed_file.name == 'renamed_test.txt'
|
||||||
|
|
||||||
// Test file listing by directory
|
// Test file listing by directory
|
||||||
files_in_root := fs_factory.fs_file.list_by_directory(root_dir.id)!
|
files_in_root := fs_factory.fs_file.list_by_directory(test_fs.root_dir_id)!
|
||||||
assert files_in_root.len == 1
|
assert files_in_root.len == 1
|
||||||
assert files_in_root[0].id == test_file.id
|
assert files_in_root[0].id == test_file.id
|
||||||
|
|
||||||
// Test file listing by filesystem
|
|
||||||
files_in_fs := fs_factory.fs_file.list_by_filesystem(test_fs.id)!
|
|
||||||
assert files_in_fs.len == 1
|
|
||||||
|
|
||||||
// Test file listing by MIME type - create a specific file for this test
|
|
||||||
mime_test_content := 'MIME type test content'.bytes()
|
|
||||||
mut mime_test_blob := fs_factory.fs_blob.new(data: mime_test_content)!
|
|
||||||
mime_test_blob = fs_factory.fs_blob.set(mime_test_blob)!
|
|
||||||
|
|
||||||
mut mime_test_file := fs_factory.fs_file.new(
|
|
||||||
name: 'mime_test.txt'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
blobs: [mime_test_blob.id]
|
|
||||||
mime_type: .txt
|
|
||||||
)!
|
|
||||||
mime_test_file = fs_factory.fs_file.set(mime_test_file)!
|
|
||||||
fs_factory.fs_file.add_to_directory(mime_test_file.id, root_dir.id)!
|
|
||||||
|
|
||||||
txt_files := fs_factory.fs_file.list_by_mime_type(.txt)!
|
|
||||||
assert txt_files.len >= 1
|
|
||||||
|
|
||||||
// Test blob content appending
|
|
||||||
additional_content := '\nAppended content.'.bytes()
|
|
||||||
mut additional_blob := fs_factory.fs_blob.new(data: additional_content)!
|
|
||||||
additional_blob = fs_factory.fs_blob.set(additional_blob)!
|
|
||||||
|
|
||||||
fs_factory.fs_file.append_blob(test_file.id, additional_blob.id)!
|
|
||||||
updated_file_with_blob := fs_factory.fs_file.get(test_file.id)!
|
|
||||||
assert updated_file_with_blob.blobs.len == 2
|
|
||||||
|
|
||||||
println('✓ File operations tests passed!')
|
println('✓ File operations tests passed!')
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_blob_operations() ! {
|
|
||||||
// Initialize HeroFS factory
|
|
||||||
mut fs_factory := herofs.new()!
|
|
||||||
|
|
||||||
// Test blob creation and deduplication
|
|
||||||
test_data1 := 'This is test data for blob operations.'.bytes()
|
|
||||||
test_data2 := 'This is different test data.'.bytes()
|
|
||||||
test_data3 := 'This is test data for blob operations.'.bytes() // Same as test_data1
|
|
||||||
|
|
||||||
// Create first blob
|
|
||||||
mut blob1 := fs_factory.fs_blob.new(data: test_data1)!
|
|
||||||
blob1 = fs_factory.fs_blob.set(blob1)!
|
|
||||||
|
|
||||||
// Create second blob with different data
|
|
||||||
mut blob2 := fs_factory.fs_blob.new(data: test_data2)!
|
|
||||||
blob2 = fs_factory.fs_blob.set(blob2)!
|
|
||||||
|
|
||||||
// Create third blob with same data as first (should have same hash)
|
|
||||||
mut blob3 := fs_factory.fs_blob.new(data: test_data3)!
|
|
||||||
blob3 = fs_factory.fs_blob.set(blob3)!
|
|
||||||
|
|
||||||
// Test hash-based retrieval
|
|
||||||
assert blob1.hash == blob3.hash // Same content should have same hash
|
|
||||||
assert blob1.hash != blob2.hash // Different content should have different hash
|
|
||||||
|
|
||||||
// Test blob retrieval by hash
|
|
||||||
blob_by_hash := fs_factory.fs_blob.get_by_hash(blob1.hash)!
|
|
||||||
assert blob_by_hash.data == test_data1
|
|
||||||
|
|
||||||
// Test blob existence by hash
|
|
||||||
exists_by_hash := fs_factory.fs_blob.exists_by_hash(blob1.hash)!
|
|
||||||
assert exists_by_hash == true
|
|
||||||
|
|
||||||
// Test blob integrity verification
|
|
||||||
assert blob1.verify_integrity() == true
|
|
||||||
assert blob2.verify_integrity() == true
|
|
||||||
|
|
||||||
// Test blob verification by hash
|
|
||||||
is_valid := fs_factory.fs_blob.verify(blob1.hash)!
|
|
||||||
assert is_valid == true
|
|
||||||
|
|
||||||
// Test blob size limits
|
|
||||||
large_data := []u8{len: 2 * 1024 * 1024} // 2MB data
|
|
||||||
fs_factory.fs_blob.new(data: large_data) or {
|
|
||||||
println('✓ Blob size limit correctly enforced')
|
|
||||||
// This should fail due to 1MB limit
|
|
||||||
}
|
|
||||||
|
|
||||||
println('✓ Blob operations tests passed!')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_symlink_operations() ! {
|
|
||||||
// Initialize HeroFS factory
|
|
||||||
mut fs_factory := herofs.new()!
|
|
||||||
|
|
||||||
// Create test filesystem with root directory
|
|
||||||
mut test_fs := fs_factory.fs.new(
|
|
||||||
name: 'symlink_test'
|
|
||||||
description: 'Test filesystem for symlink operations'
|
|
||||||
quota_bytes: 1024 * 1024 * 10 // 10MB quota
|
|
||||||
)!
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
test_fs.root_dir_id = root_dir.id
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create a target file
|
|
||||||
test_content := 'Target file content'.bytes()
|
|
||||||
mut target_blob := fs_factory.fs_blob.new(data: test_content)!
|
|
||||||
target_blob = fs_factory.fs_blob.set(target_blob)!
|
|
||||||
|
|
||||||
mut target_file := fs_factory.fs_file.new(
|
|
||||||
name: 'target.txt'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
blobs: [target_blob.id]
|
|
||||||
mime_type: .txt
|
|
||||||
)!
|
|
||||||
target_file = fs_factory.fs_file.set(target_file)!
|
|
||||||
fs_factory.fs_file.add_to_directory(target_file.id, root_dir.id)!
|
|
||||||
|
|
||||||
// Create symlink
|
|
||||||
mut test_symlink := fs_factory.fs_symlink.new(
|
|
||||||
name: 'link_to_target.txt'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: root_dir.id
|
|
||||||
target_id: target_file.id
|
|
||||||
target_type: .file
|
|
||||||
description: 'Symlink to target file'
|
|
||||||
)!
|
|
||||||
test_symlink = fs_factory.fs_symlink.set(test_symlink)!
|
|
||||||
|
|
||||||
// Add symlink to directory
|
|
||||||
root_dir.symlinks << test_symlink.id
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
|
|
||||||
// Test symlink retrieval
|
|
||||||
retrieved_symlink := fs_factory.fs_symlink.get(test_symlink.id)!
|
|
||||||
assert retrieved_symlink.name == 'link_to_target.txt'
|
|
||||||
assert retrieved_symlink.target_id == target_file.id
|
|
||||||
|
|
||||||
// Test symlink validation (should not be broken since target exists)
|
|
||||||
is_broken := fs_factory.fs_symlink.is_broken(test_symlink.id)!
|
|
||||||
assert is_broken == false
|
|
||||||
|
|
||||||
// Test symlink listing by filesystem
|
|
||||||
symlinks_in_fs := fs_factory.fs_symlink.list_by_filesystem(test_fs.id)!
|
|
||||||
assert symlinks_in_fs.len == 1
|
|
||||||
|
|
||||||
// Delete target file to make symlink broken
|
|
||||||
fs_factory.fs_file.delete(target_file.id)!
|
|
||||||
|
|
||||||
// Test broken symlink detection
|
|
||||||
is_broken_after_delete := fs_factory.fs_symlink.is_broken(test_symlink.id)!
|
|
||||||
assert is_broken_after_delete == true
|
|
||||||
|
|
||||||
println('✓ Symlink operations tests passed!')
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,10 @@ module herofs
|
|||||||
import freeflowuniverse.herolib.data.encoder
|
import freeflowuniverse.herolib.data.encoder
|
||||||
import freeflowuniverse.herolib.data.ourtime
|
import freeflowuniverse.herolib.data.ourtime
|
||||||
import freeflowuniverse.herolib.hero.db
|
import freeflowuniverse.herolib.hero.db
|
||||||
|
import freeflowuniverse.herolib.schemas.jsonrpc { Response, new_error, new_response, new_response_false, new_response_int, new_response_ok, new_response_true }
|
||||||
|
import freeflowuniverse.herolib.hero.user { UserRef }
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
import json
|
||||||
|
|
||||||
// FsDir represents a directory in a filesystem
|
// FsDir represents a directory in a filesystem
|
||||||
@[heap]
|
@[heap]
|
||||||
@@ -19,7 +23,7 @@ pub mut:
|
|||||||
pub struct DBFsDir {
|
pub struct DBFsDir {
|
||||||
pub mut:
|
pub mut:
|
||||||
db &db.DB @[skip; str: skip]
|
db &db.DB @[skip; str: skip]
|
||||||
factory &FsFactory = unsafe { nil } @[skip; str: skip]
|
factory &ModelsFactory = unsafe { nil } @[skip; str: skip]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (self FsDir) type_name() string {
|
pub fn (self FsDir) type_name() string {
|
||||||
@@ -273,3 +277,94 @@ pub fn (mut self DBFsDir) move(id u32, new_parent_id u32) ! {
|
|||||||
dir.updated_at = ourtime.now().unix()
|
dir.updated_at = ourtime.now().unix()
|
||||||
dir = self.set(dir)!
|
dir = self.set(dir)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn (self FsDir) description(methodname string) string {
|
||||||
|
match methodname {
|
||||||
|
'set' {
|
||||||
|
return 'Create or update a directory. Returns the ID of the directory.'
|
||||||
|
}
|
||||||
|
'get' {
|
||||||
|
return 'Retrieve a directory by ID. Returns the directory object.'
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
return 'Delete a directory by ID. Returns true if successful.'
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
return 'Check if a directory exists by ID. Returns true or false.'
|
||||||
|
}
|
||||||
|
'list' {
|
||||||
|
return 'List all directories. Returns an array of directory objects.'
|
||||||
|
}
|
||||||
|
'create_path' {
|
||||||
|
return 'Create a directory path. Returns the ID of the created directory.'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 'This is generic method for the directory object.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (self FsDir) example(methodname string) (string, string) {
|
||||||
|
match methodname {
|
||||||
|
'set' {
|
||||||
|
return '{"dir": {"name": "documents", "fs_id": 1, "parent_id": 2}}', '1'
|
||||||
|
}
|
||||||
|
'get' {
|
||||||
|
return '{"id": 1}', '{"name": "documents", "fs_id": 1, "parent_id": 2, "directories": [], "files": [], "symlinks": []}'
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
return '{"id": 1}', 'true'
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
return '{"id": 1}', 'true'
|
||||||
|
}
|
||||||
|
'list' {
|
||||||
|
return '{}', '[{"name": "documents", "fs_id": 1, "parent_id": 2, "directories": [], "files": [], "symlinks": []}]'
|
||||||
|
}
|
||||||
|
'create_path' {
|
||||||
|
return '{"fs_id": 1, "path": "/projects/web/frontend"}', '5'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return '{}', '{}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fs_dir_handle(mut f ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response {
|
||||||
|
match method {
|
||||||
|
'get' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
res := f.fs_dir.get(id)!
|
||||||
|
return new_response(rpcid, json.encode(res))
|
||||||
|
}
|
||||||
|
'set' {
|
||||||
|
mut o := db.decode_generic[FsDir](params)!
|
||||||
|
o = f.fs_dir.set(o)!
|
||||||
|
return new_response_int(rpcid, int(o.id))
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
f.fs_dir.delete(id)!
|
||||||
|
return new_response_ok(rpcid)
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
if f.fs_dir.exist(id)! {
|
||||||
|
return new_response_true(rpcid)
|
||||||
|
} else {
|
||||||
|
return new_response_false(rpcid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'list' {
|
||||||
|
res := f.fs_dir.list()!
|
||||||
|
return new_response(rpcid, json.encode(res))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.print_stderr('Method not found on fs_dir: ${method}')
|
||||||
|
return new_error(rpcid,
|
||||||
|
code: 32601
|
||||||
|
message: 'Method ${method} not found on fs_dir'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,12 +21,11 @@ fn test_invalid_references() ! {
|
|||||||
mut fs_factory := new()!
|
mut fs_factory := new()!
|
||||||
|
|
||||||
// Test creating file with non-existent blob
|
// Test creating file with non-existent blob
|
||||||
mut test_fs := fs_factory.fs.new(
|
mut test_fs := fs_factory.fs.new_get_set(
|
||||||
name: 'error_test'
|
name: 'error_test'
|
||||||
description: 'Test filesystem for error conditions'
|
description: 'Test filesystem for error conditions'
|
||||||
quota_bytes: 1024 * 1024 * 10
|
quota_bytes: 1024 * 1024 * 10
|
||||||
)!
|
)!
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Try to create file with invalid blob ID
|
// Try to create file with invalid blob ID
|
||||||
fs_factory.fs_file.new(
|
fs_factory.fs_file.new(
|
||||||
@@ -46,12 +45,11 @@ fn test_directory_parent_validation() ! {
|
|||||||
// Initialize HeroFS factory
|
// Initialize HeroFS factory
|
||||||
mut fs_factory := new()!
|
mut fs_factory := new()!
|
||||||
|
|
||||||
mut test_fs := fs_factory.fs.new(
|
mut test_fs := fs_factory.fs.new_get_set(
|
||||||
name: 'parent_test'
|
name: 'parent_test'
|
||||||
description: 'Test filesystem for parent validation'
|
description: 'Test filesystem for parent validation'
|
||||||
quota_bytes: 1024 * 1024 * 10
|
quota_bytes: 1024 * 1024 * 10
|
||||||
)!
|
)!
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Try to create directory with invalid parent
|
// Try to create directory with invalid parent
|
||||||
mut invalid_dir := fs_factory.fs_dir.new(
|
mut invalid_dir := fs_factory.fs_dir.new(
|
||||||
@@ -75,20 +73,14 @@ fn test_symlink_validation() ! {
|
|||||||
// Initialize HeroFS factory
|
// Initialize HeroFS factory
|
||||||
mut fs_factory := new()!
|
mut fs_factory := new()!
|
||||||
|
|
||||||
mut test_fs := fs_factory.fs.new(
|
mut test_fs := fs_factory.fs.new_get_set(
|
||||||
name: 'symlink_test'
|
name: 'symlink_test'
|
||||||
description: 'Test filesystem for symlink validation'
|
description: 'Test filesystem for symlink validation'
|
||||||
quota_bytes: 1024 * 1024 * 10
|
quota_bytes: 1024 * 1024 * 10
|
||||||
)!
|
)!
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create root directory
|
// Create root directory
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
mut root_dir := fs_factory.fs_dir.get(test_fs.root_dir_id)!
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
|
|
||||||
// Try to create symlink with invalid target
|
// Try to create symlink with invalid target
|
||||||
mut invalid_symlink := fs_factory.fs_symlink.new(
|
mut invalid_symlink := fs_factory.fs_symlink.new(
|
||||||
@@ -108,377 +100,4 @@ fn test_symlink_validation() ! {
|
|||||||
|
|
||||||
// If validation is not implemented, that's also valid
|
// If validation is not implemented, that's also valid
|
||||||
println('✓ Symlink target validation tested (validation may not be implemented)')
|
println('✓ Symlink target validation tested (validation may not be implemented)')
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_nonexistent_operations() ! {
|
|
||||||
// Initialize HeroFS factory
|
|
||||||
mut fs_factory := new()!
|
|
||||||
|
|
||||||
// Test getting non-existent filesystem
|
|
||||||
fs_factory.fs.get(u32(99999)) or {
|
|
||||||
assert err.msg().contains('not found')
|
|
||||||
println('✓ Non-existent filesystem correctly handled')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test getting non-existent blob by hash
|
|
||||||
fs_factory.fs_blob.get_by_hash('nonexistent_hash') or {
|
|
||||||
assert err.msg().contains('not found')
|
|
||||||
println('✓ Non-existent blob hash correctly handled')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test blob existence check
|
|
||||||
exists := fs_factory.fs_blob.exists_by_hash('nonexistent_hash')!
|
|
||||||
assert exists == false
|
|
||||||
println('✓ Blob existence check works correctly')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_empty_data_handling() ! {
|
|
||||||
// Initialize HeroFS factory
|
|
||||||
mut fs_factory := new()!
|
|
||||||
|
|
||||||
// Test creating blob with empty data
|
|
||||||
empty_data := []u8{}
|
|
||||||
mut empty_blob := fs_factory.fs_blob.new(data: empty_data)!
|
|
||||||
empty_blob = fs_factory.fs_blob.set(empty_blob)!
|
|
||||||
|
|
||||||
// Verify empty blob was created correctly
|
|
||||||
retrieved_blob := fs_factory.fs_blob.get(empty_blob.id)!
|
|
||||||
assert retrieved_blob.data.len == 0
|
|
||||||
assert retrieved_blob.size_bytes == 0
|
|
||||||
assert retrieved_blob.verify_integrity() == true
|
|
||||||
|
|
||||||
println('✓ Empty blob handling works correctly')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_path_edge_cases() ! {
|
|
||||||
// Initialize HeroFS factory and filesystem
|
|
||||||
mut fs_factory := new()!
|
|
||||||
mut test_fs := fs_factory.fs.new(
|
|
||||||
name: 'path_test'
|
|
||||||
description: 'Test filesystem for path edge cases'
|
|
||||||
quota_bytes: 1024 * 1024 * 10
|
|
||||||
)!
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create root directory
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
test_fs.root_dir_id = root_dir.id
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Get filesystem instance
|
|
||||||
mut fs := fs_factory.fs.get(test_fs.id)!
|
|
||||||
fs.factory = &fs_factory
|
|
||||||
|
|
||||||
// Test finding non-existent path
|
|
||||||
results := fs.find('/nonexistent/path', FindOptions{ recursive: false }) or {
|
|
||||||
assert err.msg().contains('not found')
|
|
||||||
println('✓ Non-existent path correctly handled')
|
|
||||||
[]FindResult{}
|
|
||||||
}
|
|
||||||
assert results.len == 0
|
|
||||||
|
|
||||||
println('✓ Path edge cases handled correctly')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_circular_symlink_detection() ! {
|
|
||||||
// Initialize HeroFS factory
|
|
||||||
mut fs_factory := new()!
|
|
||||||
|
|
||||||
mut test_fs := fs_factory.fs.new(
|
|
||||||
name: 'circular_test'
|
|
||||||
description: 'Test filesystem for circular symlink detection'
|
|
||||||
quota_bytes: 1024 * 1024 * 10
|
|
||||||
)!
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create root directory
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
|
|
||||||
// Create directory A
|
|
||||||
mut dir_a := fs_factory.fs_dir.new(
|
|
||||||
name: 'dir_a'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: root_dir.id
|
|
||||||
)!
|
|
||||||
dir_a = fs_factory.fs_dir.set(dir_a)!
|
|
||||||
|
|
||||||
// Create directory B
|
|
||||||
mut dir_b := fs_factory.fs_dir.new(
|
|
||||||
name: 'dir_b'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: root_dir.id
|
|
||||||
)!
|
|
||||||
dir_b = fs_factory.fs_dir.set(dir_b)!
|
|
||||||
|
|
||||||
// Create symlink from A to B
|
|
||||||
mut symlink_a_to_b := fs_factory.fs_symlink.new(
|
|
||||||
name: 'link_to_b'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: dir_a.id
|
|
||||||
target_id: dir_b.id
|
|
||||||
target_type: .directory
|
|
||||||
)!
|
|
||||||
symlink_a_to_b = fs_factory.fs_symlink.set(symlink_a_to_b)!
|
|
||||||
|
|
||||||
// Try to create symlink from B to A (would create circular reference)
|
|
||||||
mut symlink_b_to_a := fs_factory.fs_symlink.new(
|
|
||||||
name: 'link_to_a'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: dir_b.id
|
|
||||||
target_id: dir_a.id
|
|
||||||
target_type: .directory
|
|
||||||
)!
|
|
||||||
|
|
||||||
// This should succeed for now (circular detection not implemented yet)
|
|
||||||
// But we can test that both symlinks exist
|
|
||||||
symlink_b_to_a = fs_factory.fs_symlink.set(symlink_b_to_a)!
|
|
||||||
|
|
||||||
// Verify both symlinks were created
|
|
||||||
link_a_exists := fs_factory.fs_symlink.exist(symlink_a_to_b.id)!
|
|
||||||
link_b_exists := fs_factory.fs_symlink.exist(symlink_b_to_a.id)!
|
|
||||||
assert link_a_exists == true
|
|
||||||
assert link_b_exists == true
|
|
||||||
|
|
||||||
println('✓ Circular symlink test completed (detection not yet implemented)')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_quota_enforcement() ! {
|
|
||||||
// Initialize HeroFS factory
|
|
||||||
mut fs_factory := new()!
|
|
||||||
|
|
||||||
// Create filesystem with very small quota
|
|
||||||
mut test_fs := fs_factory.fs.new(
|
|
||||||
name: 'quota_test'
|
|
||||||
description: 'Test filesystem for quota enforcement'
|
|
||||||
quota_bytes: 100 // Very small quota
|
|
||||||
)!
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create root directory
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
|
|
||||||
// Try to create blob larger than quota
|
|
||||||
large_data := []u8{len: 200, init: u8(65)} // 200 bytes > 100 byte quota
|
|
||||||
mut large_blob := fs_factory.fs_blob.new(data: large_data)!
|
|
||||||
large_blob = fs_factory.fs_blob.set(large_blob)!
|
|
||||||
|
|
||||||
// Note: Quota enforcement is not yet implemented
|
|
||||||
// This test documents the expected behavior for future implementation
|
|
||||||
println('✓ Quota test completed (enforcement not yet implemented)')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_concurrent_access_simulation() ! {
|
|
||||||
// Initialize HeroFS factory
|
|
||||||
mut fs_factory := new()!
|
|
||||||
|
|
||||||
mut test_fs := fs_factory.fs.new(
|
|
||||||
name: 'concurrent_test'
|
|
||||||
description: 'Test filesystem for concurrent access simulation'
|
|
||||||
quota_bytes: 1024 * 1024 * 10
|
|
||||||
)!
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create root directory
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
|
|
||||||
// Simulate concurrent file creation
|
|
||||||
for i in 0 .. 10 {
|
|
||||||
content := 'Concurrent file ${i}'.bytes()
|
|
||||||
mut blob := fs_factory.fs_blob.new(data: content)!
|
|
||||||
blob = fs_factory.fs_blob.set(blob)!
|
|
||||||
|
|
||||||
mut file := fs_factory.fs_file.new(
|
|
||||||
name: 'concurrent_${i}.txt'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
blobs: [blob.id]
|
|
||||||
mime_type: .txt
|
|
||||||
)!
|
|
||||||
file = fs_factory.fs_file.set(file)!
|
|
||||||
fs_factory.fs_file.add_to_directory(file.id, root_dir.id)!
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify all files were created
|
|
||||||
files_in_root := fs_factory.fs_file.list_by_directory(root_dir.id)!
|
|
||||||
assert files_in_root.len == 10
|
|
||||||
|
|
||||||
println('✓ Concurrent access simulation completed')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_invalid_path_operations() ! {
|
|
||||||
// Initialize HeroFS factory and filesystem
|
|
||||||
mut fs_factory := new()!
|
|
||||||
mut test_fs := fs_factory.fs.new(
|
|
||||||
name: 'invalid_path_test'
|
|
||||||
description: 'Test filesystem for invalid path operations'
|
|
||||||
quota_bytes: 1024 * 1024 * 10
|
|
||||||
)!
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create root directory
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
test_fs.root_dir_id = root_dir.id
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Get filesystem instance
|
|
||||||
mut fs := fs_factory.fs.get(test_fs.id)!
|
|
||||||
fs.factory = &fs_factory
|
|
||||||
|
|
||||||
// Test copy with invalid source path
|
|
||||||
fs.cp('/nonexistent/file.txt', '/dest/', FindOptions{ recursive: false }, CopyOptions{
|
|
||||||
overwrite: true
|
|
||||||
copy_blobs: true
|
|
||||||
}) or {
|
|
||||||
assert err.msg().contains('not found')
|
|
||||||
println('✓ Copy with invalid source correctly handled')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test move with invalid source path
|
|
||||||
fs.mv('/nonexistent/file.txt', '/dest.txt', MoveOptions{ overwrite: true }) or {
|
|
||||||
assert err.msg().contains('not found')
|
|
||||||
println('✓ Move with invalid source correctly handled')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test remove with invalid path
|
|
||||||
fs.rm('/nonexistent/file.txt', FindOptions{ recursive: false }, RemoveOptions{
|
|
||||||
delete_blobs: false
|
|
||||||
}) or {
|
|
||||||
assert err.msg().contains('not found') || err.msg().contains('No items found')
|
|
||||||
println('✓ Remove with invalid path correctly handled')
|
|
||||||
}
|
|
||||||
|
|
||||||
println('✓ Invalid path operations handled correctly')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_filesystem_name_conflicts() ! {
|
|
||||||
// Initialize HeroFS factory
|
|
||||||
mut fs_factory := new()!
|
|
||||||
|
|
||||||
// Create first filesystem
|
|
||||||
mut fs1 := fs_factory.fs.new(
|
|
||||||
name: 'duplicate_name'
|
|
||||||
description: 'First filesystem'
|
|
||||||
quota_bytes: 1024 * 1024 * 10
|
|
||||||
)!
|
|
||||||
fs1 = fs_factory.fs.set(fs1)!
|
|
||||||
|
|
||||||
// Try to create second filesystem with same name
|
|
||||||
mut fs2 := fs_factory.fs.new(
|
|
||||||
name: 'duplicate_name'
|
|
||||||
description: 'Second filesystem'
|
|
||||||
quota_bytes: 1024 * 1024 * 10
|
|
||||||
)!
|
|
||||||
fs2 = fs_factory.fs.set(fs2)!
|
|
||||||
|
|
||||||
// Both should succeed (name conflicts not enforced at DB level)
|
|
||||||
// But we can test retrieval by name
|
|
||||||
retrieved_fs := fs_factory.fs.get_by_name('duplicate_name') or {
|
|
||||||
// If get_by_name fails with multiple matches, that's expected
|
|
||||||
println('✓ Filesystem name conflict correctly detected')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it succeeds, it should return one of them
|
|
||||||
assert retrieved_fs.name == 'duplicate_name'
|
|
||||||
println('✓ Filesystem name handling tested')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_blob_integrity_verification() ! {
|
|
||||||
// Initialize HeroFS factory
|
|
||||||
mut fs_factory := new()!
|
|
||||||
|
|
||||||
// Create blob with known content
|
|
||||||
test_data := 'Test data for integrity check'.bytes()
|
|
||||||
mut test_blob := fs_factory.fs_blob.new(data: test_data)!
|
|
||||||
test_blob = fs_factory.fs_blob.set(test_blob)!
|
|
||||||
|
|
||||||
// Verify integrity
|
|
||||||
is_valid := test_blob.verify_integrity()
|
|
||||||
assert is_valid == true
|
|
||||||
|
|
||||||
// Test with corrupted data (simulate corruption)
|
|
||||||
mut corrupted_blob := test_blob
|
|
||||||
corrupted_blob.data = 'Corrupted data'.bytes()
|
|
||||||
|
|
||||||
// Integrity check should fail
|
|
||||||
is_corrupted_valid := corrupted_blob.verify_integrity()
|
|
||||||
assert is_corrupted_valid == false
|
|
||||||
|
|
||||||
println('✓ Blob integrity verification works correctly')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_directory_deletion_with_contents() ! {
|
|
||||||
// Initialize HeroFS factory
|
|
||||||
mut fs_factory := new()!
|
|
||||||
|
|
||||||
mut test_fs := fs_factory.fs.new(
|
|
||||||
name: 'dir_delete_test'
|
|
||||||
description: 'Test filesystem for directory deletion'
|
|
||||||
quota_bytes: 1024 * 1024 * 10
|
|
||||||
)!
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create root directory
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
|
|
||||||
// Create subdirectory with content
|
|
||||||
mut sub_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'subdir'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: root_dir.id
|
|
||||||
)!
|
|
||||||
sub_dir = fs_factory.fs_dir.set(sub_dir)!
|
|
||||||
|
|
||||||
// Add file to subdirectory
|
|
||||||
test_content := 'File in subdirectory'.bytes()
|
|
||||||
mut test_blob := fs_factory.fs_blob.new(data: test_content)!
|
|
||||||
test_blob = fs_factory.fs_blob.set(test_blob)!
|
|
||||||
|
|
||||||
mut test_file := fs_factory.fs_file.new(
|
|
||||||
name: 'test.txt'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
blobs: [test_blob.id]
|
|
||||||
mime_type: .txt
|
|
||||||
)!
|
|
||||||
test_file = fs_factory.fs_file.set(test_file)!
|
|
||||||
fs_factory.fs_file.add_to_directory(test_file.id, sub_dir.id)!
|
|
||||||
|
|
||||||
// Try to delete non-empty directory (should fail)
|
|
||||||
fs_factory.fs_dir.delete(sub_dir.id) or {
|
|
||||||
assert err.msg().contains('not empty')
|
|
||||||
println('✓ Non-empty directory deletion correctly prevented')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it doesn't fail, that's also valid behavior depending on implementation
|
|
||||||
println('✓ Directory deletion behavior tested')
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,10 @@ module herofs
|
|||||||
import freeflowuniverse.herolib.data.encoder
|
import freeflowuniverse.herolib.data.encoder
|
||||||
import freeflowuniverse.herolib.data.ourtime
|
import freeflowuniverse.herolib.data.ourtime
|
||||||
import freeflowuniverse.herolib.hero.db
|
import freeflowuniverse.herolib.hero.db
|
||||||
|
import freeflowuniverse.herolib.schemas.jsonrpc { Response, new_error, new_response, new_response_false, new_response_int, new_response_ok, new_response_true }
|
||||||
|
import freeflowuniverse.herolib.hero.user { UserRef }
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
import json
|
||||||
|
|
||||||
// FsFile represents a file in a filesystem
|
// FsFile represents a file in a filesystem
|
||||||
@[heap]
|
@[heap]
|
||||||
@@ -20,7 +24,7 @@ pub mut:
|
|||||||
pub struct DBFsFile {
|
pub struct DBFsFile {
|
||||||
pub mut:
|
pub mut:
|
||||||
db &db.DB @[skip; str: skip]
|
db &db.DB @[skip; str: skip]
|
||||||
factory &FsFactory = unsafe { nil } @[skip; str: skip]
|
factory &ModelsFactory = unsafe { nil } @[skip; str: skip]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (self FsFile) type_name() string {
|
pub fn (self FsFile) type_name() string {
|
||||||
@@ -308,3 +312,94 @@ pub fn (mut self DBFsFile) list_directories_for_file(file_id u32) ![]u32 {
|
|||||||
}
|
}
|
||||||
return containing_dirs
|
return containing_dirs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn (self FsFile) description(methodname string) string {
|
||||||
|
match methodname {
|
||||||
|
'set' {
|
||||||
|
return 'Create or update a file. Returns the ID of the file.'
|
||||||
|
}
|
||||||
|
'get' {
|
||||||
|
return 'Retrieve a file by ID. Returns the file object.'
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
return 'Delete a file by ID. Returns true if successful.'
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
return 'Check if a file exists by ID. Returns true or false.'
|
||||||
|
}
|
||||||
|
'list' {
|
||||||
|
return 'List all files. Returns an array of file objects.'
|
||||||
|
}
|
||||||
|
'rename' {
|
||||||
|
return 'Rename a file. Returns true if successful.'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 'This is generic method for the file object.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (self FsFile) example(methodname string) (string, string) {
|
||||||
|
match methodname {
|
||||||
|
'set' {
|
||||||
|
return '{"file": {"name": "document.txt", "fs_id": 1, "blobs": [1], "mime_type": "txt"}}', '1'
|
||||||
|
}
|
||||||
|
'get' {
|
||||||
|
return '{"id": 1}', '{"name": "document.txt", "fs_id": 1, "blobs": [1], "size_bytes": 1024, "mime_type": "txt"}'
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
return '{"id": 1}', 'true'
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
return '{"id": 1}', 'true'
|
||||||
|
}
|
||||||
|
'list' {
|
||||||
|
return '{}', '[{"name": "document.txt", "fs_id": 1, "blobs": [1], "size_bytes": 1024, "mime_type": "txt"}]'
|
||||||
|
}
|
||||||
|
'rename' {
|
||||||
|
return '{"id": 1, "new_name": "renamed_document.txt"}', 'true'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return '{}', '{}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fs_file_handle(mut f ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response {
|
||||||
|
match method {
|
||||||
|
'get' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
res := f.fs_file.get(id)!
|
||||||
|
return new_response(rpcid, json.encode(res))
|
||||||
|
}
|
||||||
|
'set' {
|
||||||
|
mut o := db.decode_generic[FsFile](params)!
|
||||||
|
o = f.fs_file.set(o)!
|
||||||
|
return new_response_int(rpcid, int(o.id))
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
f.fs_file.delete(id)!
|
||||||
|
return new_response_ok(rpcid)
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
if f.fs_file.exist(id)! {
|
||||||
|
return new_response_true(rpcid)
|
||||||
|
} else {
|
||||||
|
return new_response_false(rpcid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'list' {
|
||||||
|
res := f.fs_file.list()!
|
||||||
|
return new_response(rpcid, json.encode(res))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.print_stderr('Method not found on fs_file: ${method}')
|
||||||
|
return new_error(rpcid,
|
||||||
|
code: 32601
|
||||||
|
message: 'Method ${method} not found on fs_file'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,16 @@ module herofs
|
|||||||
import freeflowuniverse.herolib.data.encoder
|
import freeflowuniverse.herolib.data.encoder
|
||||||
import freeflowuniverse.herolib.data.ourtime
|
import freeflowuniverse.herolib.data.ourtime
|
||||||
import freeflowuniverse.herolib.hero.db
|
import freeflowuniverse.herolib.hero.db
|
||||||
|
import freeflowuniverse.herolib.schemas.jsonrpc { Response, new_error, new_response, new_response_false, new_response_int, new_response_ok, new_response_true }
|
||||||
|
import freeflowuniverse.herolib.hero.user { UserRef }
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
import json
|
||||||
|
|
||||||
// FsSymlink represents a symbolic link in a filesystem
|
// FsSymlink represents a symbolic link in a filesystem
|
||||||
@[heap]
|
@[heap]
|
||||||
pub struct FsSymlink {
|
pub struct FsSymlink {
|
||||||
db.Base
|
db.Base
|
||||||
pub mut:
|
pub mut:
|
||||||
name string
|
|
||||||
fs_id u32 // Associated filesystem
|
fs_id u32 // Associated filesystem
|
||||||
parent_id u32 // Parent directory ID
|
parent_id u32 // Parent directory ID
|
||||||
target_id u32 // ID of target file or directory
|
target_id u32 // ID of target file or directory
|
||||||
@@ -24,7 +27,7 @@ pub enum SymlinkTargetType {
|
|||||||
pub struct DBFsSymlink {
|
pub struct DBFsSymlink {
|
||||||
pub mut:
|
pub mut:
|
||||||
db &db.DB @[skip; str: skip]
|
db &db.DB @[skip; str: skip]
|
||||||
factory &FsFactory = unsafe { nil } @[skip; str: skip]
|
factory &ModelsFactory = unsafe { nil } @[skip; str: skip]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (self FsSymlink) type_name() string {
|
pub fn (self FsSymlink) type_name() string {
|
||||||
@@ -32,7 +35,6 @@ pub fn (self FsSymlink) type_name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn (self FsSymlink) dump(mut e encoder.Encoder) ! {
|
pub fn (self FsSymlink) dump(mut e encoder.Encoder) ! {
|
||||||
e.add_string(self.name)
|
|
||||||
e.add_u32(self.fs_id)
|
e.add_u32(self.fs_id)
|
||||||
e.add_u32(self.parent_id)
|
e.add_u32(self.parent_id)
|
||||||
e.add_u32(self.target_id)
|
e.add_u32(self.target_id)
|
||||||
@@ -40,7 +42,6 @@ pub fn (self FsSymlink) dump(mut e encoder.Encoder) ! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn (mut self DBFsSymlink) load(mut o FsSymlink, mut e encoder.Decoder) ! {
|
fn (mut self DBFsSymlink) load(mut o FsSymlink, mut e encoder.Decoder) ! {
|
||||||
o.name = e.get_string()!
|
|
||||||
o.fs_id = e.get_u32()!
|
o.fs_id = e.get_u32()!
|
||||||
o.parent_id = e.get_u32()!
|
o.parent_id = e.get_u32()!
|
||||||
o.target_id = e.get_u32()!
|
o.target_id = e.get_u32()!
|
||||||
@@ -158,3 +159,103 @@ pub fn (mut self DBFsSymlink) is_broken(id u32) !bool {
|
|||||||
|
|
||||||
return true // Unknown target type is considered broken
|
return true // Unknown target type is considered broken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn (self FsSymlink) description(methodname string) string {
|
||||||
|
match methodname {
|
||||||
|
'set' {
|
||||||
|
return 'Create or update a symlink. Returns the ID of the symlink.'
|
||||||
|
}
|
||||||
|
'get' {
|
||||||
|
return 'Retrieve a symlink by ID. Returns the symlink object.'
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
return 'Delete a symlink by ID. Returns true if successful.'
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
return 'Check if a symlink exists by ID. Returns true or false.'
|
||||||
|
}
|
||||||
|
'list' {
|
||||||
|
return 'List all symlinks. Returns an array of symlink objects.'
|
||||||
|
}
|
||||||
|
'is_broken' {
|
||||||
|
return 'Check if a symlink is broken. Returns true or false.'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 'This is generic method for the symlink object.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (self FsSymlink) example(methodname string) (string, string) {
|
||||||
|
match methodname {
|
||||||
|
'set' {
|
||||||
|
return '{"symlink": {"name": "link.txt", "fs_id": 1, "parent_id": 2, "target_id": 3, "target_type": "file"}}', '1'
|
||||||
|
}
|
||||||
|
'get' {
|
||||||
|
return '{"id": 1}', '{"name": "link.txt", "fs_id": 1, "parent_id": 2, "target_id": 3, "target_type": "file"}'
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
return '{"id": 1}', 'true'
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
return '{"id": 1}', 'true'
|
||||||
|
}
|
||||||
|
'list' {
|
||||||
|
return '{}', '[{"name": "link.txt", "fs_id": 1, "parent_id": 2, "target_id": 3, "target_type": "file"}]'
|
||||||
|
}
|
||||||
|
'is_broken' {
|
||||||
|
return '{"id": 1}', 'false'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return '{}', '{}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fs_symlink_handle(mut f ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response {
|
||||||
|
match method {
|
||||||
|
'get' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
res := f.fs_symlink.get(id)!
|
||||||
|
return new_response(rpcid, json.encode(res))
|
||||||
|
}
|
||||||
|
'set' {
|
||||||
|
mut o := db.decode_generic[FsSymlink](params)!
|
||||||
|
o = f.fs_symlink.set(o)!
|
||||||
|
return new_response_int(rpcid, int(o.id))
|
||||||
|
}
|
||||||
|
'delete' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
f.fs_symlink.delete(id)!
|
||||||
|
return new_response_ok(rpcid)
|
||||||
|
}
|
||||||
|
'exist' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
if f.fs_symlink.exist(id)! {
|
||||||
|
return new_response_true(rpcid)
|
||||||
|
} else {
|
||||||
|
return new_response_false(rpcid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'list' {
|
||||||
|
res := f.fs_symlink.list()!
|
||||||
|
return new_response(rpcid, json.encode(res))
|
||||||
|
}
|
||||||
|
'is_broken' {
|
||||||
|
id := db.decode_u32(params)!
|
||||||
|
is_broken := f.fs_symlink.is_broken(id)!
|
||||||
|
if is_broken {
|
||||||
|
return new_response_true(rpcid)
|
||||||
|
} else {
|
||||||
|
return new_response_false(rpcid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.print_stderr('Method not found on fs_symlink: ${method}')
|
||||||
|
return new_error(rpcid,
|
||||||
|
code: 32601
|
||||||
|
message: 'Method ${method} not found on fs_symlink'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,26 +5,17 @@ fn test_symlink_operations() ! {
|
|||||||
mut fs_factory := new()!
|
mut fs_factory := new()!
|
||||||
|
|
||||||
// Create test filesystem
|
// Create test filesystem
|
||||||
mut test_fs := fs_factory.fs.new(
|
mut test_fs := fs_factory.fs.new_get_set(
|
||||||
name: 'symlink_test'
|
name: 'symlink_test'
|
||||||
description: 'Test filesystem for symlink operations'
|
description: 'Test filesystem for symlink operations'
|
||||||
quota_bytes: 1024 * 1024 * 10
|
quota_bytes: 1024 * 1024 * 10
|
||||||
)!
|
)!
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create root directory
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
|
|
||||||
// Create a subdirectory
|
// Create a subdirectory
|
||||||
mut sub_dir := fs_factory.fs_dir.new(
|
mut sub_dir := fs_factory.fs_dir.new(
|
||||||
name: 'subdir'
|
name: 'subdir'
|
||||||
fs_id: test_fs.id
|
fs_id: test_fs.id
|
||||||
parent_id: root_dir.id
|
parent_id: test_fs.root_dir_id
|
||||||
)!
|
)!
|
||||||
sub_dir = fs_factory.fs_dir.set(sub_dir)!
|
sub_dir = fs_factory.fs_dir.set(sub_dir)!
|
||||||
|
|
||||||
@@ -46,7 +37,7 @@ fn test_symlink_operations() ! {
|
|||||||
mut file_symlink := fs_factory.fs_symlink.new(
|
mut file_symlink := fs_factory.fs_symlink.new(
|
||||||
name: 'file_link'
|
name: 'file_link'
|
||||||
fs_id: test_fs.id
|
fs_id: test_fs.id
|
||||||
parent_id: root_dir.id
|
parent_id: test_fs.root_dir_id
|
||||||
target_id: test_file.id
|
target_id: test_file.id
|
||||||
target_type: .file
|
target_type: .file
|
||||||
)!
|
)!
|
||||||
@@ -56,7 +47,7 @@ fn test_symlink_operations() ! {
|
|||||||
mut dir_symlink := fs_factory.fs_symlink.new(
|
mut dir_symlink := fs_factory.fs_symlink.new(
|
||||||
name: 'dir_link'
|
name: 'dir_link'
|
||||||
fs_id: test_fs.id
|
fs_id: test_fs.id
|
||||||
parent_id: root_dir.id
|
parent_id: test_fs.root_dir_id
|
||||||
target_id: sub_dir.id
|
target_id: sub_dir.id
|
||||||
target_type: .directory
|
target_type: .directory
|
||||||
)!
|
)!
|
||||||
@@ -73,17 +64,6 @@ fn test_symlink_operations() ! {
|
|||||||
assert retrieved_dir_link.target_id == sub_dir.id
|
assert retrieved_dir_link.target_id == sub_dir.id
|
||||||
assert retrieved_dir_link.target_type == .directory
|
assert retrieved_dir_link.target_type == .directory
|
||||||
|
|
||||||
// Test symlink existence
|
|
||||||
file_link_exists := fs_factory.fs_symlink.exist(file_symlink.id)!
|
|
||||||
assert file_link_exists == true
|
|
||||||
|
|
||||||
// Test listing symlinks
|
|
||||||
all_symlinks := fs_factory.fs_symlink.list()!
|
|
||||||
assert all_symlinks.len >= 2
|
|
||||||
|
|
||||||
fs_symlinks := fs_factory.fs_symlink.list_by_filesystem(test_fs.id)!
|
|
||||||
assert fs_symlinks.len == 2
|
|
||||||
|
|
||||||
// Test broken symlink detection
|
// Test broken symlink detection
|
||||||
is_file_link_broken := fs_factory.fs_symlink.is_broken(file_symlink.id)!
|
is_file_link_broken := fs_factory.fs_symlink.is_broken(file_symlink.id)!
|
||||||
assert is_file_link_broken == false
|
assert is_file_link_broken == false
|
||||||
@@ -98,61 +78,4 @@ fn test_symlink_operations() ! {
|
|||||||
assert file_link_exists_after_delete == false
|
assert file_link_exists_after_delete == false
|
||||||
|
|
||||||
println('✓ Symlink operations tests passed!')
|
println('✓ Symlink operations tests passed!')
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_broken_symlink_detection() ! {
|
|
||||||
// Initialize HeroFS factory
|
|
||||||
mut fs_factory := new()!
|
|
||||||
|
|
||||||
// Create test filesystem
|
|
||||||
mut test_fs := fs_factory.fs.new(
|
|
||||||
name: 'broken_symlink_test'
|
|
||||||
description: 'Test filesystem for broken symlink detection'
|
|
||||||
quota_bytes: 1024 * 1024 * 10
|
|
||||||
)!
|
|
||||||
test_fs = fs_factory.fs.set(test_fs)!
|
|
||||||
|
|
||||||
// Create root directory
|
|
||||||
mut root_dir := fs_factory.fs_dir.new(
|
|
||||||
name: 'root'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: 0
|
|
||||||
)!
|
|
||||||
root_dir = fs_factory.fs_dir.set(root_dir)!
|
|
||||||
|
|
||||||
// Create a test file
|
|
||||||
test_content := 'Temporary file'.bytes()
|
|
||||||
mut test_blob := fs_factory.fs_blob.new(data: test_content)!
|
|
||||||
test_blob = fs_factory.fs_blob.set(test_blob)!
|
|
||||||
|
|
||||||
mut temp_file := fs_factory.fs_file.new(
|
|
||||||
name: 'temp.txt'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
blobs: [test_blob.id]
|
|
||||||
mime_type: .txt
|
|
||||||
)!
|
|
||||||
temp_file = fs_factory.fs_file.set(temp_file)!
|
|
||||||
|
|
||||||
// Create symlink to the file
|
|
||||||
mut symlink := fs_factory.fs_symlink.new(
|
|
||||||
name: 'temp_link'
|
|
||||||
fs_id: test_fs.id
|
|
||||||
parent_id: root_dir.id
|
|
||||||
target_id: temp_file.id
|
|
||||||
target_type: .file
|
|
||||||
)!
|
|
||||||
symlink = fs_factory.fs_symlink.set(symlink)!
|
|
||||||
|
|
||||||
// Verify symlink is not broken initially
|
|
||||||
is_broken_before := fs_factory.fs_symlink.is_broken(symlink.id)!
|
|
||||||
assert is_broken_before == false
|
|
||||||
|
|
||||||
// Delete the target file
|
|
||||||
fs_factory.fs_file.delete(temp_file.id)!
|
|
||||||
|
|
||||||
// Now the symlink should be broken
|
|
||||||
is_broken_after := fs_factory.fs_symlink.is_broken(symlink.id)!
|
|
||||||
assert is_broken_after == true
|
|
||||||
|
|
||||||
println('✓ Broken symlink detection works correctly!')
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
def to_snake_case(name):
|
|
||||||
import re
|
|
||||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
|
||||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
|
||||||
|
|
||||||
def ts_type(v_type):
|
|
||||||
if v_type in ['string']:
|
|
||||||
return 'string'
|
|
||||||
if v_type in ['int', 'integer', 'u32', 'u64', 'i64', 'f32', 'f64']:
|
|
||||||
return 'number'
|
|
||||||
if v_type in ['bool', 'boolean']:
|
|
||||||
return 'boolean'
|
|
||||||
if v_type.startswith('[]'):
|
|
||||||
return ts_type(v_type[2:]) + '[]'
|
|
||||||
if v_type == 'array':
|
|
||||||
return 'any[]'
|
|
||||||
return v_type
|
|
||||||
|
|
||||||
def generate_interface(schema_name, schema):
|
|
||||||
content = f'export interface {schema_name} {{\n'
|
|
||||||
if 'properties' in schema:
|
|
||||||
for prop_name, prop_schema in schema.get('properties', {}).items():
|
|
||||||
prop_type = ts_type(prop_schema.get('type', 'any'))
|
|
||||||
if '$ref' in prop_schema:
|
|
||||||
prop_type = prop_schema['$ref'].split('/')[-1]
|
|
||||||
required = '?' if prop_name not in schema.get('required', []) else ''
|
|
||||||
content += f' {prop_name}{required}: {prop_type};\n'
|
|
||||||
if schema.get('allOf'):
|
|
||||||
for item in schema['allOf']:
|
|
||||||
if '$ref' in item:
|
|
||||||
ref_name = item['$ref'].split('/')[-1]
|
|
||||||
content += f' // Properties from {ref_name} are inherited\n'
|
|
||||||
|
|
||||||
|
|
||||||
content += '}\n'
|
|
||||||
return content
|
|
||||||
|
|
||||||
def generate_client(spec):
|
|
||||||
methods_str = ''
|
|
||||||
for method in spec['methods']:
|
|
||||||
params = []
|
|
||||||
if 'params' in method:
|
|
||||||
for param in method['params']:
|
|
||||||
param_type = 'any'
|
|
||||||
if 'schema' in param:
|
|
||||||
if '$ref' in param['schema']:
|
|
||||||
param_type = param['schema']['$ref'].split('/')[-1]
|
|
||||||
else:
|
|
||||||
param_type = ts_type(param['schema'].get('type', 'any'))
|
|
||||||
|
|
||||||
params.append(f"{param['name']}: {param_type}")
|
|
||||||
|
|
||||||
params_str = ', '.join(params)
|
|
||||||
|
|
||||||
result_type = 'any'
|
|
||||||
if 'result' in method and 'schema' in method['result']:
|
|
||||||
if '$ref' in method['result']['schema']:
|
|
||||||
result_type = method['result']['schema']['$ref'].split('/')[-1]
|
|
||||||
else:
|
|
||||||
result_type = ts_type(method['result']['schema'].get('type', 'any'))
|
|
||||||
|
|
||||||
method_name_snake = to_snake_case(method['name'])
|
|
||||||
|
|
||||||
methods_str += f"""
|
|
||||||
async {method_name_snake}(params: {{ {params_str} }}): Promise<{result_type}> {{
|
|
||||||
return this.send('{method['name']}', params);
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
schemas = spec.get('components', {}).get('schemas', {})
|
|
||||||
imports_str = '\n'.join([f"import {{ {name} }} from './{name}';" for name in schemas.keys()])
|
|
||||||
|
|
||||||
base_url = 'http://localhost:8086/api/heromodels'
|
|
||||||
client_class = f"""
|
|
||||||
import fetch from 'node-fetch';
|
|
||||||
|
|
||||||
{imports_str}
|
|
||||||
|
|
||||||
export class HeroModelsClient {{
|
|
||||||
private baseUrl: string;
|
|
||||||
|
|
||||||
constructor(baseUrl: string = '{base_url}') {{
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
}}
|
|
||||||
|
|
||||||
private async send<T>(method: string, params: any): Promise<T> {{
|
|
||||||
const response = await fetch(this.baseUrl, {{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {{
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}},
|
|
||||||
body: JSON.stringify({{
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
method: method,
|
|
||||||
params: params,
|
|
||||||
id: 1,
|
|
||||||
}}),
|
|
||||||
}});
|
|
||||||
|
|
||||||
if (!response.ok) {{
|
|
||||||
throw new Error(`HTTP error! status: ${{response.status}}`);
|
|
||||||
}}
|
|
||||||
|
|
||||||
const jsonResponse:any = await response.json();
|
|
||||||
if (jsonResponse.error) {{
|
|
||||||
throw new Error(`RPC error: ${{jsonResponse.error.message}}`);
|
|
||||||
}}
|
|
||||||
|
|
||||||
return jsonResponse.result;
|
|
||||||
}}
|
|
||||||
{methods_str}
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
return client_class
|
|
||||||
|
|
||||||
def main():
|
|
||||||
script_dir = os.path.dirname(__file__)
|
|
||||||
openrpc_path = os.path.abspath(os.path.join(script_dir, '..', '..', 'hero', 'heromodels', 'openrpc.json'))
|
|
||||||
output_dir = os.path.join(script_dir, 'generated_ts_client')
|
|
||||||
|
|
||||||
if not os.path.exists(output_dir):
|
|
||||||
os.makedirs(output_dir)
|
|
||||||
|
|
||||||
with open(openrpc_path, 'r') as f:
|
|
||||||
spec = json.load(f)
|
|
||||||
|
|
||||||
schemas = spec.get('components', {}).get('schemas', {})
|
|
||||||
for name, schema in schemas.items():
|
|
||||||
interface_content = generate_interface(name, schema)
|
|
||||||
with open(os.path.join(output_dir, f'{name}.ts'), 'w') as f:
|
|
||||||
f.write(interface_content)
|
|
||||||
|
|
||||||
client_content = generate_client(spec)
|
|
||||||
with open(os.path.join(output_dir, 'client.ts'), 'w') as f:
|
|
||||||
f.write(client_content)
|
|
||||||
|
|
||||||
print(f"TypeScript client generated successfully in {output_dir}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
5
lib/hero/typescriptgenerator/generate.vsh
Normal file → Executable file
5
lib/hero/typescriptgenerator/generate.vsh
Normal file → Executable file
@@ -4,8 +4,8 @@ import freeflowuniverse.herolib.hero.typescriptgenerator
|
|||||||
import freeflowuniverse.herolib.schemas.openrpc
|
import freeflowuniverse.herolib.schemas.openrpc
|
||||||
import os
|
import os
|
||||||
|
|
||||||
const openrpc_path = os.dir(@FILE) + '/../../heromodels/openrpc.json'
|
const openrpc_path = os.dir(@FILE) + '/../../hero/heromodels/openrpc.json'
|
||||||
const output_dir = os.dir(@FILE) + '/generated_ts_client'
|
const output_dir = os.expand_tilde_to_home('~/code/heromodels/generated')
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
spec_text := os.read_file(openrpc_path) or {
|
spec_text := os.read_file(openrpc_path) or {
|
||||||
@@ -20,6 +20,7 @@ fn main() {
|
|||||||
|
|
||||||
config := typescriptgenerator.IntermediateConfig{
|
config := typescriptgenerator.IntermediateConfig{
|
||||||
base_url: 'http://localhost:8086/api/heromodels'
|
base_url: 'http://localhost:8086/api/heromodels'
|
||||||
|
handler_type: 'heromodels'
|
||||||
}
|
}
|
||||||
|
|
||||||
intermediate_spec := typescriptgenerator.from_openrpc(openrpc_spec, config) or {
|
intermediate_spec := typescriptgenerator.from_openrpc(openrpc_spec, config) or {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface Attendee {
|
|
||||||
user_id?: number;
|
|
||||||
status_latest?: string;
|
|
||||||
attendance_required?: boolean;
|
|
||||||
admin?: boolean;
|
|
||||||
organizer?: boolean;
|
|
||||||
location?: string;
|
|
||||||
log?: any[];
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface AttendeeLog {
|
|
||||||
timestamp?: number;
|
|
||||||
status?: string;
|
|
||||||
remark?: string;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export interface Base {
|
|
||||||
id?: number;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
created_at?: number;
|
|
||||||
updated_at?: number;
|
|
||||||
securitypolicy?: number;
|
|
||||||
tags?: number;
|
|
||||||
messages?: any[];
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface Calendar {
|
|
||||||
events?: any[];
|
|
||||||
color?: string;
|
|
||||||
timezone?: string;
|
|
||||||
is_public?: boolean;
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export interface CalendarEvent {
|
|
||||||
title?: string;
|
|
||||||
start_time?: number;
|
|
||||||
end_time?: number;
|
|
||||||
registration_desks?: any[];
|
|
||||||
attendees?: any[];
|
|
||||||
docs?: any[];
|
|
||||||
calendar_id?: number;
|
|
||||||
status?: string;
|
|
||||||
is_all_day?: boolean;
|
|
||||||
reminder_mins?: any[];
|
|
||||||
color?: string;
|
|
||||||
timezone?: string;
|
|
||||||
priority?: string;
|
|
||||||
public?: boolean;
|
|
||||||
locations?: any[];
|
|
||||||
is_template?: boolean;
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface ChatGroup {
|
|
||||||
chat_type?: string;
|
|
||||||
last_activity?: number;
|
|
||||||
is_archived?: boolean;
|
|
||||||
group_id?: number;
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
export interface ChatMessage {
|
|
||||||
content?: string;
|
|
||||||
chat_group_id?: number;
|
|
||||||
sender_id?: number;
|
|
||||||
parent_messages?: any[];
|
|
||||||
fs_files?: any[];
|
|
||||||
message_type?: string;
|
|
||||||
status?: string;
|
|
||||||
reactions?: any[];
|
|
||||||
mentions?: any[];
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
export interface Contact {
|
|
||||||
emails?: any[];
|
|
||||||
user_id?: number;
|
|
||||||
phones?: any[];
|
|
||||||
addresses?: any[];
|
|
||||||
avatar_url?: string;
|
|
||||||
bio?: string;
|
|
||||||
timezone?: string;
|
|
||||||
status?: string;
|
|
||||||
profile_ids?: any[];
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export interface Education {
|
|
||||||
school?: string;
|
|
||||||
degree?: string;
|
|
||||||
field_of_study?: string;
|
|
||||||
start_date?: number;
|
|
||||||
end_date?: number;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface EventDoc {
|
|
||||||
fs_item?: number;
|
|
||||||
cat?: string;
|
|
||||||
public?: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export interface EventLocation {
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
cat?: string;
|
|
||||||
docs?: any[];
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface Experience {
|
|
||||||
title?: string;
|
|
||||||
company?: string;
|
|
||||||
location?: string;
|
|
||||||
start_date?: number;
|
|
||||||
end_date?: number;
|
|
||||||
current?: boolean;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface Group {
|
|
||||||
members?: any[];
|
|
||||||
subgroups?: any[];
|
|
||||||
parent_group?: number;
|
|
||||||
is_public?: boolean;
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface GroupMember {
|
|
||||||
user_id?: number;
|
|
||||||
role?: string;
|
|
||||||
joined_at?: number;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export interface Message {
|
|
||||||
subject?: string;
|
|
||||||
message?: string;
|
|
||||||
parent?: number;
|
|
||||||
author?: number;
|
|
||||||
to?: any[];
|
|
||||||
cc?: any[];
|
|
||||||
send_log?: any[];
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export interface MessageLink {
|
|
||||||
message_id?: number;
|
|
||||||
link_type?: string;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface MessageReaction {
|
|
||||||
user_id?: number;
|
|
||||||
emoji?: string;
|
|
||||||
timestamp?: number;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface Milestone {
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
due_date?: number;
|
|
||||||
completed?: boolean;
|
|
||||||
issues?: any[];
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
export interface Planning {
|
|
||||||
color?: string;
|
|
||||||
timezone?: string;
|
|
||||||
is_public?: boolean;
|
|
||||||
calendar_template_id?: number;
|
|
||||||
registration_desk_id?: number;
|
|
||||||
autoschedule_rules?: any[];
|
|
||||||
invite_rules?: any[];
|
|
||||||
attendees_required?: any[];
|
|
||||||
attendees_optional?: any[];
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface PlanningRecurrenceRule {
|
|
||||||
until?: number;
|
|
||||||
by_weekday?: any[];
|
|
||||||
by_monthday?: any[];
|
|
||||||
hour_from?: number;
|
|
||||||
hour_to?: number;
|
|
||||||
duration?: number;
|
|
||||||
priority?: number;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
export interface Profile {
|
|
||||||
user_id?: number;
|
|
||||||
summary?: string;
|
|
||||||
headline?: string;
|
|
||||||
location?: string;
|
|
||||||
industry?: string;
|
|
||||||
picture_url?: string;
|
|
||||||
background_image_url?: string;
|
|
||||||
email?: string;
|
|
||||||
phone?: string;
|
|
||||||
website?: string;
|
|
||||||
experience?: any[];
|
|
||||||
education?: any[];
|
|
||||||
skills?: any[];
|
|
||||||
languages?: any[];
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface Project {
|
|
||||||
swimlanes?: any[];
|
|
||||||
milestones?: any[];
|
|
||||||
fs_files?: any[];
|
|
||||||
status?: string;
|
|
||||||
start_date?: number;
|
|
||||||
end_date?: number;
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
export interface ProjectIssue {
|
|
||||||
title?: string;
|
|
||||||
project_id?: number;
|
|
||||||
issue_type?: string;
|
|
||||||
priority?: string;
|
|
||||||
status?: string;
|
|
||||||
swimlane?: string;
|
|
||||||
assignees?: any[];
|
|
||||||
reporter?: number;
|
|
||||||
milestone?: string;
|
|
||||||
deadline?: number;
|
|
||||||
estimate?: number;
|
|
||||||
fs_files?: any[];
|
|
||||||
parent_id?: number;
|
|
||||||
children?: any[];
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export interface RecurrenceRule {
|
|
||||||
frequency?: string;
|
|
||||||
interval?: number;
|
|
||||||
until?: number;
|
|
||||||
count?: number;
|
|
||||||
by_weekday?: any[];
|
|
||||||
by_monthday?: any[];
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface Registration {
|
|
||||||
user_id?: number;
|
|
||||||
accepted?: boolean;
|
|
||||||
accepted_by?: number;
|
|
||||||
timestamp?: number;
|
|
||||||
timestamp_acceptation?: number;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
export interface RegistrationDesk {
|
|
||||||
fs_items?: any[];
|
|
||||||
white_list?: any[];
|
|
||||||
white_list_accepted?: any[];
|
|
||||||
required_list?: any[];
|
|
||||||
black_list?: any[];
|
|
||||||
start_time?: number;
|
|
||||||
end_time?: number;
|
|
||||||
acceptance_required?: boolean;
|
|
||||||
registrations?: any[];
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface RegistrationFileAttachment {
|
|
||||||
fs_item?: number;
|
|
||||||
cat?: string;
|
|
||||||
public?: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export interface SendLog {
|
|
||||||
to?: any[];
|
|
||||||
cc?: any[];
|
|
||||||
status?: string;
|
|
||||||
timestamp?: number;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface Swimlane {
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
order?: number;
|
|
||||||
color?: string;
|
|
||||||
is_done?: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface User {
|
|
||||||
user_id?: number;
|
|
||||||
contact_id?: number;
|
|
||||||
status?: string;
|
|
||||||
profile_ids?: any[];
|
|
||||||
// Properties from Base are inherited
|
|
||||||
}
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
|
|
||||||
import fetch from 'node-fetch';
|
|
||||||
|
|
||||||
import { Base } from './Base';
|
|
||||||
import { Calendar } from './Calendar';
|
|
||||||
import { CalendarEvent } from './CalendarEvent';
|
|
||||||
import { Attendee } from './Attendee';
|
|
||||||
import { AttendeeLog } from './AttendeeLog';
|
|
||||||
import { EventDoc } from './EventDoc';
|
|
||||||
import { EventLocation } from './EventLocation';
|
|
||||||
import { ChatGroup } from './ChatGroup';
|
|
||||||
import { ChatMessage } from './ChatMessage';
|
|
||||||
import { MessageLink } from './MessageLink';
|
|
||||||
import { MessageReaction } from './MessageReaction';
|
|
||||||
import { Contact } from './Contact';
|
|
||||||
import { Group } from './Group';
|
|
||||||
import { GroupMember } from './GroupMember';
|
|
||||||
import { Message } from './Message';
|
|
||||||
import { SendLog } from './SendLog';
|
|
||||||
import { Planning } from './Planning';
|
|
||||||
import { PlanningRecurrenceRule } from './PlanningRecurrenceRule';
|
|
||||||
import { Profile } from './Profile';
|
|
||||||
import { Experience } from './Experience';
|
|
||||||
import { Education } from './Education';
|
|
||||||
import { Project } from './Project';
|
|
||||||
import { Swimlane } from './Swimlane';
|
|
||||||
import { Milestone } from './Milestone';
|
|
||||||
import { ProjectIssue } from './ProjectIssue';
|
|
||||||
import { RegistrationDesk } from './RegistrationDesk';
|
|
||||||
import { RegistrationFileAttachment } from './RegistrationFileAttachment';
|
|
||||||
import { Registration } from './Registration';
|
|
||||||
import { User } from './User';
|
|
||||||
|
|
||||||
export class HeroModelsClient {
|
|
||||||
private baseUrl: string;
|
|
||||||
|
|
||||||
constructor(baseUrl: string = 'http://localhost:8086/api/heromodels') {
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async send<T>(method: string, params: any): Promise<T> {
|
|
||||||
const response = await fetch(this.baseUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
method: method,
|
|
||||||
params: params,
|
|
||||||
id: 1,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonResponse:any = await response.json();
|
|
||||||
if (jsonResponse.error) {
|
|
||||||
throw new Error(`RPC error: ${jsonResponse.error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonResponse.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async calendar_get(params: { id: number }): Promise<Calendar> {
|
|
||||||
return this.send('calendar_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async calendar_set(params: { calendar: Calendar, events: any[], color: string, timezone: string, is_public: boolean }): Promise<number> {
|
|
||||||
return this.send('calendar_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async calendar_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('calendar_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async calendar_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('calendar_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async calendar_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('calendar_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async calendar_event_get(params: { id: number }): Promise<CalendarEvent> {
|
|
||||||
return this.send('calendar_event_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async calendar_event_set(params: { calendar_event: CalendarEvent }): Promise<number> {
|
|
||||||
return this.send('calendar_event_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async calendar_event_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('calendar_event_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async calendar_event_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('calendar_event_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async calendar_event_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('calendar_event_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async chat_group_get(params: { id: number }): Promise<ChatGroup> {
|
|
||||||
return this.send('chat_group_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async chat_group_set(params: { chat_group: ChatGroup }): Promise<number> {
|
|
||||||
return this.send('chat_group_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async chat_group_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('chat_group_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async chat_group_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('chat_group_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async chat_group_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('chat_group_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async chat_message_get(params: { id: number }): Promise<ChatMessage> {
|
|
||||||
return this.send('chat_message_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async chat_message_set(params: { chat_message: ChatMessage }): Promise<number> {
|
|
||||||
return this.send('chat_message_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async chat_message_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('chat_message_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async chat_message_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('chat_message_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async chat_message_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('chat_message_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async contact_get(params: { id: number }): Promise<Contact> {
|
|
||||||
return this.send('contact_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async contact_set(params: { contact: Contact }): Promise<number> {
|
|
||||||
return this.send('contact_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async contact_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('contact_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async contact_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('contact_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async contact_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('contact_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async group_get(params: { id: number }): Promise<Group> {
|
|
||||||
return this.send('group_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async group_set(params: { group: Group }): Promise<number> {
|
|
||||||
return this.send('group_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async group_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('group_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async group_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('group_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async group_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('group_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async message_get(params: { id: number }): Promise<Message> {
|
|
||||||
return this.send('message_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async message_set(params: { message: Message }): Promise<number> {
|
|
||||||
return this.send('message_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async message_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('message_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async message_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('message_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async message_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('message_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async planning_get(params: { id: number }): Promise<Planning> {
|
|
||||||
return this.send('planning_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async planning_set(params: { planning: Planning }): Promise<number> {
|
|
||||||
return this.send('planning_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async planning_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('planning_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async planning_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('planning_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async planning_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('planning_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async profile_get(params: { id: number }): Promise<Profile> {
|
|
||||||
return this.send('profile_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async profile_set(params: { profile: Profile }): Promise<number> {
|
|
||||||
return this.send('profile_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async profile_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('profile_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async profile_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('profile_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async profile_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('profile_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async project_get(params: { id: number }): Promise<Project> {
|
|
||||||
return this.send('project_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async project_set(params: { project: Project }): Promise<number> {
|
|
||||||
return this.send('project_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async project_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('project_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async project_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('project_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async project_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('project_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async project_issue_get(params: { id: number }): Promise<ProjectIssue> {
|
|
||||||
return this.send('project_issue_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async project_issue_set(params: { project_issue: ProjectIssue }): Promise<number> {
|
|
||||||
return this.send('project_issue_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async project_issue_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('project_issue_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async project_issue_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('project_issue_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async project_issue_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('project_issue_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async registration_desk_get(params: { id: number }): Promise<RegistrationDesk> {
|
|
||||||
return this.send('registration_desk_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async registration_desk_set(params: { registration_desk: RegistrationDesk }): Promise<number> {
|
|
||||||
return this.send('registration_desk_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async registration_desk_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('registration_desk_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async registration_desk_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('registration_desk_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async registration_desk_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('registration_desk_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async user_get(params: { id: number }): Promise<User> {
|
|
||||||
return this.send('user_get', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async user_set(params: { user: User }): Promise<number> {
|
|
||||||
return this.send('user_set', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async user_delete(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('user_delete', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async user_exist(params: { id: number }): Promise<boolean> {
|
|
||||||
return this.send('user_exist', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async user_list(params: { }): Promise<any[]> {
|
|
||||||
return this.send('user_list', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,11 @@ module typescriptgenerator
|
|||||||
|
|
||||||
import freeflowuniverse.herolib.core.pathlib
|
import freeflowuniverse.herolib.core.pathlib
|
||||||
import freeflowuniverse.herolib.core.texttools
|
import freeflowuniverse.herolib.core.texttools
|
||||||
|
import os
|
||||||
|
|
||||||
pub fn generate_typescript_client(spec IntermediateSpec, dest_path string) ! {
|
pub fn generate_typescript_client(spec IntermediateSpec, dest_path string) ! {
|
||||||
mut dest := pathlib.get_dir(path: dest_path, create: true)!
|
mut dest := pathlib.get_dir(path: dest_path, create: true)!
|
||||||
|
|
||||||
// Generate a file for each schema
|
// Generate a file for each schema
|
||||||
for name, schema in spec.schemas {
|
for name, schema in spec.schemas {
|
||||||
mut file_content := generate_interface(schema)
|
mut file_content := generate_interface(schema)
|
||||||
@@ -17,6 +18,30 @@ pub fn generate_typescript_client(spec IntermediateSpec, dest_path string) ! {
|
|||||||
mut client_content := generate_client(spec)
|
mut client_content := generate_client(spec)
|
||||||
mut client_file_path := pathlib.get_file(path: '${dest.path}/client.ts', create: true)!
|
mut client_file_path := pathlib.get_file(path: '${dest.path}/client.ts', create: true)!
|
||||||
client_file_path.write(client_content) or { panic(err) }
|
client_file_path.write(client_content) or { panic(err) }
|
||||||
|
|
||||||
|
// Copy templates to destination
|
||||||
|
mut templates_src := os.dir(@FILE) + '/templates'
|
||||||
|
if os.exists(templates_src) {
|
||||||
|
// Copy index.html template
|
||||||
|
mut index_content := os.read_file('${templates_src}/index.html')!
|
||||||
|
mut index_file := pathlib.get_file(path: '${dest.path}/index.html', create: true)!
|
||||||
|
index_file.write(index_content)!
|
||||||
|
|
||||||
|
// Copy package.json template
|
||||||
|
mut package_content := os.read_file('${templates_src}/package.json')!
|
||||||
|
mut package_file := pathlib.get_file(path: '${dest.path}/package.json', create: true)!
|
||||||
|
package_file.write(package_content)!
|
||||||
|
|
||||||
|
// Copy tsconfig.json template
|
||||||
|
mut tsconfig_content := os.read_file('${templates_src}/tsconfig.json')!
|
||||||
|
mut tsconfig_file := pathlib.get_file(path: '${dest.path}/tsconfig.json', create: true)!
|
||||||
|
tsconfig_file.write(tsconfig_content)!
|
||||||
|
|
||||||
|
// Copy dev-server.ts template
|
||||||
|
mut dev_server_content := os.read_file('${templates_src}/dev-server.ts')!
|
||||||
|
mut dev_server_file := pathlib.get_file(path: '${dest.path}/dev-server.ts', create: true)!
|
||||||
|
dev_server_file.write(dev_server_content)!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_interface(schema IntermediateSchema) string {
|
fn generate_interface(schema IntermediateSchema) string {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ fn test_generate_typescript_client() {
|
|||||||
|
|
||||||
config := IntermediateConfig{
|
config := IntermediateConfig{
|
||||||
base_url: 'http://localhost:8086/api/heromodels'
|
base_url: 'http://localhost:8086/api/heromodels'
|
||||||
|
handler_type: 'heromodels'
|
||||||
}
|
}
|
||||||
|
|
||||||
intermediate_spec := from_openrpc(openrpc_spec, config) or {
|
intermediate_spec := from_openrpc(openrpc_spec, config) or {
|
||||||
|
|||||||
34
lib/hero/typescriptgenerator/install.sh
Executable file
34
lib/hero/typescriptgenerator/install.sh
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Install script for Hero Models TypeScript client
|
||||||
|
|
||||||
|
# Check if bun is installed
|
||||||
|
if ! command -v bun &> /dev/null; then
|
||||||
|
echo "bun is not installed. Please install bun first:"
|
||||||
|
echo "curl -fsSL https://bun.sh/install | bash"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if V is installed
|
||||||
|
if ! command -v v &> /dev/null; then
|
||||||
|
echo "V is not installed. Please install V first:"
|
||||||
|
echo "Visit https://vlang.io/ for installation instructions"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create output directory if it doesn't exist
|
||||||
|
OUTPUT_DIR="${1:-~/code/heromodels/generated}"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
# Generate TypeScript client
|
||||||
|
echo "Generating TypeScript client in $OUTPUT_DIR..."
|
||||||
|
v -enable-globals -n -w -gc none run lib/hero/typescriptgenerator/generate.vsh
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
echo "Installing dependencies..."
|
||||||
|
cd "$OUTPUT_DIR"
|
||||||
|
bun install
|
||||||
|
|
||||||
|
echo "Installation complete! The TypeScript client is ready to use."
|
||||||
|
echo "To test in development mode, run: bun run dev"
|
||||||
|
echo "To build for production, run: bun run build"
|
||||||
@@ -63,8 +63,9 @@ pub fn from_openrpc(openrpc_spec openrpc.OpenRPC, config IntermediateConfig) !In
|
|||||||
|
|
||||||
mut intermediate_spec := IntermediateSpec{
|
mut intermediate_spec := IntermediateSpec{
|
||||||
info: openrpc_spec.info
|
info: openrpc_spec.info
|
||||||
base_url: config.base_url
|
methods: []IntermediateMethod{}
|
||||||
schemas: process_schemas(openrpc_spec.components.schemas)!
|
schemas: process_schemas(openrpc_spec.components.schemas)!
|
||||||
|
base_url: config.base_url
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process all methods
|
// Process all methods
|
||||||
@@ -110,6 +111,13 @@ fn process_parameters(params []openrpc.ContentDescriptorRef) ![]IntermediatePara
|
|||||||
}
|
}
|
||||||
} else if param is jsonschema.Reference {
|
} else if param is jsonschema.Reference {
|
||||||
//TODO: handle reference
|
//TODO: handle reference
|
||||||
|
// For now, create a placeholder parameter
|
||||||
|
intermediate_params << IntermediateParam{
|
||||||
|
name: 'reference'
|
||||||
|
description: ''
|
||||||
|
type_info: 'any'
|
||||||
|
required: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +125,12 @@ fn process_parameters(params []openrpc.ContentDescriptorRef) ![]IntermediatePara
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn process_result(result openrpc.ContentDescriptorRef) !IntermediateParam {
|
fn process_result(result openrpc.ContentDescriptorRef) !IntermediateParam {
|
||||||
mut intermediate_result := IntermediateParam{}
|
mut intermediate_result := IntermediateParam{
|
||||||
|
name: ''
|
||||||
|
description: ''
|
||||||
|
type_info: ''
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
|
||||||
if result is openrpc.ContentDescriptor {
|
if result is openrpc.ContentDescriptor {
|
||||||
type_info := extract_type_from_schema(result.schema)
|
type_info := extract_type_from_schema(result.schema)
|
||||||
@@ -134,9 +147,18 @@ fn process_result(result openrpc.ContentDescriptorRef) !IntermediateParam {
|
|||||||
type_info := ref.ref.all_after_last('/')
|
type_info := ref.ref.all_after_last('/')
|
||||||
intermediate_result = IntermediateParam{
|
intermediate_result = IntermediateParam{
|
||||||
name: type_info.to_lower()
|
name: type_info.to_lower()
|
||||||
|
description: ''
|
||||||
type_info: type_info
|
type_info: type_info
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle any other cases
|
||||||
|
intermediate_result = IntermediateParam{
|
||||||
|
name: 'unknown'
|
||||||
|
description: ''
|
||||||
|
type_info: 'unknown'
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return intermediate_result
|
return intermediate_result
|
||||||
@@ -168,14 +190,14 @@ fn process_schemas(schemas map[string]jsonschema.SchemaRef) !map[string]Intermed
|
|||||||
mut properties := []IntermediateProperty{}
|
mut properties := []IntermediateProperty{}
|
||||||
for prop_name, prop_schema_ref in schema.properties {
|
for prop_name, prop_schema_ref in schema.properties {
|
||||||
prop_type := extract_type_from_schema(prop_schema_ref)
|
prop_type := extract_type_from_schema(prop_schema_ref)
|
||||||
properties << IntermediateProperty {
|
properties << IntermediateProperty{
|
||||||
name: prop_name
|
name: prop_name
|
||||||
description: "" // TODO
|
description: "" // TODO
|
||||||
type_info: prop_type
|
type_info: prop_type
|
||||||
required: prop_name in schema.required
|
required: prop_name in schema.required
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
intermediate_schemas[name] = IntermediateSchema {
|
intermediate_schemas[name] = IntermediateSchema{
|
||||||
name: name
|
name: name
|
||||||
description: schema.description
|
description: schema.description
|
||||||
properties: properties
|
properties: properties
|
||||||
|
|||||||
32
lib/hero/typescriptgenerator/templates/dev-server.ts
Normal file
32
lib/hero/typescriptgenerator/templates/dev-server.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
// Dev server script for Hero Models TypeScript client
|
||||||
|
|
||||||
|
import { serve } from "bun";
|
||||||
|
|
||||||
|
const server = serve({
|
||||||
|
port: 3000,
|
||||||
|
fetch(req) {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
|
||||||
|
// Serve static files
|
||||||
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
||||||
|
return new Response(Bun.file("index.html"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve TypeScript files
|
||||||
|
if (url.pathname.endsWith(".ts")) {
|
||||||
|
return new Response(Bun.file(url.pathname.slice(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve JavaScript files (compiled TypeScript)
|
||||||
|
if (url.pathname.endsWith(".js")) {
|
||||||
|
return new Response(Bun.file(url.pathname.slice(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response("Not found", { status: 404 });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Dev server running on http://localhost:${server.port}`);
|
||||||
|
console.log("Press Ctrl+C to stop the server");
|
||||||
125
lib/hero/typescriptgenerator/templates/index.html
Normal file
125
lib/hero/typescriptgenerator/templates/index.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Hero Models TypeScript Client Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.test-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
.result {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
input, select {
|
||||||
|
padding: 8px;
|
||||||
|
margin: 5px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Hero Models TypeScript Client Test</h1>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>Calendar Operations</h2>
|
||||||
|
<label>Calendar ID: <input type="number" id="calendarId" value="1"></label>
|
||||||
|
<button onclick="getCalendar()">Get Calendar</button>
|
||||||
|
<div class="result" id="calendarResult"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>User Operations</h2>
|
||||||
|
<label>User ID: <input type="number" id="userId" value="1"></label>
|
||||||
|
<button onclick="getUser()">Get User</button>
|
||||||
|
<div class="result" id="userResult"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>Event Operations</h2>
|
||||||
|
<label>Event ID: <input type="number" id="eventId" value="1"></label>
|
||||||
|
<button onclick="getEvent()">Get Event</button>
|
||||||
|
<div class="result" id="eventResult"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { HeroModelsClient } from './client.js';
|
||||||
|
|
||||||
|
const client = new HeroModelsClient('http://localhost:8086/api/heromodels');
|
||||||
|
|
||||||
|
async function getCalendar() {
|
||||||
|
const resultDiv = document.getElementById('calendarResult');
|
||||||
|
try {
|
||||||
|
const id = parseInt(document.getElementById('calendarId').value);
|
||||||
|
const calendar = await client.calendar_get({id});
|
||||||
|
resultDiv.textContent = JSON.stringify(calendar, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
resultDiv.textContent = `Error: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUser() {
|
||||||
|
const resultDiv = document.getElementById('userResult');
|
||||||
|
try {
|
||||||
|
const id = parseInt(document.getElementById('userId').value);
|
||||||
|
const user = await client.user_get({id});
|
||||||
|
resultDiv.textContent = JSON.stringify(user, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
resultDiv.textContent = `Error: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEvent() {
|
||||||
|
const resultDiv = document.getElementById('eventResult');
|
||||||
|
try {
|
||||||
|
const id = parseInt(document.getElementById('eventId').value);
|
||||||
|
const event = await client.event_get({id});
|
||||||
|
resultDiv.textContent = JSON.stringify(event, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
resultDiv.textContent = `Error: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
lib/hero/typescriptgenerator/templates/package.json
Normal file
18
lib/hero/typescriptgenerator/templates/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "heromodels-client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TypeScript client for Hero Models API",
|
||||||
|
"main": "client.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run dev-server.ts",
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^3.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"@types/node": "^20.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
lib/hero/typescriptgenerator/templates/tsconfig.json
Normal file
23
lib/hero/typescriptgenerator/templates/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ES2020",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": ".",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"*.ts",
|
||||||
|
"templates/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user