Files
RuvdsTest/src/Client/Shared/JsonWire.fs
danamir dfbc43a43e
Some checks failed
CI/CD / build-test-deploy (push) Has been cancelled
Make client API calls relative instead of hardcoded localhost:5144
The client always called a hardcoded http://localhost:5144, which only
worked when browser and server shared the same "localhost" — breaks for
any real remote deployment, since the browser would try to reach that
port on the visitor's own machine instead of the actual server.

Now every environment routes /api/* to the server under the same origin
the page was loaded from, so the client code needs no per-environment URL:
- Docker (client container's own nginx) proxies /api/ to the server
  container.
- `npm run dev` (Vite) proxies /api to localhost:5144 via server.proxy.
- Production nginx (reverse proxy + TLS) just needs to forward everything
  to the client container, which already knows how to route /api itself.

Also incidentally removes CORS from the picture everywhere, since none of
these setups make a cross-origin request anymore.
2026-08-06 14:15:15 +03:00

78 lines
3.5 KiB
Forth

module Client.Shared.JsonWire
// Fable.Remoting.Client pulls in Fable.Remoting.MsgPack, whose `inline`
// helpers reference private functions in a way the current Fable compiler
// rejects at build time. Until that's fixed upstream, each feature's `Api.fs`
// talks to its own plain Giraffe route by hand, using the helpers below to
// POST/GET JSON and decode the {"Ok": ...} / {"Error": ...} shape
// Fable.Remoting.Json already produces on the server (see `Server/Json.fs`).
open Fable.Core
open Fable.Core.JsInterop
open Domain
// Relative on purpose the server is always reached through whatever's
// serving this page under `/api/*`: the client's own nginx in Docker
// (see Client/nginx.conf), Vite's dev-server proxy (vite.config.js) for
// `npm run dev`, or the host reverse proxy in production. This also means
// the browser never makes a cross-origin request, so CORS never enters
// into it for any of these setups.
let private serverUrl = ""
[<Emit("Object.prototype.hasOwnProperty.call($0, $1)")>]
let hasKey (_o: obj) (_key: string) : bool = jsNative
[<Emit("fetch($0, $1).then(r => r.json())")>]
let private fetchJson (_url: string) (_init: obj) : JS.Promise<obj> = jsNative
/// `body = None` for GET-style calls with no request payload.
let callApi (token: string option) (httpMethod: string) (path: string) (body: obj option) : Async<obj> =
async {
let headers =
match token with
| Some t -> createObj [ "Content-Type" ==> "application/json"; "Authorization" ==> ("Bearer " + t) ]
| None -> createObj [ "Content-Type" ==> "application/json" ]
let baseFields = [ "method" ==> httpMethod; "headers" ==> headers ]
let fields =
match body with
| Some b -> baseFields @ [ "body" ==> JS.JSON.stringify b ]
| None -> baseFields
return! fetchJson (serverUrl + path) (createObj fields) |> Async.AwaitPromise
}
// ---- Id encode/decode: wire shape is {"CaseName": "<guid>"} ----
let encQuizId (QuizId g) : obj = createObj [ "QuizId" ==> string g ]
let encTopicId (TopicId g) : obj = createObj [ "TopicId" ==> string g ]
let encUserId (UserId g) : obj = createObj [ "UserId" ==> string g ]
let encAttemptId (AttemptId g) : obj = createObj [ "AttemptId" ==> string g ]
let encQuestionId (QuestionId g) : obj = createObj [ "QuestionId" ==> string g ]
let encOptionId (OptionId g) : obj = createObj [ "OptionId" ==> string g ]
let decUserId (o: obj) : UserId = UserId(System.Guid.Parse(o?UserId: string))
let decTopicId (o: obj) : TopicId = TopicId(System.Guid.Parse(o?TopicId: string))
let decQuizId (o: obj) : QuizId = QuizId(System.Guid.Parse(o?QuizId: string))
let decQuestionId (o: obj) : QuestionId = QuestionId(System.Guid.Parse(o?QuestionId: string))
let decOptionId (o: obj) : OptionId = OptionId(System.Guid.Parse(o?OptionId: string))
let decAttemptId (o: obj) : AttemptId = AttemptId(System.Guid.Parse(o?AttemptId: string))
let optToJs (mapper: 'a -> obj) (opt: 'a option) : obj =
match opt with
| Some x -> mapper x
| None -> null
let optDec (mapper: obj -> 'a) (raw: obj) : 'a option =
if isNullOrUndefined raw then None else Some(mapper raw)
let decodeRole (raw: obj) : Role =
match unbox<string> raw with
| "Admin" -> Admin
| "Teacher" -> Teacher
| "Student" -> Student
| other -> failwithf "Неизвестная роль: %s" other
let decodeResult (decodeOk: obj -> 'a) (raw: obj) : Result<'a, string> =
if hasKey raw "Ok" then Ok(decodeOk raw?Ok) else Error(unbox<string> raw?Error)