This commit is contained in:
Rizky 2024-01-23 23:03:13 +07:00
commit 1bd21cf6a7
44 changed files with 2920 additions and 0 deletions

180
.gitignore vendored Normal file
View File

@ -0,0 +1,180 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
app/web
app/web/*
app/static
app/static/*

22
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true,
"node_modules": true,
".gitignore": true,
"bun.lockb": true,
".vscode": true,
"types.d.ts": true
},
"hide-files.files": [
"node_modules",
".gitignore",
"bun.lockb",
".vscode",
"types.d.ts"
]
}

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# prasi-deploy
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run src/index.ts
```
This project was created using `bun init` in bun v1.0.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

3
app/db/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
# Keep environment variables out of version control
.env

1
app/db/db.ts Normal file
View File

@ -0,0 +1 @@
export * from "@prisma/client";

7
app/db/package.json Normal file
View File

@ -0,0 +1,7 @@
{
"name": "db",
"dependencies": {
"@prisma/client": "5.3.1",
"prisma": "^5.3.1"
}
}

487
app/db/prisma/schema.prisma Normal file
View File

@ -0,0 +1,487 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model m_academic_year {
id Int @id @default(autoincrement())
name String @db.VarChar
is_active Boolean
created_by Int
created_date DateTime @db.Timestamptz(6)
updated_by Int?
updated_date DateTime? @db.Timestamptz(6)
id_institution Int
start_time DateTime? @db.Timestamptz(6)
end_time DateTime? @db.Timestamptz(6)
id_client Int?
m_client m_client? @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_user_m_user_id_yearTom_academic_year m_user[] @relation("m_user_id_yearTom_academic_year")
t_logbook t_logbook[]
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model m_action {
id Int @id @default(autoincrement())
id_type Int
name String @db.VarChar
has_notes Boolean
has_attachment Boolean
has_category Boolean
is_milestone Boolean
show_on_milestone Boolean
multiple_verification Boolean
has_score Boolean
has_presentation Boolean
has_location Boolean
has_emr Boolean
has_another_role Boolean
has_title Boolean
id_client Int
has_status Boolean @default(false)
show_on_menu Boolean @default(true)
has_hospital Boolean @default(false)
attachment_name Json? @default("[]")
has_score_option Boolean @default(false)
is_schedule Boolean @default(false)
max_entry_per_day Int @default(0)
identifier String @default(" ") @db.VarChar
is_grouped_by_category Boolean @default(false)
has_operation_code Boolean @default(false)
is_exam Boolean @default(false)
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_action_type m_action_type @relation(fields: [id_type], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_action_category m_action_category[]
m_action_rolemap m_action_rolemap[]
m_action_semester m_action_semester[]
m_asm_action m_asm_action[]
m_score_option m_score_option[]
m_user_action m_user_action[]
t_logbook t_logbook[]
}
model m_action_category {
id Int @id(map: "m_action_sub_category_pkey") @default(autoincrement())
name String @db.VarChar
id_action Int
id_client Int
required_asm Boolean @default(false)
m_action m_action @relation(fields: [id_action], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_action_sub_category_id_action_fkey")
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_action_sub_category_id_client_fkey")
m_user m_user[]
t_logbook t_logbook[]
}
model m_action_role {
id Int @id(map: "m_logbook_role_pkey") @default(autoincrement())
role String @db.VarChar
id_client Int
identifier String @default(" ") @db.VarChar
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_logbook_role_id_client_fkey")
m_action_rolemap m_action_rolemap[]
t_logbook_status t_logbook_status[]
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model m_action_rolemap {
id Int @id(map: "m_action_verificator_pkey") @default(autoincrement())
id_action_role Int
id_action Int
id_client Int
type String? @default("verificator") @db.VarChar
is_required Boolean @default(true)
m_action m_action @relation(fields: [id_action], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_action_verificator_id_action_fkey")
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_action_verificator_id_client_fkey")
m_action_role m_action_role @relation(fields: [id_action_role], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_action_verificator_id_logbook_role_fkey")
}
model m_action_semester {
id Int @id @default(autoincrement())
id_semester Int
id_action Int
id_client Int
m_action m_action @relation(fields: [id_action], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_semester m_semester @relation(fields: [id_semester], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model m_action_status {
id Int @id @default(autoincrement())
status String @db.VarChar
id_action Int
id_client Int
}
model m_action_type {
id Int @id @default(autoincrement())
name String @db.VarChar
id_client Int
m_action m_action[]
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model m_asm_action {
id Int @id(map: "m_assesment_indicator_action_pkey") @default(autoincrement())
id_action Int
id_asm_param Int
id_client Int
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_action m_action @relation(fields: [id_action], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_assesment_indicator_action_id_action_fkey")
m_asm_param m_asm_param @relation(fields: [id_asm_param], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_assesment_indicator_action_id_assesment_indicator_fkey")
}
model m_asm_param {
id Int @id(map: "m_assesment_indicator_pkey") @default(autoincrement())
name String @db.VarChar
min_score Float
max_score Float
id_client Int
m_asm_action m_asm_action[]
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_assessment_indicator_id_client_fkey")
t_logbook_asm t_logbook_asm[]
}
model m_client {
id Int @id @default(autoincrement())
name String @db.VarChar
m_academic_year m_academic_year[]
m_action m_action[]
m_action_category m_action_category[]
m_action_role m_action_role[]
m_action_rolemap m_action_rolemap[]
m_action_type m_action_type[]
m_another_role m_another_role[]
m_asm_action m_asm_action[]
m_asm_param m_asm_param[]
m_client_logo m_client_logo[]
m_hospital m_hospital[]
m_role m_role[]
m_score_option m_score_option[]
m_semester m_semester[]
m_session m_session[]
m_stage m_stage[]
m_stase m_stase[]
m_user_m_user_id_clientTom_client m_user[] @relation("m_user_id_clientTom_client")
m_user_m_user_id_institutionTom_client m_user[] @relation("m_user_id_institutionTom_client")
m_user_action m_user_action[]
t_audit_trails t_audit_trails[]
t_logbook t_logbook[]
t_logbook_asm t_logbook_asm[]
t_logbook_attachment t_logbook_attachment[]
other_t_logbook_emr t_logbook_emr[] @relation("t_logbook_emrTot_logbook_emr")
t_logbook_status t_logbook_status[]
t_menu t_menu[]
t_notif t_notif[]
}
model m_client_logo {
id Int @id @default(autoincrement())
id_client Int
file String
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model m_hospital {
id Int @id @default(autoincrement())
name String @db.VarChar
address String? @db.VarChar
notes String? @db.VarChar
longitude String? @db.VarChar
latitude String? @db.VarChar
created_by Int
created_date DateTime @db.Timestamptz(6)
updated_date DateTime? @db.Timestamptz(6)
updated_by Int?
id_client Int
code String? @db.VarChar
m_user_m_hospital_created_byTom_user m_user @relation("m_hospital_created_byTom_user", fields: [created_by], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_user_m_hospital_updated_byTom_user m_user? @relation("m_hospital_updated_byTom_user", fields: [updated_by], references: [id], onDelete: NoAction, onUpdate: NoAction)
t_logbook t_logbook[]
}
model m_role {
id Int @id(map: "mst_role_pkey") @default(autoincrement())
name String @db.VarChar
created_date DateTime @db.Timestamptz(6)
created_by Int
updated_date DateTime? @db.Timestamptz(6)
updated_by Int?
id_client Int
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_user m_user[]
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model m_score_option {
id Int @id(map: "m_score_dropdown_pkey") @default(autoincrement())
score Float
id_action Int?
id_client Int
m_action m_action? @relation(fields: [id_action], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_score_dropdown_id_action_fkey")
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_score_dropdown_id_client_fkey")
}
model m_semester {
id Int @id @default(autoincrement())
name String @db.VarChar
id_stage Int
id_client Int
m_action_semester m_action_semester[]
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_stage m_stage @relation(fields: [id_stage], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_user m_user[]
t_logbook t_logbook[]
}
model m_session {
id_user Int?
session_id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
created_at DateTime? @db.Timestamptz(6)
updated_at DateTime? @db.Timestamptz(6)
id_client Int
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_user m_user? @relation(fields: [id_user], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_user")
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model m_stage {
id Int @id @default(autoincrement())
name String @db.VarChar
label_color String @db.VarChar
id_institution Int
created_date DateTime @db.Timestamptz(6)
created_by Int
updated_date DateTime? @db.Timestamptz(6)
updated_by Int?
code String? @db.VarChar(20)
id_client Int
m_semester m_semester[]
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_stase m_stase[]
}
model m_stase {
id Int @id @default(autoincrement())
name String @db.VarChar
id_stage Int
id_client Int
sequence Int
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_stage m_stage @relation(fields: [id_stage], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_stase_id_semester_fkey")
m_user m_user[]
t_logbook t_logbook[]
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model m_user {
id Int @id(map: "mst_user_pkey") @default(autoincrement())
display_name String @db.VarChar
username String? @db.VarChar
email String? @db.VarChar
password String? @db.VarChar(255)
id_role Int
is_deleted Boolean @default(false)
created_date DateTime @db.Timestamptz(6)
created_by Int?
updated_date DateTime? @db.Timestamptz(6)
updated_by Int?
phone String? @db.VarChar
address String? @db.VarChar
date_of_birth DateTime? @db.Timestamptz(6)
code String? @db.VarChar
picture String?
id_institution Int?
id_sub_category Int?
id_semester Int?
id_stase Int?
id_client Int
gender String? @db.VarChar
id_year Int?
status String @default("Active") @db.VarChar
inisial_code String? @db.VarChar
is_show Boolean @default(true)
m_hospital_m_hospital_created_byTom_user m_hospital[] @relation("m_hospital_created_byTom_user")
m_hospital_m_hospital_updated_byTom_user m_hospital[] @relation("m_hospital_updated_byTom_user")
m_session m_session[]
m_client_m_user_id_clientTom_client m_client @relation("m_user_id_clientTom_client", fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_client_m_user_id_institutionTom_client m_client? @relation("m_user_id_institutionTom_client", fields: [id_institution], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_semester m_semester? @relation(fields: [id_semester], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_stase m_stase? @relation(fields: [id_stase], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_action_category m_action_category? @relation(fields: [id_sub_category], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_academic_year_m_user_id_yearTom_academic_year m_academic_year? @relation("m_user_id_yearTom_academic_year", fields: [id_year], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_user_m_user_created_byTom_user m_user? @relation("m_user_created_byTom_user", fields: [created_by], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "mst_user_created_by_fkey")
other_m_user_m_user_created_byTom_user m_user[] @relation("m_user_created_byTom_user")
m_role m_role @relation(fields: [id_role], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "mst_user_id_role_fkey")
m_user_m_user_updated_byTom_user m_user? @relation("m_user_updated_byTom_user", fields: [updated_by], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "mst_user_updated_by_fkey")
other_m_user_m_user_updated_byTom_user m_user[] @relation("m_user_updated_byTom_user")
m_user_action m_user_action[]
t_audit_trails t_audit_trails[]
t_logbook t_logbook[]
t_logbook_status t_logbook_status[]
t_menu_t_menu_created_byTom_user t_menu[] @relation("t_menu_created_byTom_user")
t_menu_t_menu_updated_byTom_user t_menu[] @relation("t_menu_updated_byTom_user")
t_notif t_notif[]
}
model m_user_action {
id Int @id(map: "m_action_user_pkey") @default(autoincrement())
id_action_hidden Int
id_user Int
id_client Int
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_action_user_id_client_fkey")
m_action m_action @relation(fields: [id_action_hidden], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_user m_user @relation(fields: [id_user], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model t_audit_trails {
id Int @id @default(autoincrement())
activity String
ip_user String? @db.VarChar
id_user Int
timestamp DateTime @db.Timestamptz(6)
type String @default("visit") @db.VarChar
meta Json?
id_client Int
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_user m_user @relation(fields: [id_user], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model t_logbook {
id Int @id @default(autoincrement())
title String?
notes String?
date DateTime @db.Timestamptz(6)
location String? @db.VarChar
id_hospital Int?
id_category Int?
id_action Int
is_presentation Boolean @default(false)
created_by Int
created_date DateTime @db.Timestamptz(6)
updated_by Int?
updated_date DateTime? @db.Timestamptz(6)
id_user Int?
id_client Int
verified Boolean? @default(false)
operation_code String? @db.VarChar
id_another_role Int?
id_semester Int?
id_stase Int?
schedule_status String @default("pending") @db.VarChar
exam_result String @default("Lolos") @db.VarChar
id_academic_year Int?
verified_status String @default("pending") @db.VarChar
m_academic_year m_academic_year? @relation(fields: [id_academic_year], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_action m_action @relation(fields: [id_action], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_action_category m_action_category? @relation(fields: [id_category], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_hospital m_hospital? @relation(fields: [id_hospital], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_another_role m_another_role? @relation(fields: [id_another_role], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "t_logbook_id_operation_type_fkey")
m_semester m_semester? @relation(fields: [id_semester], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_stase m_stase? @relation(fields: [id_stase], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_user m_user? @relation(fields: [id_user], references: [id], onDelete: NoAction, onUpdate: NoAction)
t_logbook_asm t_logbook_asm[]
t_logbook_attachment t_logbook_attachment[]
t_logbook_emr t_logbook_emr[]
t_logbook_status t_logbook_status[]
t_notif t_notif[]
}
model t_logbook_asm {
id Int @id(map: "t_logbook_assesment_pkey") @default(autoincrement())
id_logbook Int
id_asm_param Int
score Float
created_by DateTime @db.Timestamptz(6)
created_date Int
updated_date DateTime? @db.Timestamptz(6)
updated_by Int?
id_client Int
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_asm_param m_asm_param @relation(fields: [id_asm_param], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "t_logbook_assesment_id_assesment_indicator_fkey")
t_logbook t_logbook @relation(fields: [id_logbook], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "t_logbook_assesment_id_logbook_fkey")
}
model t_logbook_attachment {
id Int @id @default(autoincrement())
id_logbook Int
url_file String?
name String? @db.VarChar
id_client Int
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
t_logbook t_logbook @relation(fields: [id_logbook], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model t_logbook_emr {
id Int @id(map: "t_logbook_medical_record_pkey") @default(autoincrement())
id_logbook Int
emr_number String? @db.VarChar
diagnosis String? @db.VarChar
treatment String? @db.VarChar
age Int?
gender String? @db.VarChar
id_client Int?
month Int?
patient_name String? @db.VarChar
t_logbook_emr m_client? @relation("t_logbook_emrTot_logbook_emr", fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
t_logbook t_logbook @relation(fields: [id_logbook], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "t_logbook_medical_record_id_logbook_fkey")
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model t_logbook_status {
id Int @id(map: "t_logbook_participants_pkey") @default(autoincrement())
id_user Int
id_logbook Int
id_action_role Int
status String @default("pending") @db.VarChar
date_time DateTime? @db.Timestamptz(6)
id_client Int
m_user m_user @relation(fields: [id_user], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "t_logbook_participants_id_user_fkey")
m_action_role m_action_role @relation(fields: [id_action_role], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
t_logbook t_logbook @relation(fields: [id_logbook], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "t_logbook_participants_id_logbook_fkey")
}
model t_menu {
id Int @id @default(autoincrement())
id_user String @db.VarChar
created_by Int
updated_date DateTime? @db.Timestamptz(6)
updated_by Int?
id_action Int
id_client Int
m_user_t_menu_created_byTom_user m_user @relation("t_menu_created_byTom_user", fields: [created_by], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_user_t_menu_updated_byTom_user m_user? @relation("t_menu_updated_byTom_user", fields: [updated_by], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model t_notif {
id Int @id @default(autoincrement())
message String
date DateTime @db.Timestamptz(6)
type String @db.VarChar
id_user Int
url String @db.VarChar
id_role Int
read Boolean @default(false)
id_client Int
id_logbook Int?
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction)
t_logbook t_logbook? @relation(fields: [id_logbook], references: [id], onDelete: NoAction, onUpdate: NoAction)
m_user m_user @relation(fields: [id_user], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model m_another_role {
id Int @id(map: "m_operation_type_pkey") @default(autoincrement())
role_name String @db.VarChar
id_client Int
m_client m_client @relation(fields: [id_client], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "m_operation_type_id_client_fkey")
t_logbook t_logbook[]
}

1
app/db/test.mjs Normal file
View File

@ -0,0 +1 @@
import { PrismaClient } from "@prisma/client";

292
app/srv/exports.d.ts vendored Normal file
View File

@ -0,0 +1,292 @@
declare module "app/db/db" {
}
declare module "pkgs/utils/global" {
import { Logger } from "pino";
import { RadixRouter } from "radix3";
import { Database } from "bun:sqlite";
type SingleRoute = {
url: string;
args: string[];
fn: (...arg: any[]) => Promise<any>;
path: string;
};
export const g: {
dburl: string;
datadir: string;
mode: "dev" | "prod";
log: Logger;
firebaseInit: boolean;
firebase: admin.app.App;
notif: {
db: Database;
};
api: Record<string, SingleRoute>;
domains: null | Record<string, string>;
web: Record<string, {
site_id: string;
current: number;
deploying: null | {
status: string;
received: number;
total: number;
};
deploys: number[];
domains: string[];
router: null | RadixRouter<{
id: string;
}>;
cacheKey: number;
cache: null | {
site: {
id: string;
name: string;
favicon: string;
domain: string;
id_user: string;
created_at: Date | null;
id_org: string | null;
updated_at: Date | null;
responsive: string;
} | null;
pages: {
id: string;
name: string;
url: string;
content_tree: any;
id_site: string;
created_at: Date | null;
js_compiled: string | null;
js: string | null;
updated_at: Date | null;
id_folder: string | null;
is_deleted: boolean;
}[];
npm: {
site: Record<string, string>;
pages: Record<string, Record<string, string>>;
};
comps: {
id: string;
name: string;
content_tree: any;
created_at: Date | null;
updated_at: Date | null;
type: string;
id_component_group: string | null;
props: any;
}[];
};
}>;
router: RadixRouter<SingleRoute>;
port: number;
frm: {
js: string;
etag: string;
};
};
}
declare module "pkgs/server/serve-web" {
export const serveWeb: (url: URL, req: Request) => Promise<false | string[] | Response>;
export const generateIndexHtml: (base_url: string, site_id: string) => string;
}
declare module "pkgs/api/_file" {
export const _: {
url: string;
api(): Promise<Response>;
};
}
declare module "pkgs/utils/dir" {
export const dir: (path: string) => string;
}
declare module "pkgs/server/load-web" {
export const loadWeb: () => Promise<void>;
export const loadWebCache: (site_id: string, ts: number | string) => Promise<void>;
}
declare module "pkgs/api/_deploy" {
export const _: {
url: string;
api(action: ({
type: "check";
} | {
type: "db-update";
url: string;
} | {
type: "db-pull";
} | {
type: "restart";
} | {
type: "domain-add";
domain: string;
} | {
type: "domain-del";
domain: string;
} | {
type: "deploy-del";
ts: string;
} | {
type: "deploy";
dlurl: string;
} | {
type: "deploy-status";
} | {
type: "redeploy";
ts: string;
}) & {
id_site: string;
}): Promise<"ok" | {
now: number;
current: any;
deploys: any;
domains: any;
db: {
url: any;
};
} | {
now: number;
current: any;
deploys: any;
domains?: undefined;
db?: undefined;
}>;
};
export const downloadFile: (url: string, filePath: string, progress?: (rec: number, total: number) => void) => Promise<boolean>;
}
declare module "pkgs/api/_prasi" {
export const _: {
url: string;
api(): Promise<void>;
};
export const getApiEntry: () => any;
}
declare module "pkgs/api/_notif" {
export const _: {
url: string;
api(action: string, data: {
type: "register";
token: string;
id: string;
} | {
type: "send";
id: string;
body: string;
title: string;
data?: any;
}): Promise<{
result: string;
error?: undefined;
totalDevice?: undefined;
} | {
error: string;
result?: undefined;
totalDevice?: undefined;
} | {
result: string;
totalDevice: any;
error?: undefined;
}>;
};
}
declare module "pkgs/api/_web" {
export const _: {
url: string;
api(id: string, _: string): Promise<any>;
};
}
declare module "pkgs/api/_proxy" {
export const _: {
url: string;
api(arg: {
url: string;
method: "POST" | "GET";
headers: any;
body: any;
}): Promise<Response>;
};
}
declare module "pkgs/api/_upload" {
export const _: {
url: string;
api(body: any): Promise<string>;
};
}
declare module "pkgs/api/_dbs" {
export const _: {
url: string;
api(dbName: any, action?: string): Promise<void>;
};
}
declare module "pkgs/api/_api_frm" {
export const _: {
url: string;
api(): Promise<void>;
};
}
declare module "app/srv/exports" {
export const _file: {
name: string;
url: string;
path: string;
args: any[];
handler: Promise<typeof import("pkgs/api/_file")>;
};
export const _deploy: {
name: string;
url: string;
path: string;
args: string[];
handler: Promise<typeof import("pkgs/api/_deploy")>;
};
export const _prasi: {
name: string;
url: string;
path: string;
args: any[];
handler: Promise<typeof import("pkgs/api/_prasi")>;
};
export const _notif: {
name: string;
url: string;
path: string;
args: string[];
handler: Promise<typeof import("pkgs/api/_notif")>;
};
export const _web: {
name: string;
url: string;
path: string;
args: string[];
handler: Promise<typeof import("pkgs/api/_web")>;
};
export const _proxy: {
name: string;
url: string;
path: string;
args: string[];
handler: Promise<typeof import("pkgs/api/_proxy")>;
};
export const _upload: {
name: string;
url: string;
path: string;
args: string[];
handler: Promise<typeof import("pkgs/api/_upload")>;
};
export const _dbs: {
name: string;
url: string;
path: string;
args: string[];
handler: Promise<typeof import("pkgs/api/_dbs")>;
};
export const _api_frm: {
name: string;
url: string;
path: string;
args: any[];
handler: Promise<typeof import("pkgs/api/_api_frm")>;
};
}

63
app/srv/exports.ts Normal file
View File

@ -0,0 +1,63 @@
export const _file = {
name: "_file",
url: "/_file/**",
path: "app/srv/api/_file.ts",
args: [],
handler: import("../../pkgs/api/_file")
}
export const _deploy = {
name: "_deploy",
url: "/_deploy",
path: "app/srv/api/_deploy.ts",
args: ["action"],
handler: import("../../pkgs/api/_deploy")
}
export const _prasi = {
name: "_prasi",
url: "/_prasi/**",
path: "app/srv/api/_prasi.ts",
args: [],
handler: import("../../pkgs/api/_prasi")
}
export const _notif = {
name: "_notif",
url: "/_notif/:action/:token",
path: "app/srv/api/_notif.ts",
args: ["action","data"],
handler: import("../../pkgs/api/_notif")
}
export const _web = {
name: "_web",
url: "/_web/:id/**",
path: "app/srv/api/_web.ts",
args: ["id","_"],
handler: import("../../pkgs/api/_web")
}
export const _proxy = {
name: "_proxy",
url: "/_proxy/*",
path: "app/srv/api/_proxy.ts",
args: ["arg"],
handler: import("../../pkgs/api/_proxy")
}
export const _upload = {
name: "_upload",
url: "/_upload",
path: "app/srv/api/_upload.ts",
args: ["body"],
handler: import("../../pkgs/api/_upload")
}
export const _dbs = {
name: "_dbs",
url: "/_dbs/:dbName/:action",
path: "app/srv/api/_dbs.ts",
args: ["dbName","action"],
handler: import("../../pkgs/api/_dbs")
}
export const _api_frm = {
name: "_api_frm",
url: "/_api_frm",
path: "app/srv/api/_api_frm.ts",
args: [],
handler: import("../../pkgs/api/_api_frm")
}

1
app/srv/global.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "@surfy/multipart-parser";

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "prasi-deploy",
"module": "src/index.ts",
"type": "module",
"scripts": {
"dev": "bun run --silent --watch ./pkgs/index.ts dev",
"prod": "bun run --silent ./pkgs/index.ts",
"pkgs-upgrade": "bun run --silent ./pkgs/upgrade.ts"
},
"workspaces": [
"app/*",
"pkgs"
],
"devDependencies": {
"bun-types": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": { "firebase-admin": "^11.11.0" }
}

22
pkgs/api/_api_frm.ts Normal file
View File

@ -0,0 +1,22 @@
import { apiContext } from "service-srv";
import { g } from "utils/global";
export const _ = {
url: "/_api_frm",
async api() {
const { req, res } = apiContext(this);
let allowUrl = req.headers.get("origin") || req.headers.get("referer");
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT");
res.setHeader("Access-Control-Allow-Headers", "content-type");
res.setHeader("Access-Control-Allow-Credentials", "true");
if (allowUrl) {
res.setHeader("Access-Control-Allow-Origin", allowUrl);
}
res.setHeader("content-type", "text/html");
res.setHeader("etag", g.frm.etag);
res.send(`<script>${g.frm.js}</script>`);
},
};

20
pkgs/api/_dbs.ts Normal file
View File

@ -0,0 +1,20 @@
import { apiContext } from "service-srv";
import { execQuery } from "utils/query";
export const _ = {
url: "/_dbs/:dbName/:action",
async api(dbName: any, action?: string) {
const { req, res } = apiContext(this);
const body = req.params;
try {
const result = await execQuery(body, db);
res.send(result);
} catch (e: any) {
res.sendStatus(500);
res.send(e.message);
console.error(e);
}
},
};

228
pkgs/api/_deploy.ts Normal file
View File

@ -0,0 +1,228 @@
import { $ } from "execa";
import * as fs from "fs";
import { dirAsync, removeAsync } from "fs-jetpack";
import { apiContext } from "service-srv";
import { dir } from "utils/dir";
import { g } from "utils/global";
import { restartServer } from "utils/restart";
import { loadWebCache } from "../server/load-web";
export const _ = {
url: "/_deploy",
async api(
action: (
| { type: "check" }
| { type: "db-update"; url: string }
| { type: "db-pull" }
| { type: "restart" }
| { type: "domain-add"; domain: string }
| { type: "domain-del"; domain: string }
| { type: "deploy-del"; ts: string }
| { type: "deploy"; dlurl: string }
| { type: "deploy-status" }
| { type: "redeploy"; ts: string }
) & {
id_site: string;
}
) {
const { res } = apiContext(this);
if (!g.web[action.id_site]) {
g.web[action.id_site] = {
current: 0,
domains: [],
deploying: null,
router: null,
deploys: [],
site_id: action.id_site,
cacheKey: 0,
cache: null,
};
}
const path = dir(`app/web/${action.id_site}`);
await dirAsync(path);
const web = g.web[action.id_site];
if (!web.domains) {
web.domains = [];
}
switch (action.type) {
case "check":
return {
now: Date.now(),
current: web.current,
deploys: web.deploys,
domains: web.domains,
db: {
url: g.dburl || "-",
},
};
case "db-update":
if (action.url) {
g.dburl = action.url;
await Bun.write(
dir("app/db/.env"),
`\
DATABASE_URL="${action.url}"
`
);
}
return "ok";
case "db-pull":
{
await $({ cwd: dir("app/db") })`bun prisma db pull`;
await $({ cwd: dir("app/db") })`bun prisma generate`;
res.send("ok");
setTimeout(() => {
restartServer();
}, 300);
}
break;
case "restart":
{
res.send("ok");
setTimeout(() => {
restartServer();
}, 300);
}
break;
case "domain-add":
{
web.domains.push(action.domain);
await Bun.write(`${path}/domains.json`, JSON.stringify(web.domains));
g.domains = null;
res.send("ok");
}
break;
case "domain-del":
{
web.domains = web.domains.filter((e) => e !== action.domain);
await Bun.write(`${path}/domains.json`, web.domains);
g.domains = null;
res.send("ok");
}
break;
case "deploy-del":
{
web.deploys = web.deploys.filter((e) => e !== parseInt(action.ts));
try {
await removeAsync(`${path}/deploys/${action.ts}`);
} catch (e) {}
return {
now: Date.now(),
current: web.current,
deploys: web.deploys,
};
}
break;
case "deploy-status":
break;
case "deploy":
{
await fs.promises.mkdir(`${path}/deploys`, { recursive: true });
const cur = Date.now();
const filePath = `${path}/deploys/${cur}`;
web.deploying = {
status: "generating",
received: 0,
total: 0,
};
if (
await downloadFile(action.dlurl, filePath, (rec, total) => {
web.deploying = {
status: "transfering",
received: rec,
total: total,
};
})
) {
web.deploying.status = "deploying";
await fs.promises.writeFile(`${path}/current`, cur.toString());
web.current = cur;
web.deploys.push(cur);
await loadWebCache(web.site_id, web.current);
}
web.deploying = null;
return {
now: Date.now(),
current: web.current,
deploys: web.deploys,
};
}
break;
case "redeploy":
{
const cur = parseInt(action.ts);
const lastcur = web.current;
try {
if (web.deploys.find((e) => e === cur)) {
web.current = cur;
await fs.promises.writeFile(`${path}/current`, cur.toString());
await loadWebCache(web.site_id, web.current);
}
} catch (e) {
web.current = lastcur;
web.deploys = web.deploys.filter((e) => e !== parseInt(action.ts));
await removeAsync(`${path}/deploys/${action.ts}`);
}
return {
now: Date.now(),
current: web.current,
deploys: web.deploys,
};
}
break;
}
},
};
export const downloadFile = async (
url: string,
filePath: string,
progress?: (rec: number, total: number) => void
) => {
try {
const _url = new URL(url);
if (_url.hostname === "localhost") {
_url.hostname = "127.0.0.1";
}
const res = await fetch(_url);
if (res.body) {
const file = Bun.file(filePath);
const writer = file.writer();
const reader = res.body.getReader();
// Step 3: read the data
let receivedLength = 0; // received that many bytes at the moment
let chunks = []; // array of received binary chunks (comprises the body)
while (true) {
const { done, value } = await reader.read();
if (done) {
writer.end();
break;
}
chunks.push(value);
writer.write(value);
receivedLength += value.length;
if (progress) {
progress(
receivedLength,
parseInt(res.headers.get("content-length") || "0")
);
}
}
}
return true;
} catch (e) {
console.log(e);
return false;
}
};

70
pkgs/api/_file.ts Normal file
View File

@ -0,0 +1,70 @@
import { apiContext } from "service-srv";
import { dir } from "utils/dir";
import { g } from "utils/global";
import { generateIndexHtml } from "../server/serve-web";
import mime from "mime";
export const _ = {
url: "/_file/**",
async api() {
const { req } = apiContext(this);
const rpath = decodeURIComponent(req.params._);
let res = new Response("NOT FOUND", { status: 404 });
try {
if (rpath.startsWith("site")) {
if (rpath === "site-html") {
res = new Response(generateIndexHtml("[[base_url]]", "[[site_id]]"));
}
if (rpath === "site-zip") {
const path = dir(`app/static/site.zip`);
res = new Response(Bun.file(path));
}
if (rpath === "site-md5") {
const path = dir(`app/static/md5`);
res = new Response(Bun.file(path));
}
} else if (rpath.startsWith("current-")) {
if (rpath.startsWith("current-md5-")) {
const site_id = rpath.substring("current-md5-".length);
const path = dir(`app/web/${site_id}/current`);
res = new Response(Bun.file(path));
} else {
const site_id = rpath.substring("current-".length);
const path = dir(`app/web/${site_id}/current`);
const id = await Bun.file(path).text();
if (id) {
const path = dir(`app/web/${site_id}/deploys/${id}`);
res = new Response(Bun.file(path));
}
}
}
} catch (e) {
res = new Response("NOT FOUND", { status: 404 });
}
const path = dir(`${g.datadir}/upload/${rpath}`);
const file = Bun.file(path);
if (await file.exists()) {
res = new Response(file);
} else {
res = new Response("NOT FOUND", { status: 404 });
}
const arr = path.split("-");
const ext = arr.pop();
const fname = arr.join("-") + "." + ext;
const ctype = mime.getType(fname);
if (ctype) {
res.headers.set("content-type", ctype);
}
res.headers.set("Access-Control-Allow-Origin", "*");
res.headers.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT");
res.headers.set("Access-Control-Allow-Headers", "content-type");
res.headers.set("Access-Control-Allow-Credentials", "true");
return res;
},
};

95
pkgs/api/_notif.ts Normal file
View File

@ -0,0 +1,95 @@
import { Database } from "bun:sqlite";
import admin from "firebase-admin";
import { apiContext } from "service-srv";
import { dir } from "utils/dir";
import { g } from "utils/global";
export const _ = {
url: "/_notif/:action/:token",
async api(
action: string,
data:
| { type: "register"; token: string; id: string }
| { type: "send"; id: string; body: string; title: string; data?: any }
) {
const { req } = apiContext(this);
if (!g.firebaseInit) {
g.firebaseInit = true;
try {
g.firebase = admin.initializeApp({
credential: admin.credential.cert(dir("firebase-admin.json")),
});
g.notif = {
db: new Database(dir(`${g.datadir}/notif.sqlite`)),
};
g.notif.db.exec(`
CREATE TABLE IF NOT EXISTS notif (
token TEXT PRIMARY KEY,
id TEXT NOT NULL
);
`);
} catch (e) {
console.error(e);
}
}
if (g.firebase) {
switch (action) {
case "register":
{
if (data && data.type === "register" && data.id) {
if (data.token) {
const q = g.notif.db.query(
`SELECT * FROM notif WHERE token = '${data.token}'`
);
const result = q.all();
if (result.length > 0) {
g.notif.db.exec(
`UPDATE notif SET id = '${data.id}' WHERE token = '${data.token}'`
);
} else {
g.notif.db.exec(
`INSERT INTO notif VALUES ('${data.token}', '${data.id}')`
);
}
return { result: "OK" };
} else {
return { error: "missing token" };
}
}
}
break;
case "send":
{
if (data && data.type === "send") {
const q = g.notif.db.query<{ token: string }, any>(
`SELECT * FROM notif WHERE id = '${data.id}'`
);
let result = q.all();
for (const c of result) {
try {
await g.firebase.messaging().send({
notification: { body: data.body, title: data.title },
data: data.data,
token: c.token,
});
} catch (e) {
console.error(e);
result = result.filter((v) => v.token !== c.token);
}
}
return { result: "OK", totalDevice: result.length };
}
}
break;
}
}
return { error: "missing ./firebase-admin.json" };
},
};

143
pkgs/api/_prasi.ts Normal file
View File

@ -0,0 +1,143 @@
import { readAsync } from "fs-jetpack";
import { apiContext } from "service-srv";
import { g } from "utils/global";
import { dir } from "utils/dir";
const cache = {
dev: "",
prod: "",
};
export const _ = {
url: "/_prasi/**",
async api() {
const { req, res } = apiContext(this);
res.setHeader("Access-Control-Allow-Origin", "*");
const action = {
_: () => {
res.send({ prasi: "v2" });
},
"load.json": async () => {
res.setHeader("content-type", "application/json");
res.send(
JSON.stringify({
apiEntry: getApiEntry(),
apiTypes: (await getApiTypes()) || "",
prismaTypes: {
"prisma.d.ts": await getPrisma("prisma"),
"runtime/index.d.ts": await getPrisma("runtime"),
"runtime/library.d.ts": await getPrisma("library"),
},
})
);
},
"load.js": async () => {
res.setHeader("content-type", "text/javascript");
const url = req.query_parameters["url"]
? JSON.stringify(req.query_parameters["url"])
: "undefined";
if (!cache.dev) {
cache.dev = `\
(() => {
const baseurl = new URL(location.href);
baseurl.pathname = '';
const url = ${url} || baseurl.toString();
const w = window;
if (!w.prasiApi) {
w.prasiApi = {};
}
w.prasiApi[url] = {
apiEntry: ${JSON.stringify(getApiEntry())},
apiTypes: ${JSON.stringify((await getApiTypes()) || "")},
prismaTypes: {
"prisma.d.ts": ${await getPrisma("prisma")},
"runtime/index.d.ts": ${await getPrisma("runtime")},
"runtime/library.d.ts": ${await getPrisma("library")},
},
};
})();`;
cache.prod = `\
(() => {
const baseurl = new URL(location.href);
baseurl.pathname = '';
const url = ${url} || baseurl.toString();
const w = window;
if (!w.prasiApi) {
w.prasiApi = {};
}
w.prasiApi[url] = {
apiEntry: ${JSON.stringify(getApiEntry())},
};
})();`;
}
if (req.query_parameters["dev"]) {
res.send(cache.dev);
} else {
res.send(cache.prod);
}
},
};
const pathname: keyof typeof action = req.params._.split("/")[0] as any;
const run = action[pathname];
if (run) {
await run();
}
},
};
export const getApiEntry = () => {
const res: any = {};
for (const [k, v] of Object.entries(g.api)) {
const name = k.substring(0, k.length - 3);
res[name] = { ...v, name, path: `app/srv/api/${v.path}` };
}
return res;
};
const getApiTypes = async () => {
return (
`\
declare module "gen/srv/api/entry" {
export * as srv from "gen/srv/api/srv";
}
` +
((await readAsync(dir("app/srv/exports.d.ts"))) || "")
.replace(/\"app\/srv\/api/gi, '"srv/api')
.replace(
'declare module "app/srv/exports"',
'declare module "gen/srv/api/srv"'
)
);
};
const getPrisma = async (path: string) => {
if (path === "prisma")
return JSON.stringify(
(
(await readAsync(dir("node_modules/.prisma/client/index.d.ts"))) || ""
).replace(`@prisma/client/runtime/library`, `./runtime/library`)
);
if (path === "runtime")
return JSON.stringify(
await readAsync(
dir("node_modules/@prisma/client/runtime/index-browser.d.ts")
)
);
if (path === "library")
return JSON.stringify(
await readAsync(dir("node_modules/@prisma/client/runtime/library.d.ts"))
);
return JSON.stringify({});
};

41
pkgs/api/_proxy.ts Normal file
View File

@ -0,0 +1,41 @@
import { g } from "utils/global";
import { gzipAsync } from "utils/gzip";
export const _ = {
url: "/_proxy/*",
async api(arg: {
url: string;
method: "POST" | "GET";
headers: any;
body: any;
}) {
const res = await fetch(
arg.url,
arg.body
? {
method: arg.method || "POST",
headers: arg.headers,
body: arg.body,
}
: {
headers: arg.headers,
}
);
let body: any = null;
const headers: any = {};
res.headers.forEach((v, k) => {
headers[k] = v;
});
body = await res.arrayBuffer();
if (headers["content-encoding"] === "gzip") {
body = await gzipAsync(new Uint8Array(body));
} else {
delete headers["content-encoding"];
}
return new Response(body, { headers });
},
};

36
pkgs/api/_upload.ts Normal file
View File

@ -0,0 +1,36 @@
import mp from "@surfy/multipart-parser";
import { writeAsync } from "fs-jetpack";
import { apiContext } from "service-srv";
import { dir } from "utils/dir";
import { g } from "utils/global";
export const _ = {
url: "/_upload",
async api(body: any) {
const { req } = apiContext(this);
let url = "";
const raw = await req.arrayBuffer();
const parts = mp(Buffer.from(raw)) as Record<
string,
{ fileName: string; mime: string; type: string; buffer: Buffer }
>;
for (const [_, part] of Object.entries(parts)) {
const d = new Date();
const path = `${d.getFullYear()}-${d.getMonth()}/${d.getDate()}/${d.getTime()}-${part.fileName
?.replace(/[\W_]+/g, "-")
.toLowerCase()}`;
url = `/_file/${path}`;
await writeAsync(dir(`${g.datadir}/upload/${path}`), part.buffer);
}
return url;
},
};
function toArrayBuffer(buffer: Buffer) {
return buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
);
}

79
pkgs/api/_web.ts Normal file
View File

@ -0,0 +1,79 @@
import mime from "mime";
import { apiContext } from "service-srv";
import { g } from "utils/global";
import { getApiEntry } from "./_prasi";
export const _ = {
url: "/_web/:id/**",
async api(id: string, _: string) {
const { req, res } = apiContext(this);
const web = g.web[id];
if (web) {
const cache = web.cache;
if (cache) {
const parts = _.split("/");
switch (parts[0]) {
case "site": {
res.setHeader("content-type", "application/json");
if (req.query_parameters["prod"]) {
return {
site: cache.site,
pages: cache.pages.map((e) => {
return {
id: e.id,
url: e.url,
};
}),
api: getApiEntry(),
};
} else {
return cache.site;
}
}
case "pages": {
res.setHeader("content-type", "application/json");
return cache.pages.map((e) => {
return {
id: e.id,
url: e.url,
};
});
}
case "page": {
res.setHeader("content-type", "application/json");
return cache.pages.find((e) => e.id === parts[1]);
}
case "npm-site": {
let path = parts.slice(1).join("/");
res.setHeader("content-type", mime.getType(path) || "text/plain");
if (path === "site.js") {
path = "index.js";
}
return cache.npm.site[path];
}
case "npm-page": {
const page_id = parts[1];
if (cache.npm.pages[page_id]) {
let path = parts.slice(2).join("/");
res.setHeader("content-type", mime.getType(path) || "text/plain");
if (path === "page.js") {
path = "index.js";
}
return cache.npm.pages[page_id][path];
}
res.setHeader("content-type", "text/javascript");
}
case "comp": {
res.setHeader("content-type", "application/json");
return cache.comps.find((e) => e.id === parts[1]);
}
}
}
}
return req.params;
},
};

35
pkgs/index.ts Normal file
View File

@ -0,0 +1,35 @@
import { generateAPIFrm } from "./server/api-frm";
import { createServer } from "./server/create";
import { prepareAPITypes } from "./server/prep-api-ts";
import { config } from "./utils/config";
import { g } from "./utils/global";
import { createLogger } from "./utils/logger";
import { loadWeb } from "./server/load-web";
import { ensureNotRunning } from "utils/ensure";
import { preparePrisma } from "utils/prisma";
import { startDevWatcher } from "utils/dev-watcher";
g.mode = process.argv.includes("dev") ? "dev" : "prod";
g.datadir = g.mode === "prod" ? "../data" : ".data";
await preparePrisma();
await createLogger();
await ensureNotRunning();
if (g.db) {
await g.db.$connect();
}
await config.init();
await loadWeb();
g.log.info(g.mode === "dev" ? "DEVELOPMENT" : "PRODUCTION");
if (g.mode === "dev") {
await startDevWatcher();
}
await createServer();
await generateAPIFrm();
await prepareAPITypes();

19
pkgs/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "pkgs",
"dependencies": {
"@surfy/multipart-parser": "^1.0.2",
"@swc/core": "^1.3.91",
"@types/mime": "^3.0.2",
"@types/unzipper": "^0.10.7",
"execa": "^8.0.1",
"fs-jetpack": "^5.1.0",
"lmdb": "^2.8.5",
"mime": "^3.0.0",
"pino": "^8.15.3",
"pino-pretty": "^10.2.0",
"radash": "^11.0.0",
"radix3": "^1.1.0",
"typescript": "^5.2.2",
"unzipper": "^0.10.14"
}
}

62
pkgs/server/api-ctx.ts Normal file
View File

@ -0,0 +1,62 @@
import { g } from "../utils/global";
const parseQueryParams = (ctx: any) => {
const pageHref = ctx.req.url;
const searchParams = new URLSearchParams(
pageHref.substring(pageHref.indexOf("?"))
);
const result: any = {};
searchParams.forEach((v, k) => {
result[k] = v;
});
return result as any;
};
export const apiContext = (ctx: any) => {
ctx.req.params = ctx.params;
ctx.req.query_parameters = parseQueryParams(ctx);
return {
req: ctx.req as Request & { params: any; query_parameters: any },
res: {
...ctx.res,
send: (body) => {
ctx.res = createResponse(ctx.res, body);
},
sendStatus: (code: number) => {
ctx.res._status = code;
},
setHeader: (key: string, value: string) => {
ctx.res.headers.append(key, value);
},
} as Response & {
send: (body?: string | object) => void;
setHeader: (key: string, value: string) => void;
sendStatus: (code: number) => void;
},
};
};
export const createResponse = (existingRes: any, body: any) => {
const status =
typeof existingRes._status === "number" ? existingRes._status : undefined;
let res = new Response(
typeof body === "string" ? body : JSON.stringify(body),
status
? {
status,
}
: undefined
);
if (typeof body === "object") {
res.headers.append("content-type", "application/json");
}
const cur = existingRes as Response;
for (const [key, value] of cur.headers.entries()) {
res.headers.append(key, value);
}
return res;
};

70
pkgs/server/api-frm.ts Normal file
View File

@ -0,0 +1,70 @@
import { transform } from "@swc/core";
import { g } from "../utils/global";
import { createHash } from "crypto";
export const generateAPIFrm = async () => {
const res = await transform(
`
(BigInt.prototype).toJSON = function () {
return "BigInt::" + this.toString();
};
const replacer = (key, value) => {
if (typeof value === "string" && value.startsWith('BigInt::')) {
return BigInt(value.substr(8));
}
return value;
}
window.addEventListener('message', (e) => {
const msg = e.data;
const init = Object.assign({}, msg.init)
let input = msg.input;
let url = msg.input;
if (typeof msg.input === 'string') {
if (!input.startsWith('http')) {
url = new URL(\`\$\{location.origin\}\$\{input\}\`)
} else {
url = new URL(input)
}
}
if (init && init.body && typeof init.body === 'object') {
if (Array.isArray(init.body)) {
const body = new FormData();
body.append("file", init.body[0]);
init.body = body;
}
}
fetch(url.pathname, init)
.then(async (res) => {
if (res) {
const body = await res.text();
if (res.ok) {
try {
parent.postMessage({result: JSON.parse(body, replacer), id: msg.id }, '*')
} catch(e) {
parent.postMessage({result: body, id: msg.id }, '*')
}
} else {
try {
parent.postMessage({error: JSON.parse(body, replacer), id: msg.id }, '*')
} catch(e) {
parent.postMessage({error: body, id: msg.id }, '*')
}
}
}
})
})
parent.postMessage('initialized', '*')`,
{ minify: true }
);
g.frm = {
js: res.code,
etag: createHash("md5").update(res.code).digest("hex"),
};
};

121
pkgs/server/create.ts Normal file
View File

@ -0,0 +1,121 @@
import { inspectAsync, listAsync } from "fs-jetpack";
import { join } from "path";
import { createRouter } from "radix3";
import { g } from "../utils/global";
import { parseArgs } from "./parse-args";
import { serveAPI } from "./serve-api";
import { serveWeb } from "./serve-web";
import { dir } from "../utils/dir";
import { file } from "bun";
import { trim } from "radash";
export const createServer = async () => {
g.router = createRouter({ strictTrailingSlash: true });
g.api = {};
const scan = async (path: string, root?: string) => {
const apis = await listAsync(path);
if (apis) {
for (const filename of apis) {
const importPath = join(path, filename);
if (filename.endsWith(".ts")) {
try {
const api = await import(importPath);
let args: string[] = await parseArgs(importPath);
const route = {
url: api._.url,
args,
fn: api._.api,
path: importPath.substring((root || path).length + 1),
};
g.api[filename] = route;
g.router.insert(route.url, g.api[filename]);
} catch (e) {
g.log.warn(
`Failed to import app/srv/api${importPath.substring(
(root || path).length
)}`
);
const f = file(importPath);
if (f.size > 0) {
console.error(e);
} else {
g.log.warn(` ➨ file is empty`);
}
}
} else {
const dir = await inspectAsync(importPath);
if (dir?.type === "dir") {
await scan(importPath, path);
}
}
}
}
};
await scan(dir(`app/srv/api`));
await scan(dir(`pkgs/api`));
g.server = Bun.serve({
port: g.port,
async fetch(req) {
const url = new URL(req.url);
const web = await serveWeb(url, req);
let index = ["", ""];
if (web) {
if (Array.isArray(web)) index = web;
else {
return web;
}
}
const api = await serveAPI(url, req);
if (api) {
return api;
}
if (index) {
let status: any = {};
if (!["", "index.html"].includes(trim(url.pathname, " /"))) {
status = {
status: 404,
statusText: "Not Found",
};
}
const [site_id, body] = index;
if (g.web[site_id]) {
const router = g.web[site_id].router;
if (router) {
let found = router.lookup(url.pathname);
if (!found) {
found = router.lookup(url.pathname + "/");
}
if (found) {
status = {};
}
}
}
return new Response(body, {
...status,
headers: {
"content-type": "text/html",
},
});
}
return new Response(`404 Not Found`, {
status: 404,
statusText: "Not Found",
});
},
});
if (process.env.PRASI_MODE === "dev") {
g.log.info(`http://localhost:${g.server.port}`);
} else {
g.log.info(`Started at port: ${g.server.port}`);
}
};

129
pkgs/server/load-web.ts Normal file
View File

@ -0,0 +1,129 @@
import { file } from "bun";
import { $ } from "execa";
import {
dirAsync,
existsAsync,
inspectTreeAsync,
readAsync,
removeAsync,
writeAsync,
} from "fs-jetpack";
import { createRouter } from "radix3";
import { gunzipSync } from "zlib";
import { downloadFile } from "../api/_deploy";
import { dir } from "../utils/dir";
import { g } from "../utils/global";
export const loadWeb = async () => {
g.web = {};
await dirAsync(dir(`app/static`));
const siteZip = `${
g.mode === "dev" ? "http://localhost:4550" : "https://prasi.app"
}/site-bundle`;
const zipPath = dir(`app/static/site.zip`);
const md5Path = dir(`app/static/md5`);
if (!(await file(zipPath).exists()) || !(await file(md5Path).exists())) {
const md5 = await fetch(`${siteZip}/md5`);
await writeAsync(md5Path, await md5.text());
await new Promise<void>((r) => setTimeout(r, 1000));
await downloadFile(`${siteZip}/download`, zipPath);
await removeAsync(dir(`app/static/site`));
await $({ cwd: dir(`app/static`) })`unzip site.zip`;
} else {
const md5 = await fetch(`${siteZip}/md5`);
const md5txt = await md5.text();
if (md5txt !== (await readAsync(md5Path))) {
const e = await fetch(`${siteZip}/download`);
await removeAsync(dir(`app/static`));
await dirAsync(dir(`app/static`));
await downloadFile(`${siteZip}/download`, zipPath);
await writeAsync(md5Path, md5txt);
await $({ cwd: dir(`app/static`) })`unzip site.zip`;
}
}
const list = await inspectTreeAsync(dir(`app/web`));
for (const web of list?.children || []) {
if (web.type === "file") continue;
const deploy = web.children?.find((e) => e.name === "deploys");
if (!deploy) {
await dirAsync(dir(`app/web/${web.name}/deploys`));
}
g.web[web.name] = {
current: parseInt(
(await readAsync(dir(`app/web/${web.name}/current`))) || "0"
),
deploys: deploy ? deploy.children.map((e) => parseInt(e.name)) : [],
domains:
(await readAsync(dir(`app/web/${web.name}/domains.json`), "json")) ||
[],
site_id: web.name,
deploying: null,
cacheKey: 0,
router: null,
cache: null,
};
const cur = g.web[web.name];
if (!cur.deploys.includes(cur.current)) {
cur.current = 0;
}
if (cur.current) {
await loadWebCache(cur.site_id, cur.current);
}
}
};
const decoder = new TextDecoder();
export const loadWebCache = async (site_id: string, ts: number | string) => {
const web = g.web[site_id];
if (web) {
const path = dir(`app/web/${site_id}/deploys/${ts}`);
if (await existsAsync(path)) {
const fileContent = await readAsync(path, "buffer");
if (fileContent) {
console.log(
`Loading site ${site_id}: ${humanFileSize(fileContent.byteLength)}`
);
const res = gunzipSync(fileContent);
web.cache = JSON.parse(decoder.decode(res));
web.router = createRouter();
for (const p of web.cache?.pages || []) {
web.router.insert(p.url, p);
}
}
}
}
};
function humanFileSize(bytes: any, si = false, dp = 1) {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + " B";
}
const units = si
? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
: ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
let u = -1;
const r = 10 ** dp;
do {
bytes /= thresh;
++u;
} while (
Math.round(Math.abs(bytes) * r) / r >= thresh &&
u < units.length - 1
);
return bytes.toFixed(dp) + " " + units[u];
}

36
pkgs/server/parse-args.ts Normal file
View File

@ -0,0 +1,36 @@
import { parseFile } from "@swc/core";
import { readAsync } from "fs-jetpack";
export const parseArgs = async (path: string) => {
const res = await parseFile(path, { syntax: "typescript" });
const args: string[] = [];
for (const e of res.body) {
if (
e.type === "ExportDeclaration" &&
e.declaration.type === "VariableDeclaration"
) {
const declare = e.declaration.declarations[0];
if (
declare &&
declare.type === "VariableDeclarator" &&
declare.id.type === "Identifier" &&
declare.id.value === "_" &&
declare.init?.type === "ObjectExpression"
) {
for (const prop of declare.init.properties) {
if (prop.type === "MethodProperty") {
for (const param of prop.params) {
if (
param.type === "Parameter" &&
param.pat.type === "Identifier"
) {
args.push(param.pat.value);
}
}
}
}
}
}
}
return args;
};

View File

@ -0,0 +1,55 @@
import { spawnSync } from "bun";
import { existsAsync, readAsync } from "fs-jetpack";
import { dir } from "../utils/dir";
import { g } from "../utils/global";
export const prepareAPITypes = async () => {
const out: string[] = [];
for (const [k, v] of Object.entries(g.api)) {
const name = k.substring(0, k.length - 3).replace(/\W/gi, "_");
let p = {
path: `"app/srv/api/${v.path}"`,
handler: `"./api/${v.path.substring(0, v.path.length - 3)}"`,
};
if (!(await existsAsync(dir(p.path)))) {
p.path = `"pkgs/api/${v.path}"`;
p.handler = `"../../pkgs/api/${v.path.substring(0, v.path.length - 3)}"`;
}
out.push(`\
export const ${name} = {
name: "${name}",
url: "${v.url}",
path: "app/srv/api/${v.path}",
args: ${JSON.stringify(v.args)},
handler: import(${p.handler})
}`);
}
await Bun.write(dir(`app/srv/exports.ts`), out.join(`\n`));
const targetFile = dir("app/srv/exports.d.ts");
spawnSync(
[
dir("node_modules/.bin/tsc"),
dir("app/srv/exports.ts"),
"--declaration",
"--emitDeclarationOnly",
"--outFile",
targetFile,
],
{
cwd: dir(`node_modules/.bin`),
}
);
let res = await readAsync(targetFile);
if (res) {
res = res.replace('export * from "@prisma/client";', "");
res = res.replace("server: Server;", "");
res = res.replace(`import { PrismaClient } from "app/db/db";`, "");
res = res.replace(`db: PrismaClient;`, "");
await Bun.write(targetFile, res);
}
};

71
pkgs/server/serve-api.ts Normal file
View File

@ -0,0 +1,71 @@
import { createResponse } from "service-srv";
import { g } from "../utils/global";
export const serveAPI = async (url: URL, req: Request) => {
let found = g.router.lookup(url.pathname);
if (!found?.url) {
if (!url.pathname.endsWith("/")) {
found = g.router.lookup(url.pathname + "/");
}
if (!found?.url) {
found = null;
}
}
if (found) {
const params = { ...found.params };
let args = found.args.map((e) => {
return params[e];
});
if (req.method !== "GET") {
if (!req.headers.get("content-type")?.startsWith("multipart/form-data")) {
try {
const json = await req.json();
if (typeof json === "object") {
if (Array.isArray(json)) {
args = json;
for (let i = 0; i < json.length; i++) {
const val = json[i];
if (found.args[i]) {
params[found.args[i]] = val;
}
}
} else {
for (const [k, v] of Object.entries(json)) {
params[k] = v;
}
for (const [k, v] of Object.entries(params)) {
const idx = found.args.findIndex((arg) => arg === k);
if (idx >= 0) {
args[idx] = v;
}
}
}
}
} catch (e) {}
}
}
const current = {
req,
res: new Response(),
...found,
params,
};
const finalResponse = await current.fn(...args);
if (finalResponse instanceof Response) {
return finalResponse;
}
if (finalResponse) {
return createResponse(current.res, finalResponse);
}
return current.res;
}
};

104
pkgs/server/serve-web.ts Normal file
View File

@ -0,0 +1,104 @@
import { statSync } from "fs";
import { join } from "path";
import { g } from "../utils/global";
import { dir } from "utils/dir";
const index = {
html: "",
css: {
src: null as any,
encoding: "",
},
isFile: {} as Record<string, boolean>,
};
export const serveWeb = async (url: URL, req: Request) => {
const domain = url.hostname;
let site_id = "";
if (!g.domains) {
g.domains = {};
for (const web of Object.values(g.web)) {
for (const d of web.domains) {
const durl = new URL(d);
g.domains[durl.hostname] = web.site_id;
}
}
}
if (typeof g.domains[domain] === "undefined") {
g.domains[domain] = "";
}
site_id = g.domains[domain];
if (!site_id) {
return false;
}
const base = dir(`app/static/site`);
// const base = `/Users/r/Developer/prasi/.output/app/srv/site`;
let path = join(base, url.pathname);
if (url.pathname === "/site_id") {
return new Response(site_id);
}
if (url.pathname.startsWith("/index.css")) {
if (!index.css.src) {
const res = await fetch("https://prasi.app/index.css");
index.css.src = await res.arrayBuffer();
index.css.encoding = res.headers.get("content-encoding") || "";
}
return new Response(index.css.src, {
headers: {
"content-type": "text/css",
"content-encoding": index.css.encoding,
},
});
}
try {
if (typeof index.isFile[path] === "undefined") {
const s = statSync(path);
if (s.isFile()) {
index.isFile[path] = true;
return new Response(Bun.file(path));
}
} else if (index.isFile[path]) {
return new Response(Bun.file(path));
}
} catch (e) {}
if (!index.html) {
index.html = generateIndexHtml("", site_id);
}
return [site_id, index.html];
};
export const generateIndexHtml = (base_url: string, site_id: string) => {
const base = base_url.endsWith("/")
? base_url.substring(0, base_url.length - 1)
: base_url;
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<link rel="stylesheet" href="${base}/index.css?fresh">
</head>
<body class="flex-col flex-1 w-full min-h-screen flex opacity-0">
<div style="position:absolute;opacity:0.1;top:0;left:0">&nbsp;</div>
<div id="root"></div>
<script src="${base}/site.js" type="module"></script>
<script>window.id_site = "${site_id}";</script>
<script
src="https://js.sentry-cdn.com/a2dfb8b1128f4018b83bdc9c08d18da2.min.js"
crossorigin="anonymous"
></script>
</body>
</html>`;
};

31
pkgs/upgrade.ts Normal file
View File

@ -0,0 +1,31 @@
import { spawnSync } from "bun";
import { dirAsync, removeAsync } from "fs-jetpack";
import { dirname } from "path";
import unzipper from "unzipper";
import { dir } from "utils/dir";
const res = await fetch(
`https://github.com/avolut/prasi-api/archive/refs/heads/main.zip`,
{ method: "get" }
);
const data = await unzipper.Open.buffer(Buffer.from(await res.arrayBuffer()));
const promises: Promise<void>[] = [];
await removeAsync(dir("pkgs"));
for (const file of data.files) {
if (file.type === "File") {
const path = file.path.split("/").slice(1).join("/");
if (path === "tsconfig.json" || path.startsWith("pkgs")) {
promises.push(
new Promise<void>(async (done) => {
await dirAsync(dirname(dir(path)));
await Bun.write(dir(path), await file.buffer());
done();
})
);
}
}
}
await Promise.all(promises);
spawnSync({ cmd: ["bun", "install"], stdout: "inherit", stderr: "inherit" });

34
pkgs/utils/config.ts Normal file
View File

@ -0,0 +1,34 @@
import { dirAsync, readAsync } from "fs-jetpack";
import { dir } from "./dir";
import { g } from "./global";
const _internal = { config: {} as any, writeTimeout: null as any };
export const config = {
init: async () => {
await dirAsync(dir(`${g.datadir}/config`));
await dirAsync(dir(`${g.datadir}/files`));
_internal.config =
(await readAsync(dir(`${g.datadir}/config/conf.json`), "json")) || {};
},
get all() {
return _internal.config;
},
get(key: string) {
if (key.endsWith("url")) {
if (!(_internal.config[key] instanceof URL)) {
_internal.config[key] = new URL(_internal.config[key] || "");
}
}
return _internal.config[key];
},
set(key: string, value: any) {
_internal.config[key] = value;
clearTimeout(_internal.writeTimeout);
_internal.writeTimeout = setTimeout(() => {
Bun.write(dir(`${g.datadir}/config/conf.json`), _internal.config);
}, 100);
},
};

26
pkgs/utils/dev-watcher.ts Normal file
View File

@ -0,0 +1,26 @@
import { file } from "bun";
import { watch } from "fs";
import { dir } from "./dir";
import { dirAsync } from "fs-jetpack";
export const startDevWatcher = async () => {
await dirAsync(dir(`app/srv/api`));
watch(dir(`app/srv/api`), async (event, filename) => {
const s = file(dir(`app/srv/api/${filename}`));
if (s.size === 0) {
await Bun.write(
`app/srv/api/${filename}`,
`\
import { apiContext } from "service-srv";
export const _ = {
url: "/${filename?.substring(0, filename.length - 3)})}",
async api() {
const { req, res } = apiContext(this);
return "This is ${filename}";
}
}`
);
}
});
};

5
pkgs/utils/dir.ts Normal file
View File

@ -0,0 +1,5 @@
import { join } from "path";
export const dir = (path: string) => {
return join(process.cwd(), path);
};

37
pkgs/utils/ensure.ts Normal file
View File

@ -0,0 +1,37 @@
import { connect } from "bun";
import { g } from "./global";
export const ensureNotRunning = async () => {
await new Promise<void>(async (resolve) => {
const checkPort = () => {
return new Promise<boolean>(async (done) => {
try {
const s = await connect({
hostname: "0.0.0.0",
port: g.port,
socket: {
open(socket) {},
data(socket, data) {},
close(socket) {},
drain(socket) {},
error(socket, error) {},
},
});
s.end();
done(false);
} catch (e) {
done(true);
}
});
};
if (!(await checkPort())) {
g.log.warn(`Port ${process.env.PORT} is used, waiting...`);
setInterval(async () => {
if (await checkPort()) resolve();
}, 500);
} else {
resolve();
}
});
};

88
pkgs/utils/global.ts Normal file
View File

@ -0,0 +1,88 @@
import { Server } from "bun";
import { Logger } from "pino";
import { RadixRouter } from "radix3";
import { PrismaClient } from "../../app/db/db";
import admin from "firebase-admin";
import { Database } from "bun:sqlite";
type SingleRoute = {
url: string;
args: string[];
fn: (...arg: any[]) => Promise<any>;
path: string;
};
export const g = global as unknown as {
db: PrismaClient;
dburl: string;
datadir: string;
mode: "dev" | "prod";
server: Server;
log: Logger;
firebaseInit: boolean,
firebase: admin.app.App;
notif: {
db: Database;
};
api: Record<string, SingleRoute>;
domains: null | Record<string, string>;
web: Record<
string,
{
site_id: string;
current: number;
deploying: null | { status: string; received: number; total: number };
deploys: number[];
domains: string[];
router: null | RadixRouter<{ id: string }>;
cacheKey: number;
cache: null | {
site: {
id: string;
name: string;
favicon: string;
domain: string;
id_user: string;
created_at: Date | null;
id_org: string | null;
updated_at: Date | null;
responsive: string;
} | null;
pages: {
id: string;
name: string;
url: string;
content_tree: any;
id_site: string;
created_at: Date | null;
js_compiled: string | null;
js: string | null;
updated_at: Date | null;
id_folder: string | null;
is_deleted: boolean;
}[];
npm: {
site: Record<string, string>;
pages: Record<string, Record<string, string>>;
};
comps: {
id: string;
name: string;
content_tree: any;
created_at: Date | null;
updated_at: Date | null;
type: string;
id_component_group: string | null;
props: any;
}[];
};
}
>;
router: RadixRouter<SingleRoute>;
port: number;
frm: {
js: string;
etag: string;
};
};

25
pkgs/utils/gzip.ts Normal file
View File

@ -0,0 +1,25 @@
import { gzip, gunzip } from "zlib";
export const gzipAsync = (bin: Uint8Array) => {
return new Promise<Buffer>((resolve, reject) => {
gzip(bin, (err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
};
export const gunzipAsync = (bin: Uint8Array) => {
return new Promise<Buffer>((resolve, reject) => {
gunzip(bin, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
};

10
pkgs/utils/logger.ts Normal file
View File

@ -0,0 +1,10 @@
import { pino } from "pino";
import { g } from "./global";
export const createLogger = async () => {
g.log = pino({
transport: {
target: "pino-pretty",
},
});
};

17
pkgs/utils/prisma.ts Normal file
View File

@ -0,0 +1,17 @@
import { existsAsync } from "fs-jetpack";
import { dir } from "./dir";
import { $ } from "execa";
import { g } from "./global";
export const preparePrisma = async () => {
if (await existsAsync(dir("app/db/.env"))) {
if (!(await existsAsync(dir("node_modules/.prisma")))) {
await $({ cwd: dir(`app/db`) })`bun prisma generate`;
}
const { PrismaClient } = await import("../../app/db/db");
g.db = new PrismaClient();
}
g.dburl = process.env.DATABASE_URL || "";
g.port = parseInt(process.env.PORT || "3000");
};

43
pkgs/utils/query.ts Normal file
View File

@ -0,0 +1,43 @@
export type DBArg = {
db: string;
table: string;
action: string;
params: any[];
};
export const execQuery = async (args: DBArg, prisma: any) => {
const { table, action, params } = args;
const tableInstance = prisma[table];
if (tableInstance) {
if (action === "query" && table.startsWith("$query")) {
try {
const q = params.shift();
q.sql = true;
Object.freeze(q);
return await tableInstance.bind(prisma)(q, ...params);
} catch (e) {
console.log(e);
return e;
}
}
const method = tableInstance[action];
if (method) {
try {
const result = await method(...params);
if (!result) {
return JSON.stringify(result);
}
return result;
} catch (e: any) {
throw new Error(e.message);
}
}
}
};

10
pkgs/utils/restart.ts Normal file
View File

@ -0,0 +1,10 @@
import { $ } from "execa";
import { g } from "./global";
export const restartServer = () => {
if (g.mode === "dev") {
$`bun ${g.mode}`;
}
process.exit(0);
};

33
pkgs/utils/wait-until.ts Normal file
View File

@ -0,0 +1,33 @@
export const waitUntil = (
condition: number | (() => any),
timeout?: number
) => {
return new Promise<void>(async (resolve) => {
if (typeof condition === "function") {
let tout = null as any;
if (timeout) {
tout = setTimeout(resolve, timeout);
}
if (await condition()) {
clearTimeout(tout);
resolve();
return;
}
let count = 0;
const c = setInterval(async () => {
if (await condition()) {
if (tout) clearTimeout(tout);
clearInterval(c);
resolve();
}
if (count > 100) {
clearInterval(c);
}
}, 10);
} else if (typeof condition === "number") {
setTimeout(() => {
resolve();
}, condition);
}
});
};

32
tsconfig.json Normal file
View File

@ -0,0 +1,32 @@
{
"compilerOptions": {
"lib": [
"ESNext"
],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"paths": {
"service-srv": [
"./pkgs/server/api-ctx.ts"
],
"utils/*": [
"./pkgs/utils/*"
]
},
"types": [
"bun-types" // add Bun global
]
}
}