create-fff-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +21 -0
- package/src/index.ts +119 -0
- package/template/.config/dotnet-tools.json +5 -0
- package/template/dev-server.ts +415 -0
- package/template/index.html +23 -0
- package/template/migrate.ts +166 -0
- package/template/migrations/0001_init.sql +25 -0
- package/template/migrations/0002_employees.sql +16 -0
- package/template/package.json +29 -0
- package/template/public/app.js +156 -0
- package/template/public/runtime.js +149 -0
- package/template/public/style.css +769 -0
- package/template/schema.sql +25 -0
- package/template/src/client/App.fs +55 -0
- package/template/src/client/Client.fsproj +24 -0
- package/template/src/client/Nav.fs +75 -0
- package/template/src/server/Domain/Employees/Employee.fs +34 -0
- package/template/src/server/Endpoints/Employees/Routes.fs +35 -0
- package/template/src/server/Endpoints/Pages/About.fjsx +66 -0
- package/template/src/server/Endpoints/Pages/EmployeeEdit.fjsx +66 -0
- package/template/src/server/Endpoints/Pages/EmployeeList.fjsx +54 -0
- package/template/src/server/Endpoints/Pages/EmployeeNew.fjsx +55 -0
- package/template/src/server/Endpoints/Pages/Layout.fjsx +40 -0
- package/template/src/server/Endpoints/Pages/Routes.fs +117 -0
- package/template/src/server/Endpoints/Pages/Templates.fs +45 -0
- package/template/src/server/Server.fsproj +28 -0
- package/template/src/server/Worker.fs +45 -0
- package/template/src/server/Workflows/Employees/EmployeeWorkflows.fs +86 -0
- package/template/webpack.client.js +18 -0
- package/template/wrangler.toml +23 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module App.Server.Endpoints.Pages.Templates
|
|
2
|
+
|
|
3
|
+
open Fable.Core
|
|
4
|
+
open Fable.Core.JsInterop
|
|
5
|
+
|
|
6
|
+
// ── Compiled FJSX page template modules ──────────────────────────────────────
|
|
7
|
+
// These static imports are resolved at Worker startup.
|
|
8
|
+
// The FJSX files must be compiled first:
|
|
9
|
+
// fjsx --dir src/server/Endpoints/Pages --out dist/server/Endpoints/Pages
|
|
10
|
+
|
|
11
|
+
[<ImportAll("./Layout.js")>]
|
|
12
|
+
let private layoutMod : obj = jsNative
|
|
13
|
+
|
|
14
|
+
[<ImportAll("./EmployeeList.js")>]
|
|
15
|
+
let private employeeListMod : obj = jsNative
|
|
16
|
+
|
|
17
|
+
[<ImportAll("./EmployeeNew.js")>]
|
|
18
|
+
let private employeeNewMod : obj = jsNative
|
|
19
|
+
|
|
20
|
+
[<ImportAll("./About.js")>]
|
|
21
|
+
let private aboutMod : obj = jsNative
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
[<ImportAll("./EmployeeEdit.js")>]
|
|
25
|
+
let private employeeEditMod : obj = jsNative
|
|
26
|
+
// ── Render helpers ────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
let renderLayout (data: obj) : string = layoutMod?renderToHtml $ data
|
|
29
|
+
let renderEmployeeList (data: obj) : string = employeeListMod?renderToHtml $ data
|
|
30
|
+
let renderEmployeeNew (data: obj) : string = employeeNewMod?renderToHtml $ data
|
|
31
|
+
let renderAbout () : string = aboutMod?renderToHtml $ (createObj [])
|
|
32
|
+
|
|
33
|
+
let renderEmployeeEdit (data: obj) : string = employeeEditMod?renderToHtml $ data
|
|
34
|
+
|
|
35
|
+
/// Full-page HTML shell.
|
|
36
|
+
/// title — page title shown in <title> tag
|
|
37
|
+
/// activePath — used to highlight the active nav link
|
|
38
|
+
/// body — raw HTML injected into <main>
|
|
39
|
+
let shell (title: string) (activePath: string) (body: string) : string =
|
|
40
|
+
renderLayout (createObj [
|
|
41
|
+
"title" ==> title
|
|
42
|
+
"homeActive" ==> (activePath = "/")
|
|
43
|
+
"aboutActive" ==> (activePath = "/about")
|
|
44
|
+
"body" ==> body
|
|
45
|
+
])
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
2
|
+
|
|
3
|
+
<PropertyGroup>
|
|
4
|
+
<TargetFramework>net9.0</TargetFramework>
|
|
5
|
+
<RootNamespace>App.Server</RootNamespace>
|
|
6
|
+
</PropertyGroup>
|
|
7
|
+
|
|
8
|
+
<ItemGroup>
|
|
9
|
+
<!-- Domain — pure types, no side effects -->
|
|
10
|
+
<Compile Include="Domain/Employees/Employee.fs" />
|
|
11
|
+
|
|
12
|
+
<!-- Workflows — business logic (may use Infrastructure) -->
|
|
13
|
+
<Compile Include="Workflows/Employees/EmployeeWorkflows.fs" />
|
|
14
|
+
|
|
15
|
+
<!-- Endpoints — HTTP layer (routes + page templates) -->
|
|
16
|
+
<Compile Include="Endpoints/Employees/Routes.fs" />
|
|
17
|
+
<Compile Include="Endpoints/Pages/Templates.fs" />
|
|
18
|
+
<Compile Include="Endpoints/Pages/Routes.fs" />
|
|
19
|
+
|
|
20
|
+
<!-- Entry point -->
|
|
21
|
+
<Compile Include="Worker.fs" />
|
|
22
|
+
</ItemGroup>
|
|
23
|
+
|
|
24
|
+
<ItemGroup>
|
|
25
|
+
<PackageReference Include="FffStack.Server" Version="0.1.0" />
|
|
26
|
+
</ItemGroup>
|
|
27
|
+
|
|
28
|
+
</Project>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module App.Server.Worker
|
|
2
|
+
|
|
3
|
+
open FffStack.Server
|
|
4
|
+
open Fable.Core
|
|
5
|
+
|
|
6
|
+
let router = Router()
|
|
7
|
+
|
|
8
|
+
// ── API routes (JSON) ─────────────────────────────────────────────────────────
|
|
9
|
+
Endpoints.Employees.Routes.register router
|
|
10
|
+
|
|
11
|
+
// ── Page routes (HTML, MPA) ───────────────────────────────────────────────────
|
|
12
|
+
Endpoints.Pages.Routes.register router
|
|
13
|
+
|
|
14
|
+
// ── Error pages ───────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
router.OnNotFound(fun _ctx ->
|
|
17
|
+
promise {
|
|
18
|
+
let body = """
|
|
19
|
+
<section class="card">
|
|
20
|
+
<div class="card-label">404</div>
|
|
21
|
+
<h2>Page not found</h2>
|
|
22
|
+
<p class="text-muted" style="margin-top:0.5rem">The page you were looking for doesn't exist.</p>
|
|
23
|
+
<a href="/" class="btn btn-primary" style="display:inline-block;margin-top:1.25rem">← Back to Employees</a>
|
|
24
|
+
</section>"""
|
|
25
|
+
return Response.html (Endpoints.Pages.Templates.shell "Not Found" "" body)
|
|
26
|
+
}) |> ignore
|
|
27
|
+
|
|
28
|
+
router.OnError(fun _ctx ex ->
|
|
29
|
+
let msg = ex.Message
|
|
30
|
+
let body =
|
|
31
|
+
sprintf """
|
|
32
|
+
<section class="card">
|
|
33
|
+
<div class="card-label">500</div>
|
|
34
|
+
<h2>Something went wrong</h2>
|
|
35
|
+
<p class="text-muted" style="margin-top:0.5rem">%s</p>
|
|
36
|
+
<a href="/" class="btn btn-secondary" style="display:inline-block;margin-top:1.25rem">← Back to Employees</a>
|
|
37
|
+
</section>"""
|
|
38
|
+
msg
|
|
39
|
+
promise {
|
|
40
|
+
return Response.html (Endpoints.Pages.Templates.shell "Error" "" body)
|
|
41
|
+
}) |> ignore
|
|
42
|
+
|
|
43
|
+
// ── Cloudflare Workers entry point ────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
router.ExportAsWorkerDefault()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module App.Server.Workflows.Employees
|
|
2
|
+
|
|
3
|
+
open FffStack.Server
|
|
4
|
+
open Fable.Core
|
|
5
|
+
open Fable.Core.JsInterop
|
|
6
|
+
open App.Server.Domain.Employees
|
|
7
|
+
|
|
8
|
+
[<Emit("crypto.randomUUID()")>]
|
|
9
|
+
let private newId () : string = jsNative
|
|
10
|
+
|
|
11
|
+
let getAll (db: ID1Database) : JS.Promise<Employee list> =
|
|
12
|
+
queryAll<Employee> db
|
|
13
|
+
"SELECT id, name, email, department, role, status, joined_at as joinedAt \
|
|
14
|
+
FROM employees ORDER BY name ASC"
|
|
15
|
+
[||]
|
|
16
|
+
|
|
17
|
+
let getById (db: ID1Database) (id: string) : JS.Promise<Employee option> =
|
|
18
|
+
queryOne<Employee> db
|
|
19
|
+
"SELECT id, name, email, department, role, status, joined_at as joinedAt \
|
|
20
|
+
FROM employees WHERE id = ?"
|
|
21
|
+
[| id |]
|
|
22
|
+
|
|
23
|
+
let create
|
|
24
|
+
(db: ID1Database)
|
|
25
|
+
(rawName: string)
|
|
26
|
+
(rawEmail: string)
|
|
27
|
+
(department: string)
|
|
28
|
+
(role: string)
|
|
29
|
+
(status: string)
|
|
30
|
+
: JS.Promise<Result<Employee, string>> =
|
|
31
|
+
promise {
|
|
32
|
+
match EmployeeName.create rawName, WorkEmail.create rawEmail with
|
|
33
|
+
| Error e, _ -> return Error e
|
|
34
|
+
| _, Error e -> return Error e
|
|
35
|
+
| Ok name, Ok email ->
|
|
36
|
+
let id = newId ()
|
|
37
|
+
let n = EmployeeName.value name
|
|
38
|
+
let em = WorkEmail.value email
|
|
39
|
+
let dep = department.Trim()
|
|
40
|
+
let rol = role.Trim()
|
|
41
|
+
let st = if status = "inactive" then "inactive" else "active"
|
|
42
|
+
do! execute db
|
|
43
|
+
"INSERT INTO employees (id, name, email, department, role, status, joined_at) \
|
|
44
|
+
VALUES (?, ?, ?, ?, ?, ?, date('now'))"
|
|
45
|
+
[| id; n; em; dep; rol; st |]
|
|
46
|
+
|> Promise.map ignore
|
|
47
|
+
return Ok { id = id; name = n; email = em; department = dep
|
|
48
|
+
role = rol; status = st; joinedAt = "" }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let update
|
|
52
|
+
(db: ID1Database)
|
|
53
|
+
(id: string)
|
|
54
|
+
(rawName: string)
|
|
55
|
+
(rawEmail: string)
|
|
56
|
+
(department: string)
|
|
57
|
+
(role: string)
|
|
58
|
+
(status: string)
|
|
59
|
+
: JS.Promise<Result<Employee, string>> =
|
|
60
|
+
promise {
|
|
61
|
+
match EmployeeName.create rawName, WorkEmail.create rawEmail with
|
|
62
|
+
| Error e, _ -> return Error e
|
|
63
|
+
| _, Error e -> return Error e
|
|
64
|
+
| Ok name, Ok email ->
|
|
65
|
+
let n = EmployeeName.value name
|
|
66
|
+
let em = WorkEmail.value email
|
|
67
|
+
let dep = department.Trim()
|
|
68
|
+
let rol = role.Trim()
|
|
69
|
+
let st = if status = "inactive" then "inactive" else "active"
|
|
70
|
+
do! execute db
|
|
71
|
+
"UPDATE employees SET name = ?, email = ?, department = ?, role = ?, status = ? WHERE id = ?"
|
|
72
|
+
[| n; em; dep; rol; st; id |]
|
|
73
|
+
|> Promise.map ignore
|
|
74
|
+
return Ok { id = id; name = n; email = em; department = dep
|
|
75
|
+
role = rol; status = st; joinedAt = "" }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let delete (db: ID1Database) (id: string) : JS.Promise<bool> =
|
|
79
|
+
promise {
|
|
80
|
+
match! getById db id with
|
|
81
|
+
| None -> return false
|
|
82
|
+
| Some _ ->
|
|
83
|
+
do! execute db "DELETE FROM employees WHERE id = ?" [| id |]
|
|
84
|
+
|> Promise.map ignore
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
entry: './dist/client/fable/App.js',
|
|
9
|
+
output: {
|
|
10
|
+
path: path.resolve(__dirname, 'dist/client'),
|
|
11
|
+
filename: 'bundle.js',
|
|
12
|
+
},
|
|
13
|
+
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
|
14
|
+
devtool: 'source-map',
|
|
15
|
+
resolve: {
|
|
16
|
+
extensions: ['.js'],
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name = "fff-stack-app"
|
|
2
|
+
main = "dist/server/Worker.js"
|
|
3
|
+
compatibility_date = "2024-09-23"
|
|
4
|
+
|
|
5
|
+
# Client assets served from Cloudflare Pages / Workers Static Assets
|
|
6
|
+
[assets]
|
|
7
|
+
directory = "./dist/client"
|
|
8
|
+
binding = "ASSETS"
|
|
9
|
+
|
|
10
|
+
# Cloudflare D1 (SQLite)
|
|
11
|
+
[[d1_databases]]
|
|
12
|
+
binding = "DB"
|
|
13
|
+
database_name = "fff-stack-db"
|
|
14
|
+
database_id = "YOUR_D1_DATABASE_ID" # replace after: wrangler d1 create fff-stack-db
|
|
15
|
+
|
|
16
|
+
# Cloudflare R2 object storage
|
|
17
|
+
[[r2_buckets]]
|
|
18
|
+
binding = "BUCKET"
|
|
19
|
+
bucket_name = "fff-stack-bucket" # replace with your bucket name
|
|
20
|
+
# preview_bucket_name = "fff-stack-bucket-dev" # optional local dev bucket
|
|
21
|
+
|
|
22
|
+
[build]
|
|
23
|
+
command = "bun run build"
|