wip fix
This commit is contained in:
commit
1bd21cf6a7
|
|
@ -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/*
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "@prisma/client";
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "db",
|
||||
"dependencies": {
|
||||
"@prisma/client": "5.3.1",
|
||||
"prisma": "^5.3.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -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[]
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
|
@ -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")>;
|
||||
};
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
declare module "@surfy/multipart-parser";
|
||||
|
|
@ -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" }
|
||||
}
|
||||
|
|
@ -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>`);
|
||||
},
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
@ -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" };
|
||||
},
|
||||
};
|
||||
|
|
@ -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({});
|
||||
};
|
||||
|
|
@ -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 });
|
||||
},
|
||||
};
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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"),
|
||||
};
|
||||
};
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
};
|
||||
|
|
@ -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];
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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"> </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>`;
|
||||
};
|
||||
|
|
@ -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" });
|
||||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
@ -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}";
|
||||
}
|
||||
}`
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { join } from "path";
|
||||
|
||||
export const dir = (path: string) => {
|
||||
return join(process.cwd(), path);
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { pino } from "pino";
|
||||
import { g } from "./global";
|
||||
|
||||
export const createLogger = async () => {
|
||||
g.log = pino({
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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");
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { $ } from "execa";
|
||||
import { g } from "./global";
|
||||
|
||||
export const restartServer = () => {
|
||||
if (g.mode === "dev") {
|
||||
$`bun ${g.mode}`;
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue