├── .vscode └── settings.json ├── hurl ├── playground.hurl ├── login-post.hurl ├── sites-create01.hurl ├── users-create.hurl ├── sites-create02.hurl ├── sites-patch04.hurl ├── sites-create04.hurl └── sites-create03.hurl ├── src ├── types.ts ├── endpoints │ ├── sessions.ts │ ├── catchall.ts │ ├── ranges.ts │ ├── OLD-loginPost.ts │ ├── registration.ts │ ├── OLD-users.ts │ ├── SEED-sites.ts │ ├── RESTART-sites.ts │ └── sites.ts └── index.ts ├── worker-configuration.d.ts ├── README.md ├── uml ├── sites-read.puml ├── sites-write.puml ├── sequence.puml ├── SitesRetrieval.svg └── SitesPersistence.svg ├── package.json ├── tsconfig.json ├── wrangler.jsonc ├── .gitignore └── migrations └── 0001_schema_creation.sql /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "wrangler.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /hurl/playground.hurl: -------------------------------------------------------------------------------- 1 | GET https://scn-api-playground.hsinih.workers.dev/api/sites 2 | HTTP 200 3 | -------------------------------------------------------------------------------- /hurl/login-post.hurl: -------------------------------------------------------------------------------- 1 | POST https://scn-api-playground.hsinih.workers.dev/secure/login 2 | { 3 | "username": "test20@example.org", 4 | "password": "secret-placeholder-why-is-this-even-here" 5 | } 6 | -------------------------------------------------------------------------------- /hurl/sites-create01.hurl: -------------------------------------------------------------------------------- 1 | POST https://scn-api-playground.hsinih.workers.dev/api/sites 2 | { 3 | "id": "site-01", 4 | "name": "David-TCN", 5 | "latitude": 47.23911, 6 | "longitude": -122.44989, 7 | "status": "active", 8 | "address": "2309 S L St, Tacoma, WA 98405" 9 | } 10 | -------------------------------------------------------------------------------- /hurl/users-create.hurl: -------------------------------------------------------------------------------- 1 | POST https://scn-api-playground.hsinih.workers.dev/api/users 2 | { 3 | "email": "test20@example.org", 4 | "firstName": "testFirst", 5 | "lastName": "testLast", 6 | "registered": false, 7 | "isEnabled": true, 8 | "identity": "ABC-123-020" 9 | } 10 | -------------------------------------------------------------------------------- /hurl/sites-create02.hurl: -------------------------------------------------------------------------------- 1 | POST https://scn-api-playground.hsinih.workers.dev/api/sites 2 | { 3 | "id": "site-02", 4 | "name": "SURGEtacoma", 5 | "latitude": 47.23854, 6 | "longitude": -122.44094, 7 | "status": "confirmed", 8 | "address": "2367 Tacoma Ave S, Tacoma, WA 98402" 9 | } 10 | -------------------------------------------------------------------------------- /hurl/sites-patch04.hurl: -------------------------------------------------------------------------------- 1 | PUT https://scn-api-playground.hsinih.workers.dev/api/sites/4 2 | { 3 | "id": "site-04", 4 | "name": "Oareao OCC Masjid", 5 | "latitude": 47.52391, 6 | "longitude": -122.27636, 7 | "status": "patch-test", 8 | "address": "8812 Renton Ave S, Seattle, WA 98118" 9 | } 10 | -------------------------------------------------------------------------------- /hurl/sites-create04.hurl: -------------------------------------------------------------------------------- 1 | POST https://scn-api-playground.hsinih.workers.dev/api/sites 2 | { 3 | "id": "site-04", 4 | "name": "Oareao OCC Masjid", 5 | "latitude": 47.52391, 6 | "longitude": -122.27636, 7 | "status": "in-conversation", 8 | "address": "8812 Renton Ave S, Seattle, WA 98118" 9 | } 10 | -------------------------------------------------------------------------------- /hurl/sites-create03.hurl: -------------------------------------------------------------------------------- 1 | POST https://scn-api-playground.hsinih.workers.dev/api/sites 2 | { 3 | "id": "site-03", 4 | "name": "Filipino Community Center", 5 | "latitude": 47.5501, 6 | "longitude": -122.2872, 7 | "status": "confirmed", 8 | "address": "5740 Martin Luther King Jr Way S, Seattle, WA 98118" 9 | } 10 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { DateTime, Str } from "chanfana"; 2 | import type { Context } from "hono"; 3 | import { z } from "zod"; 4 | 5 | export type AppContext = Context<{ Bindings: Env }>; 6 | 7 | 8 | 9 | export const Task = z.object({ 10 | name: Str({ example: "lorem" }), 11 | slug: Str(), 12 | description: Str({ required: false }), 13 | completed: z.boolean().default(false), 14 | due_date: DateTime(), 15 | }); 16 | -------------------------------------------------------------------------------- /worker-configuration.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by Wrangler 2 | // After adding bindings to `wrangler.jsonc`, regenerate this interface via `npm run cf-typegen` 3 | 4 | 5 | 6 | declare namespace Cloudflare { 7 | interface Env { 8 | CORS_ORIGIN: ["http://localhost","http://localhost:3000","http://localhost:3002","https://patterns.github.io"]; 9 | DB: D1Database; 10 | } 11 | } 12 | interface Env extends Cloudflare.Env {} 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Workers OpenAPI 3.1 2 | 3 | [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https%3A%2F%2Fgithub.com%2Fpatterns%2Fcf-openapi-sandbox) 4 | 5 | This is a Cloudflare Worker with OpenAPI 3.1 using [chanfana](https://github.com/cloudflare/chanfana) and [Hono](https://github.com/honojs/hono). 6 | 7 | 8 | ## placeholder 9 | 1. CORS_ORIGIN is needed in the middleware to include the (fe) GitHub Pages base URL 10 | 11 | -------------------------------------------------------------------------------- /uml/sites-read.puml: -------------------------------------------------------------------------------- 1 | @startuml SitesRetrieval 2 | 3 | title "Sites (read)" 4 | 5 | skinparam monochrome true 6 | start 7 | -> GET /api/sites; 8 | 9 | :File Exist Check "sites.json"; 10 | if (exist?) then (yes) 11 | :Read file "sites.json"; 12 | else (no) 13 | :Create "sites.json" from copying "sites-default.json"; 14 | :Read file "sites-default.json"; 15 | endif 16 | 17 | if (success?) then (ok) 18 | :Convert bytes to text/string; 19 | :Parse into JSON obj; 20 | else (no) 21 | :Error 500; 22 | endif 23 | 24 | :Write Response; 25 | -> Close Stream; 26 | stop 27 | @enduml 28 | 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudflare-workers-openapi", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "npm run db:migrations:apply && wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "db:migrations:apply": "wrangler d1 migrations apply DB --remote", 10 | "cf-typegen": "wrangler types" 11 | }, 12 | "dependencies": { 13 | "chanfana": "^2.8.0", 14 | "hono": "^4.7.9", 15 | "zod": "^3.24.4" 16 | }, 17 | "devDependencies": { 18 | "@cloudflare/workers-types": "^4.20250506.0", 19 | "@types/node": "22.13.0", 20 | "@types/service-worker-mock": "^2.0.4", 21 | "wrangler": "^4.14.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/endpoints/sessions.ts: -------------------------------------------------------------------------------- 1 | import { CreateEndpoint } from 'chanfana'; 2 | import { z } from 'zod'; 3 | 4 | 5 | // with create, we want to accept free form JSON for now 6 | const CreateModel = z.object({ 7 | uuid: z.string().optional(), 8 | }).catchall(z.unknown()); 9 | 10 | const createMeta = { 11 | model: { 12 | schema: CreateModel, 13 | //// tableName: 'in-memory', 14 | }, 15 | }; 16 | 17 | export class SessionCreate extends CreateEndpoint { 18 | _meta = createMeta; 19 | 20 | async create(data: z.infer) { 21 | //TODO save to database here 22 | console.log("Session :", data); 23 | return { result: 'success' }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/endpoints/catchall.ts: -------------------------------------------------------------------------------- 1 | import { ListEndpoint, CreateEndpoint } from 'chanfana'; 2 | import { z } from 'zod'; 3 | 4 | 5 | // with create, we want to accept free form JSON for now 6 | const CreateModel = z.object({ 7 | uuid: z.string().optional(), 8 | }).catchall(z.unknown()); 9 | 10 | const createMeta = { 11 | model: { 12 | schema: CreateModel, 13 | //// tableName: 'in-memory', 14 | }, 15 | }; 16 | 17 | export class CatchallList extends ListEndpoint { _meta = createMeta; } 18 | export class CatchallCreate extends CreateEndpoint { 19 | _meta = createMeta; 20 | 21 | async create(data: z.infer) { 22 | //TODO save to database here 23 | console.log("Create :", data); 24 | return data; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /uml/sites-write.puml: -------------------------------------------------------------------------------- 1 | @startuml SitesPersistence 2 | 3 | title "Sites (write)" 4 | 5 | skinparam monochrome true 6 | start 7 | -> POST /secure/edit_sites; 8 | :Ensure Login middleware; 9 | if (hasLoginSession?) then (yes) 10 | :Access Body Property "sites"; 11 | if (valid?) then (ok) 12 | :Parse into JSON obj; 13 | if (valid?) then (ok) 14 | :Marshal JSON to text/string; 15 | :Write file "sites.json"; 16 | if (success?) then (ok) 17 | :Status 201; 18 | else (no) 19 | :Error 500; 20 | endif 21 | 22 | else (no) 23 | :Error 400; 24 | endif 25 | else (no) 26 | :Error 400; 27 | endif 28 | 29 | else (no) 30 | :Error 400; 31 | endif 32 | :Write Response; 33 | -> Close Stream; 34 | stop 35 | @enduml 36 | 37 | -------------------------------------------------------------------------------- /src/endpoints/ranges.ts: -------------------------------------------------------------------------------- 1 | import { ReadEndpoint } from 'chanfana'; 2 | import { z } from 'zod'; 3 | 4 | 5 | // Define the Range Model 6 | const RangeModel = z.object({ 7 | center: z.array(z.number()), 8 | minLat: z.number(), 9 | minLon: z.number(), 10 | maxLat: z.number(), 11 | maxLon: z.number(), 12 | }); 13 | 14 | // Define the Meta object for Range 15 | const rangeMeta = { 16 | model: { 17 | schema: RangeModel, 18 | primaryKeys: [], 19 | }, 20 | }; 21 | 22 | export class RangeList extends ReadEndpoint { 23 | _meta = rangeMeta; 24 | 25 | async fetch() { 26 | const cents = [ // simulate data 27 | 47.23911, -122.44989, 28 | ]; 29 | return { center: cents, minLat: 47.5001, minLon: -122.4382, maxLat: 47.734, maxLon: -122.2364}; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Base Options: */ 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "verbatimModuleSyntax": false, 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | /* Strictness */ 12 | "noImplicitAny": false, 13 | "noImplicitThis": true, 14 | "strictNullChecks": false, 15 | "strict": true, 16 | "noUncheckedIndexedAccess": true, 17 | /* If NOT transpiling with TypeScript: */ 18 | "moduleResolution": "Bundler", 19 | "module": "es2022", 20 | "noEmit": true, 21 | "lib": ["es2022"], 22 | "types": [ 23 | "@types/node", 24 | "@types/service-worker-mock", 25 | "@cloudflare/workers-types/2023-07-01" 26 | ] 27 | }, 28 | "exclude": ["node_modules", "dist", "tests"], 29 | "include": ["src", "worker-configuration.d.ts"] 30 | } 31 | -------------------------------------------------------------------------------- /uml/sequence.puml: -------------------------------------------------------------------------------- 1 | @startuml Admin Edit Sites 2 | hide footbox 3 | 4 | participant User 5 | participant "Vis" as FE << (W,#ADD1B2) Web >> order 1 6 | participant "Backend" as BE << (B,#FFB700) API >> order 2 7 | participant "Filesystem" as FS << (F,#FFB700) Disc >> order 3 8 | participant LDAP #white 9 | 10 | User -> FE : Nav to admin portal 11 | FE --> LDAP 12 | FE -> BE : POST /secure/get-users 13 | BE --> FE : List pending and registered 14 | User -> FE : Nav to edit sites 15 | FE -> BE : GET /api/sites (src/routes/query.ts) 16 | 17 | alt "File exists (sites.json)" 18 | else "Yes" 19 | 20 | BE -> FS: Read models/sites.json 21 | 22 | else "No" 23 | 24 | BE -[#red]> FS: Copy sites-default.json to models/sites.json 25 | BE -[#red]> FS: Read models/sites-default.json 26 | 27 | end 28 | 29 | FS --> BE 30 | BE --> FE : List sites 31 | User -> FE : Edit JSON and submit 32 | FE -> BE : POST /secure/edit_sites (src/routes/edit-sites.ts) 33 | 34 | @enduml 35 | -------------------------------------------------------------------------------- /src/endpoints/OLD-loginPost.ts: -------------------------------------------------------------------------------- 1 | import { Bool, OpenAPIRoute, Str } from "chanfana"; 2 | import { z } from "zod"; 3 | import { type AppContext } from "../types"; 4 | 5 | export class LoginPost extends OpenAPIRoute { 6 | schema = { 7 | tags: ["Login"], 8 | summary: "User Log in", 9 | request: { 10 | body: z.object({ 11 | username: Str({ description: "username" }), 12 | password: Str({ description: "password" }), 13 | }), 14 | }, 15 | responses: { 16 | "200": { 17 | description: "Returns if the login was ", 18 | content: { 19 | "application/json": { 20 | schema: z.object({ 21 | series: z.object({ 22 | success: Bool(), 23 | result: z.object({ 24 | data: Str(), 25 | }), 26 | }), 27 | }), 28 | }, 29 | }, 30 | }, 31 | }, 32 | }; 33 | 34 | async handle(c: AppContext) { 35 | // Get validated data 36 | const data = await this.getValidatedData(); 37 | 38 | // Retrieve the validated slug 39 | const { username, password } = data.body; 40 | 41 | // Implement your own object deletion here 42 | 43 | // Return the output for confirmation 44 | return { 45 | result: { 46 | data: "success", 47 | }, 48 | success: true, 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /wrangler.jsonc: -------------------------------------------------------------------------------- 1 | /** 2 | * For more details on how to configure Wrangler, refer to: 3 | * https://developers.cloudflare.com/workers/wrangler/configuration/ 4 | */ 5 | { 6 | "$schema": "node_modules/wrangler/config-schema.json", 7 | "name": "scn-api-playground", 8 | "main": "src/index.ts", 9 | "compatibility_date": "2025-05-06", 10 | 11 | 12 | 13 | /** 14 | * Smart Placement 15 | * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement 16 | */ 17 | // "placement": { "mode": "smart" }, 18 | 19 | /** 20 | * Bindings 21 | * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including 22 | * databases, object storage, AI inference, real-time communication and more. 23 | * https://developers.cloudflare.com/workers/runtime-apis/bindings/ 24 | */ 25 | 26 | /** 27 | * Environment Variables 28 | * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables 29 | */ 30 | "vars": { "CORS_ORIGIN": ["http://localhost", "http://localhost:3000", "http://localhost:3002", "https://patterns.github.io"] }, 31 | /** 32 | * Note: Use secrets to store sensitive data. 33 | * https://developers.cloudflare.com/workers/configuration/secrets/ 34 | */ 35 | 36 | /** 37 | * Static Assets 38 | * https://developers.cloudflare.com/workers/static-assets/binding/ 39 | */ 40 | // "assets": { "directory": "./public/", "binding": "ASSETS" }, 41 | 42 | /** 43 | * Service Bindings (communicate between multiple Workers) 44 | * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings 45 | */ 46 | // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] 47 | 48 | "d1_databases": [ 49 | { 50 | "binding": "DB", 51 | "database_name": "scn-api-playground", 52 | "database_id": "c01ef748-84c4-48d6-a5a8-b37ceb0f431f" 53 | } 54 | ], 55 | "observability": { 56 | "enabled": true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/endpoints/registration.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIRoute, CreateEndpoint } from 'chanfana'; 2 | import { z } from 'zod'; 3 | import { type Context } from 'hono'; 4 | 5 | const RegisterModel = z.object({ 6 | publicKey: z.string(), 7 | message: z.string(), 8 | sigMessage: z.string(), 9 | }); 10 | const registerMeta = { 11 | model: { 12 | schema: RegisterModel, 13 | primaryKeys: ['publicKey'], 14 | tableName: 'registration', 15 | }, 16 | }; 17 | // we "mash" the user list together 18 | const UserModel = z.object({ 19 | identity: z.string(), 20 | email: z.string().email(), 21 | firstName: z.string(), 22 | lastName: z.string(), 23 | registered: z.boolean(), 24 | issueDate: z.string().datetime(), 25 | isEnabled: z.boolean(), 26 | publicKey: z.string(), 27 | qrCode: z.string(), 28 | lastOnline: z.string(), 29 | }); 30 | const MashModel = z.object({ 31 | pending: z.array(UserModel), 32 | registered: z.array(UserModel), 33 | }); 34 | const mashMeta = { 35 | model: { 36 | schema: MashModel, 37 | 38 | }, 39 | }; 40 | 41 | export class RegisterCreate extends CreateEndpoint { 42 | _meta = registerMeta; 43 | 44 | async create(data: z.infer) { 45 | //TODO save to database here 46 | console.log("Creating registration:", data); 47 | return data; 48 | } 49 | } 50 | 51 | export class PendingRegisteredMash extends OpenAPIRoute { 52 | schema = { 53 | responses: MashModel, 54 | }; 55 | 56 | async handle(c: Context) { 57 | // simulate records 58 | const pends = [{ identity: 'id-1', email: 'kermit@sesame.street', firstName: 'Kermit', lastName: 'The-Frog', registered: false, issueDate: '', isEnabled: true, publicKey: 'abcxyz', qrCode: '', lastOnline: '' }]; 59 | const regs = [{ identity: 'id-9', email: 'misspiggy@sesame.street', firstName: 'Miss', lastName: 'Piggy', registered: true, issueDate: '', isEnabled: true, publicKey: 'xyz123', qrCode: '', lastOnline: '' }]; 60 | 61 | return { pending: pends, registered: regs }; 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/endpoints/OLD-users.ts: -------------------------------------------------------------------------------- 1 | 2 | import { D1CreateEndpoint, D1ReadEndpoint, D1ListEndpoint } from "chanfana"; 3 | import { z } from "zod"; 4 | 5 | 6 | // Define the User Model 7 | const UserModel = z.object({ 8 | identity: z.string(), 9 | email: z.string().email(), 10 | firstName: z.string(), 11 | lastName: z.string(), 12 | registered: z.boolean(), 13 | issueDate: z.string().datetime(), 14 | isEnabled: z.boolean(), 15 | publicKey: z.string(), 16 | qrCode: z.string(), 17 | lastOnline: z.string(), 18 | }); 19 | 20 | // Define the Meta object for User 21 | const userMeta = { 22 | model: { 23 | schema: UserModel.omit({id: true, created: true, rawdata: true}), 24 | primaryKeys: ['id'], 25 | tableName: 'users', // Table name in D1 database 26 | }, 27 | }; 28 | 29 | export class UserFetch extends D1ReadEndpoint { _meta = userMeta; dbName = "DB"; } 30 | export class UserList extends D1ListEndpoint { _meta = userMeta; dbName = "DB"; } 31 | 32 | // with create, we want to accept free form JSON for now (require "email" as only rule) 33 | const CreateModel = z.object({ 34 | email: z.string().email(), 35 | }).catchall(z.unknown()); 36 | const createMeta = { 37 | model: { 38 | schema: CreateModel, 39 | tableName: 'users', 40 | }, 41 | }; 42 | export class UserCreate extends D1CreateEndpoint { 43 | _meta = createMeta; 44 | dbName = "DB"; 45 | 46 | async create(data: z.infer) { 47 | let inserted; 48 | let serialized; 49 | try { 50 | serialized = JSON.stringify(data) 51 | } catch (e: any) { 52 | // capture exception when stringify encounters BigInt/circular 53 | serialized = JSON.stringify(e, Object.getOwnPropertyNames(e)) 54 | } 55 | try { 56 | const result = await this.getDBBinding() 57 | .prepare( 58 | `INSERT INTO ${this.meta.model.tableName} (rawdata) VALUES (?) RETURNING *`, 59 | ) 60 | .bind(serialized) 61 | .all(); 62 | 63 | inserted = result.results[0] as O; 64 | } catch (e: any) { 65 | if (e.message.includes("UNIQUE constraint failed")) { 66 | const constraintMessage = e.message.split("UNIQUE constraint failed:")[1].split(":")[0].trim(); 67 | if (this.constraintsMessages[constraintMessage]) { 68 | throw this.constraintsMessages[constraintMessage]; 69 | } 70 | } 71 | 72 | throw new ApiException(e.message); 73 | } 74 | return inserted; 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /src/endpoints/SEED-sites.ts: -------------------------------------------------------------------------------- 1 | import { ListEndpoint } from 'chanfana'; 2 | import { z } from 'zod'; 3 | 4 | 5 | // Define the Site Model 6 | const SiteModel = z.object({ 7 | name: z.string(), 8 | latitude: z.number(), 9 | longitude: z.number(), 10 | status: z.string(), 11 | address: z.string(), 12 | cell_id: z.array(z.string()).optional(), 13 | }); 14 | 15 | // Define the Meta object for Site 16 | const siteMeta = { 17 | model: { 18 | schema: SiteModel.omit({id: true, created: true, rawdata: true}), 19 | primaryKeys: ['id'], 20 | tableName: 'sites', // Table name in D1 database 21 | }, 22 | }; 23 | 24 | export class SiteList extends ListEndpoint { 25 | _meta = siteMeta; 26 | filterFields = ['status']; 27 | orderByFields = ['name', 'status']; 28 | defaultOrderBy = 'name'; 29 | 30 | async list(filters: any) { 31 | const options = filters.options; 32 | const filterConditions = filters.filters; 33 | console.log("Listing sites with params:", options, "and filters:", filterConditions); 34 | const sites = [ // simulate sites data 35 | { id: "site-1", name: "David-TCN", latitude: 47.23911, longitude: -122.44989, status: "active", address: "2309 S L St, Tacoma, WA 98405"}, 36 | { id: "site-2", name: "SURGEtacoma", latitude: 47.23854, longitude: -122.44094, status: "confirmed", address: "2367 Tacoma Ave S, Tacoma, WA 98402"}, 37 | { id: "site-3", name: "Filipino Community Center", latitude: 47.5501, longitude: -122.2872, status: "confirmed", address: "5740 Martin Luther King Jr Way S, Seattle, WA 98118"}, 38 | { id: "site-4", name: "Oareao OCC Masjid", latitude: 47.52391, longitude: -122.27636, status: "in-conversation", address: "8812 Renton Ave S, Seattle, WA 98118"}, 39 | { id: "site-5", name: "ALTSpace", latitude: 47.60816, longitude: -122.30192, status: "in-conversation", address: "2318 E Cherry St, Seattle, WA 98122"}, 40 | { id: "site-6", name: "Franklin High School", latitude: 47.576, longitude: -122.29307, status: "in-conversation", address: "3013 S Mt Baker Blvd, Seattle, WA 98144"}, 41 | { id: "site-7", name: "Garfield High School", latitude: 47.60533, longitude: -122.3018, status: "in-conversation", address: "400 23rd Ave, Seattle, WA 98122"}, 42 | { id: "site-8", name: "Skyway Library", latitude: 47.49049, longitude: -122.23853, status: "in-conversation", address: "12601 76th Ave S, Seattle, WA 98178"}, 43 | { id: "site-9", name: "University Heights Center", latitude: 47.66613, longitude: -122.31374, status: "in-conversation", address: "5031 University Way NE, Seattle, WA 98105"}, 44 | { id: "site-10", name: "Hillside Church Kent", latitude: 47.38639, longitude: -122.22205, status: "in-conversation", address: "930 E James St, Kent, WA 98031"}, 45 | 46 | ]; 47 | return { result: sites }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono, type Context } from 'hono'; 2 | import { cors } from 'hono/cors'; 3 | import { fromHono } from "chanfana"; 4 | import { CatchallList, CatchallCreate } from './endpoints/catchall'; 5 | import { RegisterCreate, PendingRegisteredMash } from './endpoints/registration'; 6 | import { RangeList } from './endpoints/ranges'; 7 | import { SessionCreate } from './endpoints/sessions'; 8 | import { SiteList, SiteFetch, SiteCreate, SiteUpdate } from "./endpoints/sites"; 9 | 10 | /****import { UserList, UserFetch, UserCreate } from "./endpoints/users"; 11 | import { UserRegister } from "./endpoints/registration"; 12 | import { SignalCreate } from "./endpoints/signals"; 13 | ****/ 14 | 15 | 16 | // Start a Hono app 17 | const app = new Hono<{ Bindings: Env }>(); 18 | const loggingMiddleware = async (c, next) => { 19 | const startTime = Date.now() 20 | await next(); 21 | const endTime = Date.now() 22 | const duration = endTime - startTime; 23 | const method = c.req.method; 24 | const url = c.req.url; 25 | const status = c.res.status; 26 | console.log(`${method} ${url} - ${status} - ${duration}ms`); 27 | }; 28 | 29 | app.use(loggingMiddleware); 30 | app.use('*', (c, next) => { 31 | const corsMiddlewareHandler = cors({ 32 | origin: c.env.CORS_ORIGIN, 33 | allowHeaders: ['Content-Type'], 34 | maxAge: 86400, 35 | credentials: true, 36 | }) 37 | return corsMiddlewareHandler(c, next) 38 | }); 39 | 40 | // Setup OpenAPI registry 41 | const openapi = fromHono(app, { 42 | docs_url: "/", 43 | }); 44 | 45 | 46 | openapi.get("/api/sites", SiteList); 47 | openapi.post("/api/sites", SiteCreate); 48 | openapi.get("/api/sites/:id", SiteFetch); 49 | openapi.put("/api/sites/:id", SiteUpdate); 50 | /**** 51 | openapi.get("/api/users", UserList); 52 | openapi.post("/api/users", UserCreate); 53 | openapi.get("/api/users/:id", UserFetch); 54 | openapi.post("/api/report_signal", SignalCreate); 55 | ****/ 56 | openapi.get('/api/data', CatchallList); 57 | openapi.get('/api/success', CatchallList); 58 | openapi.get('/api/failure', CatchallList); 59 | openapi.get('/api/sitesSummary', CatchallList); 60 | openapi.get('/api/lineSummary', CatchallList); 61 | openapi.get('/api/logout', CatchallList); 62 | openapi.get('/api/markers', CatchallList); 63 | openapi.get('/api/dataRange', RangeList); 64 | 65 | openapi.post('/api/register', RegisterCreate); 66 | openapi.post('/api/report_signal', CatchallCreate); 67 | openapi.post('/api/report_measurement', CatchallCreate); 68 | openapi.post('/secure/get_groups', CatchallCreate); 69 | openapi.post('/secure/delete_group', CatchallCreate); 70 | openapi.post('/secure/delete_manual', CatchallCreate); 71 | openapi.post('/secure/upload_data', CatchallCreate); 72 | openapi.post('/secure/get-users', PendingRegisteredMash); 73 | openapi.post('/secure/toggle-users', CatchallCreate); 74 | openapi.post('/secure/login', SessionCreate); 75 | openapi.post('/secure/edit_sites', CatchallCreate); 76 | openapi.post('/secure/new-user', CatchallCreate); 77 | 78 | // Export the Hono app 79 | export default app; 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /migrations/0001_schema_creation.sql: -------------------------------------------------------------------------------- 1 | -- Migration number: 0001 2025-05-06T22:20:57.448Z 2 | 3 | 4 | DROP TABLE IF EXISTS users; 5 | CREATE TABLE IF NOT EXISTS users ( 6 | id INTEGER PRIMARY KEY AUTOINCREMENT, 7 | rawdata TEXT NOT NULL, 8 | created TEXT DEFAULT CURRENT_TIMESTAMP, 9 | email AS (json_extract(rawdata, '$.email')) STORED, 10 | first_name AS (json_extract(rawdata, '$.firstName')) STORED, 11 | last_name AS (json_extract(rawdata, '$.lastName')) STORED, 12 | registered AS (json_extract(rawdata, '$.registered')) STORED, 13 | issue_date AS (json_extract(rawdata, '$.issueDate')) STORED, 14 | is_enabled AS (json_extract(rawdata, '$.isEnabled')) STORED, 15 | public_key AS (json_extract(rawdata, '$.publicKey')) STORED, 16 | qr_code AS (json_extract(rawdata, '$.qrCode')) STORED, 17 | last_online AS (json_extract(rawdata, '$.lastOnline')) STORED, 18 | "identity" AS (json_extract(rawdata, '$.identity')) STORED 19 | ); 20 | DROP TABLE IF EXISTS measurement; 21 | CREATE TABLE IF NOT EXISTS measurement ( 22 | id INTEGER PRIMARY KEY AUTOINCREMENT, 23 | rawdata TEXT NOT NULL, 24 | created TEXT DEFAULT CURRENT_TIMESTAMP, 25 | latitude AS (json_extract(rawdata, '$.latitude')) STORED, 26 | longitude AS (json_extract(rawdata, '$.longitude')) STORED, 27 | "timestamp" AS (json_extract(rawdata, '$.timestamp')) STORED, 28 | upload_speed AS (json_extract(rawdata, '$.upload_speed')) STORED, 29 | download_speed AS (json_extract(rawdata, '$.download_speed')) STORED, 30 | ping AS (json_extract(rawdata, '$.ping')) STORED, 31 | cell_id AS (json_extract(rawdata, '$.cell_id')) STORED, 32 | device_id AS (json_extract(rawdata, '$.device_id')) STORED, 33 | device_type AS (json_extract(rawdata, '$.device_type')) STORED, 34 | "group" AS (json_extract(rawdata, '$.group')) STORED, 35 | mid AS (json_extract(rawdata, '$.mid')) STORED, 36 | show_data AS (json_extract(rawdata, '$.show_data')) STORED 37 | ); 38 | DROP TABLE IF EXISTS "admin"; 39 | CREATE TABLE IF NOT EXISTS "admin" ( 40 | id INTEGER PRIMARY KEY AUTOINCREMENT, 41 | rawdata TEXT NOT NULL, 42 | created TEXT DEFAULT CURRENT_TIMESTAMP, 43 | "uid" AS (json_extract(rawdata, '$.uid')) STORED, 44 | token AS (json_extract(rawdata, '$.token')) STORED, 45 | "exp" AS (json_extract(rawdata, '$.exp')) STORED 46 | ); 47 | DROP TABLE IF EXISTS "signal"; 48 | CREATE TABLE IF NOT EXISTS "signal" ( 49 | id INTEGER PRIMARY KEY AUTOINCREMENT, 50 | rawdata TEXT NOT NULL, 51 | created TEXT DEFAULT CURRENT_TIMESTAMP, 52 | latitude AS (json_extract(rawdata, '$.latitude')) STORED, 53 | longitude AS (json_extract(rawdata, '$.longitude')) STORED, 54 | "timestamp" AS (json_extract(rawdata, '$.timestamp')) STORED, 55 | dbm AS (json_extract(rawdata, '$.dbm')) STORED, 56 | level_code AS (json_extract(rawdata, '$.level_code')) STORED, 57 | cell_id AS (json_extract(rawdata, '$.cell_id')) STORED, 58 | device_id AS (json_extract(rawdata, '$.device_id')) STORED, 59 | device_type AS (json_extract(rawdata, '$.device_type')) STORED, 60 | "group" AS (json_extract(rawdata, '$.group')) STORED, 61 | mid AS (json_extract(rawdata, '$.mid')) STORED, 62 | show_data AS (json_extract(rawdata, '$.show_data')) STORED 63 | ); 64 | 65 | DROP TABLE IF EXISTS sites; 66 | CREATE TABLE IF NOT EXISTS sites ( 67 | id INTEGER PRIMARY KEY AUTOINCREMENT, 68 | rawdata TEXT NOT NULL, 69 | created TEXT DEFAULT CURRENT_TIMESTAMP, 70 | latitude AS (json_extract(rawdata, '$.latitude')) STORED, 71 | longitude AS (json_extract(rawdata, '$.longitude')) STORED, 72 | "status" AS (json_extract(rawdata, '$.status')) STORED, 73 | address AS (json_extract(rawdata, '$.address')) STORED, 74 | cell_id AS (json_extract(rawdata, '$.cell_id')) STORED, 75 | color AS (json_extract(rawdata, '$.color')) STORED, 76 | boundary AS (json_extract(rawdata, '$.boundary')) STORED, 77 | "name" AS (json_extract(rawdata, '$.name')) STORED 78 | ); 79 | 80 | DROP TABLE IF EXISTS registration; 81 | CREATE TABLE IF NOT EXISTS registration ( 82 | id INTEGER PRIMARY KEY AUTOINCREMENT, 83 | rawdata TEXT NOT NULL, 84 | created TEXT DEFAULT CURRENT_TIMESTAMP, 85 | public_key AS (json_extract(rawdata, '$.publicKey')) STORED, 86 | message AS (json_extract(rawdata, '$.message')) STORED, 87 | sig_message AS (json_extract(rawdata, '$.sigMessage')) STORED 88 | ); 89 | 90 | DROP TABLE IF EXISTS datareport; 91 | CREATE TABLE IF NOT EXISTS datareport ( 92 | id INTEGER PRIMARY KEY AUTOINCREMENT, 93 | rawdata TEXT NOT NULL, 94 | created TEXT DEFAULT CURRENT_TIMESTAMP, 95 | h_pkr AS (json_extract(rawdata, '$.h_pkr')) STORED, 96 | sigma_m AS (json_extract(rawdata, '$.sigma_m')) STORED, 97 | "m" AS (json_extract(rawdata, '$.M')) STORED 98 | ); 99 | -------------------------------------------------------------------------------- /src/endpoints/RESTART-sites.ts: -------------------------------------------------------------------------------- 1 | 2 | import { D1CreateEndpoint, D1UpdateEndpoint, D1ReadEndpoint, D1ListEndpoint } from "chanfana"; 3 | import { z } from "zod"; 4 | 5 | 6 | // Define the Site Model 7 | const SiteModel = z.object({ 8 | name: z.string(), 9 | latitude: z.number(), 10 | longitude: z.number(), 11 | status: z.string(), 12 | address: z.string(), 13 | cell_id: z.array(z.string()).optional(), 14 | }); 15 | 16 | // Define the Meta object for Site 17 | const siteMeta = { 18 | model: { 19 | schema: SiteModel.omit({id: true, created: true, rawdata: true}), 20 | primaryKeys: ['id'], 21 | tableName: 'sites', // Table name in D1 database 22 | }, 23 | }; 24 | 25 | export class SiteFetch extends D1ReadEndpoint { _meta = siteMeta; dbName = "DB"; } 26 | export class SiteList extends D1ListEndpoint { _meta = siteMeta; dbName = "DB"; } 27 | 28 | //TODO "safer replace", check for existing record and move/mark as expired status (to be log/audit table). 29 | // Then insert the new record into table. 30 | // with create, we want to accept free form JSON for now 31 | const CreateModel = z.object({ 32 | name: z.string().min(3), 33 | }).catchall(z.unknown()); 34 | const createMeta = { 35 | model: { 36 | schema: CreateModel, 37 | tableName: 'sites', 38 | }, 39 | }; 40 | export class SiteCreate extends D1CreateEndpoint { 41 | _meta = createMeta; 42 | dbName = "DB"; 43 | 44 | async create(data: z.infer) { 45 | let inserted; 46 | let serialized; 47 | try { 48 | serialized = JSON.stringify(data) 49 | } catch (e: any) { 50 | // capture exception when stringify encounters BigInt/circular 51 | serialized = JSON.stringify(e, Object.getOwnPropertyNames(e)) 52 | } 53 | try { 54 | const result = await this.getDBBinding() 55 | .prepare( 56 | `INSERT INTO ${this.meta.model.tableName} (rawdata) VALUES (?) RETURNING *`, 57 | ) 58 | .bind(serialized) 59 | .all(); 60 | 61 | inserted = result.results[0] as O; 62 | } catch (e: any) { 63 | if (e.message.includes("UNIQUE constraint failed")) { 64 | const constraintMessage = e.message.split("UNIQUE constraint failed:")[1].split(":")[0].trim(); 65 | if (this.constraintsMessages[constraintMessage]) { 66 | throw this.constraintsMessages[constraintMessage]; 67 | } 68 | } 69 | 70 | throw new ApiException(e.message); 71 | } 72 | return inserted; 73 | } 74 | } 75 | // with update, we want to accept free form JSON for now 76 | export class SiteUpdate extends D1UpdateEndpoint { 77 | _meta = createMeta; 78 | dbName = "DB"; 79 | 80 | async getObject(filters: any) { 81 | const data = this.getValidatedData(); 82 | const jsonrequest = data.body; 83 | const sid = jsonrequest.id; 84 | let serialized; 85 | try { 86 | serialized = JSON.stringify(jsonrequest) 87 | } catch (e: any) { 88 | // capture exception when stringify encounters BigInt/circular 89 | serialized = JSON.stringify(e, Object.getOwnPropertyNames(e)) 90 | } 91 | // call json_patch to merge the json that will be saved in the next step 92 | // also pass the row key on to the next step which we use instead of another json-extract 93 | try { 94 | const result = await this.getDBBinding() 95 | .prepare( 96 | `SELECT id AS pk, json_patch(rawdata, ?2) AS mergedjson FROM ${this.meta.model.tableName} WHERE ?1 = json_extract(rawdata, '$.id')`, 97 | ) 98 | .bind(sid, serialized) 99 | .all(); 100 | 101 | mergedSite = result.results[0]; 102 | } catch (e: any) { 103 | throw new ApiException(e.message); 104 | } 105 | return mergedSite; 106 | } 107 | async update(mergedObj: any, filters: any) { 108 | let updated; 109 | let serialized; 110 | const rowkey = mergedObj.pk; 111 | try { 112 | serialized = JSON.stringify(mergedObj.mergedjson) 113 | } catch (e: any) { 114 | // capture exception when stringify encounters BigInt/circular 115 | serialized = JSON.stringify(e, Object.getOwnPropertyNames(e)) 116 | } 117 | try { 118 | const result = await this.getDBBinding() 119 | .prepare( 120 | `UPDATE ${this.meta.model.tableName} SET rawdata = ?2 WHERE id = ?1 RETURNING *`, 121 | ) 122 | .bind(rowkey,serialized) 123 | .all(); 124 | 125 | updated = result.results[0] as O; 126 | } catch (e: any) { 127 | throw new ApiException(e.message); 128 | } 129 | return updated; 130 | } 131 | } 132 | 133 | -------------------------------------------------------------------------------- /src/endpoints/sites.ts: -------------------------------------------------------------------------------- 1 | 2 | import { D1CreateEndpoint, D1UpdateEndpoint, D1ReadEndpoint, D1ListEndpoint } from "chanfana"; 3 | import { z } from "zod"; 4 | 5 | 6 | // Define the Site Model 7 | const SiteModel = z.object({ 8 | name: z.string(), 9 | latitude: z.number(), 10 | longitude: z.number(), 11 | status: z.string(), 12 | address: z.string(), 13 | cell_id: z.array(z.string()).optional(), 14 | }); 15 | 16 | // Define the Meta object for Site 17 | const siteMeta = { 18 | model: { 19 | schema: SiteModel.omit({id: true, created: true, rawdata: true}), 20 | primaryKeys: ['id'], 21 | tableName: 'sites', // Table name in D1 database 22 | }, 23 | }; 24 | 25 | export class SiteFetch extends D1ReadEndpoint { _meta = siteMeta; dbName = "DB"; } 26 | export class SiteList extends D1ListEndpoint { _meta = siteMeta; dbName = "DB"; } 27 | 28 | //TODO "safer replace", check for existing record and move/mark as expired status (to be log/audit table). 29 | // Then insert the new record into table. 30 | // with create, we want to accept free form JSON for now 31 | const CreateModel = z.object({ 32 | name: z.string().min(3), 33 | }).catchall(z.unknown()); 34 | const createMeta = { 35 | model: { 36 | schema: CreateModel, 37 | tableName: 'sites', 38 | }, 39 | }; 40 | export class SiteCreate extends D1CreateEndpoint { 41 | _meta = createMeta; 42 | dbName = "DB"; 43 | 44 | async create(data: z.infer) { 45 | let inserted; 46 | let serialized; 47 | try { 48 | serialized = JSON.stringify(data) 49 | } catch (e: any) { 50 | // capture exception when stringify encounters BigInt/circular 51 | serialized = JSON.stringify(e, Object.getOwnPropertyNames(e)) 52 | } 53 | try { 54 | const result = await this.getDBBinding() 55 | .prepare( 56 | `INSERT INTO ${this.meta.model.tableName} (rawdata) VALUES (?) RETURNING *`, 57 | ) 58 | .bind(serialized) 59 | .all(); 60 | 61 | inserted = result.results[0] as O; 62 | } catch (e: any) { 63 | if (e.message.includes("UNIQUE constraint failed")) { 64 | const constraintMessage = e.message.split("UNIQUE constraint failed:")[1].split(":")[0].trim(); 65 | if (this.constraintsMessages[constraintMessage]) { 66 | throw this.constraintsMessages[constraintMessage]; 67 | } 68 | } 69 | 70 | throw new ApiException(e.message); 71 | } 72 | return inserted; 73 | } 74 | } 75 | // with update, we want to accept free form JSON for now 76 | export class SiteUpdate extends D1UpdateEndpoint { 77 | _meta = createMeta; 78 | dbName = "DB"; 79 | 80 | async getObject(filters: any) { 81 | /******************** 82 | const updatedData = filters.updatedData; 83 | const sid = updatedData.id; 84 | let serialized; 85 | try { 86 | serialized = JSON.stringify(updatedData) 87 | } catch (e: any) { 88 | // capture exception when stringify encounters BigInt/circular 89 | serialized = JSON.stringify(e, Object.getOwnPropertyNames(e)) 90 | } 91 | // call json_patch to merge the json that will be saved in the next step 92 | // also pass the row key on to the next step which we use instead of another json-extract 93 | try { 94 | const result = await this.getDBBinding() 95 | .prepare( 96 | `SELECT id AS pk, json_patch(rawdata, ?2) AS mergedjson FROM ${this.meta.model.tableName} WHERE ?1 = json_extract(rawdata, '$.id')`, 97 | ) 98 | .bind(sid, serialized) 99 | .all(); 100 | 101 | mergedSite = result.results[0]; 102 | } catch (e: any) { 103 | throw new ApiException(e.message); 104 | } 105 | return mergedSite; 106 | * *******************/ 107 | } 108 | async update(mergedObj: any, filters: any) { 109 | const updatedData = filters.updatedData; 110 | const sid = updatedData.id; 111 | 112 | let updated; 113 | let serialized; 114 | try { 115 | serialized = JSON.stringify(updatedData) 116 | } catch (e: any) { 117 | // capture exception when stringify encounters BigInt/circular 118 | serialized = JSON.stringify(e, Object.getOwnPropertyNames(e)) 119 | } 120 | 121 | try { 122 | const result = await this.getDBBinding() 123 | .prepare( 124 | `UPDATE ${this.meta.model.tableName} SET rawdata = ?2 WHERE id IN (SELECT BB.id FROM ${this.meta.model.tableName} AS BB WHERE ?1=json_extract(BB.rawdata, '$.id')) RETURNING *`, 125 | ) 126 | .bind(sid, serialized) 127 | .all(); 128 | 129 | updated = result.results[0] as O; 130 | } catch (e: any) { 131 | throw new ApiException(e.message); 132 | } 133 | return updated; 134 | } 135 | } 136 | 137 | -------------------------------------------------------------------------------- /uml/SitesRetrieval.svg: -------------------------------------------------------------------------------- 1 | "Sites (read)"File Exist Check "sites.json"exist?yesnoRead file "sites.json"Create "sites.json" from copying "sites-default.json"Read file "sites-default.json"success?oknoConvert bytes to text/stringParse into JSON objError 500Write ResponseGET /api/sitesClose Stream -------------------------------------------------------------------------------- /uml/SitesPersistence.svg: -------------------------------------------------------------------------------- 1 | "Sites (write)"Ensure Login middlewarehasLoginSession?yesnoAccess Body Property "sites"valid?oknoParse into JSON objvalid?oknoMarshal JSON to text/stringWrite file "sites.json"success?oknoStatus 201Error 500Error 400Error 400Error 400Write ResponsePOST /secure/edit_sitesClose Stream --------------------------------------------------------------------------------