frappebun 0.0.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.
Files changed (66) hide show
  1. package/README.md +72 -0
  2. package/package.json +59 -0
  3. package/src/api/auth.ts +76 -0
  4. package/src/api/index.ts +10 -0
  5. package/src/api/resource.ts +177 -0
  6. package/src/api/route.ts +301 -0
  7. package/src/app/index.ts +6 -0
  8. package/src/app/loader.ts +218 -0
  9. package/src/auth/auth.ts +247 -0
  10. package/src/auth/index.ts +2 -0
  11. package/src/cli/args.ts +40 -0
  12. package/src/cli/bin.ts +12 -0
  13. package/src/cli/commands/add-api.ts +32 -0
  14. package/src/cli/commands/add-doctype.ts +43 -0
  15. package/src/cli/commands/add-page.ts +33 -0
  16. package/src/cli/commands/add-user.ts +96 -0
  17. package/src/cli/commands/dev.ts +71 -0
  18. package/src/cli/commands/drop-site.ts +27 -0
  19. package/src/cli/commands/init.ts +98 -0
  20. package/src/cli/commands/migrate.ts +110 -0
  21. package/src/cli/commands/new-site.ts +61 -0
  22. package/src/cli/commands/routes.ts +56 -0
  23. package/src/cli/commands/use.ts +30 -0
  24. package/src/cli/index.ts +73 -0
  25. package/src/cli/log.ts +13 -0
  26. package/src/cli/scaffold/templates.ts +189 -0
  27. package/src/context.ts +162 -0
  28. package/src/core/doctype/migration/migration.ts +17 -0
  29. package/src/core/doctype/role/role.ts +7 -0
  30. package/src/core/doctype/session/session.ts +16 -0
  31. package/src/core/doctype/user/user.controller.ts +11 -0
  32. package/src/core/doctype/user/user.ts +22 -0
  33. package/src/core/doctype/user_role/user_role.ts +9 -0
  34. package/src/core/doctypes.ts +25 -0
  35. package/src/core/index.ts +1 -0
  36. package/src/database/database.ts +359 -0
  37. package/src/database/filters.ts +131 -0
  38. package/src/database/index.ts +30 -0
  39. package/src/database/query-builder.ts +1118 -0
  40. package/src/database/schema.ts +188 -0
  41. package/src/doctype/define.ts +45 -0
  42. package/src/doctype/discovery.ts +57 -0
  43. package/src/doctype/field.ts +160 -0
  44. package/src/doctype/index.ts +20 -0
  45. package/src/doctype/layout.ts +62 -0
  46. package/src/doctype/query-builder-stub.ts +16 -0
  47. package/src/doctype/registry.ts +106 -0
  48. package/src/doctype/types.ts +407 -0
  49. package/src/document/document.ts +593 -0
  50. package/src/document/index.ts +6 -0
  51. package/src/document/naming.ts +56 -0
  52. package/src/errors.ts +53 -0
  53. package/src/frappe.d.ts +128 -0
  54. package/src/globals.ts +72 -0
  55. package/src/index.ts +112 -0
  56. package/src/migrations/index.ts +11 -0
  57. package/src/migrations/runner.ts +256 -0
  58. package/src/permissions/index.ts +265 -0
  59. package/src/response.ts +100 -0
  60. package/src/server.ts +210 -0
  61. package/src/site.ts +126 -0
  62. package/src/ssr/handler.ts +56 -0
  63. package/src/ssr/index.ts +11 -0
  64. package/src/ssr/page-loader.ts +200 -0
  65. package/src/ssr/renderer.ts +94 -0
  66. package/src/ssr/use-context.ts +41 -0
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # frappebun 🍞⚡
2
+
3
+ [![CI](https://github.com/netchampfaris/frappebun/actions/workflows/ci.yml/badge.svg)](https://github.com/netchampfaris/frappebun/actions/workflows/ci.yml)
4
+ [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/netchampfaris/7e5a601fde8a6df937135068d2195e70/raw/frappebun-coverage.json)](https://github.com/netchampfaris/frappebun/actions/workflows/ci.yml)
5
+
6
+ ## Quick Start
7
+
8
+ ```bash
9
+ bun install frappebun
10
+
11
+ frappe init todo
12
+ ```
13
+
14
+
15
+ ## Motivation
16
+
17
+ I've been working with Frappe Framework for a long time. There is a lot I love about it — DocType-driven development, multi-tenancy, the idea that defining your data should give you most of what you need. But there's also a lot of baggage: Python, Redis, MariaDB, a hook system held together by strings, and an API surface that grew organically over many years without much care.
18
+
19
+ This is my imagination of Frappe Framework if we designed it fresh today, with TypeScript and Bun, and design each API with taste and care.
20
+
21
+ ## The Core Idea
22
+
23
+ **Define your data, get everything for free.**
24
+
25
+ A single DocType definition should be enough to generate the database schema, REST API, forms, list views, permissions, and search — with little to no glue code. The framework should feel inevitable to read. Controllers should be pure logic. Imports should be minimal. There should be one obvious way to do each common thing.
26
+
27
+ A Frappe developer should feel at home immediately. A non-Frappe developer should understand most files without reading docs.
28
+
29
+ ## Progress
30
+
31
+ | Area | Status | Notes |
32
+ |------|--------|-------|
33
+ | Runtime & globals | ✅ Done | Bun HTTP server, `AsyncLocalStorage` context, ambient `frappe` global |
34
+ | DocType definition | ✅ Done | `doctype()`, full field library, layout helpers |
35
+ | Query builder | ✅ Done | Fluent `frappe.query()`, link-field dot-notation, filters |
36
+ | Database | ✅ Done | `frappe.db.*` API, SQLite adapter |
37
+ | Auth | ✅ Done | Session auth, `User` / `Session` DocTypes, API key auth |
38
+ | Document CRUD | ✅ Done | Lifecycle hooks, validation, naming strategies |
39
+ | REST API | ✅ Done | Opt-in `rest: true`, explicit `route()` registration |
40
+ | Permissions | ✅ Done | Role-based, `scope()`, per-action checks |
41
+ | App loading | ✅ Done | Auto-discovery, multi-site |
42
+ | CLI | ✅ Done | `frappe init`, `frappe dev`, `frappe new-site`, `frappe migrate` |
43
+ | Migrations | ✅ Done | Schema sync, data migrations, patch runner |
44
+ | Frontend SSR (minimal) | 🔄 In progress | Basic Vue SSR, `<script context>`, hydration |
45
+ | Hooks & extensions | | App extension system, doc event listeners |
46
+ | Background jobs | | Job queue, scheduled tasks |
47
+ | Frontend SSR (full) | | File-based routing, full SSR pipeline |
48
+ | Admin desk | | Auto-generated forms and list views |
49
+ | Frontend composables | | `useDoc`, `useDocList`, realtime bindings |
50
+ | Realtime | | WebSocket pub/sub, document subscriptions |
51
+ | Visual builder | | Two-way `.ts` sync with a GUI DocType editor |
52
+ | Email | | Sending, templates, incoming email |
53
+ | Files | | Upload handling, S3/local adapters |
54
+
55
+ ## Why frappebun
56
+
57
+ - **Fast** — Bun startup is near-instant, SQLite has no network hop, no Redis in the critical path
58
+ - **One process** — HTTP, WebSocket, background jobs, and asset bundling in one `bun` process, no daemons
59
+ - **Two file types** — `.ts` for server code and schema, `.vue` for UI; no Python, no Jinja, no `requirements.txt`
60
+ - **SQLite by default** — no setup to start; switch to Postgres when you need it
61
+ - **Single app per project** — one project, one app, one namespace; no multi-app overhead
62
+ - **Auto-migration in dev** — add a field, save, it's in the database; no `bench migrate` in dev
63
+ - **Ambient globals** — `frappe`, `doctype`, `controller`, `field` are global; controllers have zero imports
64
+ - **Schema and logic are separate** — `doctype()` files are restricted to literals and field helpers; no logic leaks in
65
+ - **Typed hooks** — hooks are typed function references, not strings; your editor can navigate and refactor them
66
+ - **TypeScript end-to-end** — one language across server, schema, and frontend
67
+
68
+ ## Start Here
69
+
70
+ - **[spec/goals.md](./spec/goals.md)** — Product direction, philosophy, and design taste
71
+ - **[spec/api-reference.md](./spec/api-reference.md)** — The intended `frappe` API surface
72
+ - **[spec/spec-plan.md](./spec/spec-plan.md)** — Spec index, layered plan, and build order
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "frappebun",
3
+ "version": "0.0.0",
4
+ "description": "A clean, TypeScript-native Frappe-style framework for Bun",
5
+ "keywords": [
6
+ "frappe",
7
+ "bun",
8
+ "framework",
9
+ "typescript",
10
+ "sqlite"
11
+ ],
12
+ "homepage": "https://github.com/netchampfaris/frappebun",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/netchampfaris/frappebun.git"
16
+ },
17
+ "license": "MIT",
18
+ "engines": {
19
+ "bun": ">=1.0.0"
20
+ },
21
+ "bin": {
22
+ "frappe": "src/cli/bin.ts"
23
+ },
24
+ "main": "src/index.ts",
25
+ "types": "src/frappe.d.ts",
26
+ "exports": {
27
+ ".": "./src/index.ts",
28
+ "./cli": "./src/cli/index.ts"
29
+ },
30
+ "files": [
31
+ "src/",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "prepare": "git config core.hooksPath .githooks",
37
+ "prepublishOnly": "bun run check && bun test",
38
+ "test": "bun test",
39
+ "test:coverage": "bun test --coverage",
40
+ "lint": "oxlint src tests",
41
+ "lint:fix": "oxlint src tests --fix",
42
+ "format": "oxfmt src tests",
43
+ "format:check": "oxfmt src tests --check",
44
+ "check": "bun run lint && bun run format:check"
45
+ },
46
+ "dependencies": {
47
+ "@vue/compiler-sfc": "^3.5.32",
48
+ "@vue/server-renderer": "^3.5.32",
49
+ "vue": "^3.5.32"
50
+ },
51
+ "devDependencies": {
52
+ "@types/bun": "latest",
53
+ "oxfmt": "^0.43.0",
54
+ "oxlint": "^1.58.0"
55
+ },
56
+ "peerDependencies": {
57
+ "typescript": "^5.0.0"
58
+ }
59
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * /api/auth — login, logout, and CSRF token endpoints.
3
+ */
4
+
5
+ import { contextStore } from "../context"
6
+ import { response } from "../response"
7
+ import { route } from "./route"
8
+
9
+ // POST /api/auth/login
10
+ export const login = route({
11
+ path: "/api/auth/login",
12
+ auth: "public",
13
+ method: "POST",
14
+ async handler({ email, password }: { email?: unknown; password?: unknown }) {
15
+ if (!email || !password) return response.error("email and password are required", 400)
16
+
17
+ const ctx = contextStore.getStore()!
18
+ const { login: loginFn } = await import("../auth/auth")
19
+ const result = await loginFn(String(email), String(password), ctx.db, ctx.request)
20
+
21
+ if (!result) return response.unauthorized("Invalid email or password")
22
+
23
+ // Return Response directly to control the Set-Cookie header
24
+ return new Response(
25
+ JSON.stringify({ user: result.user, fullName: result.fullName, csrfToken: result.csrfToken }),
26
+ { status: 200, headers: { "Content-Type": "application/json", "Set-Cookie": result.cookie } },
27
+ )
28
+ },
29
+ })
30
+
31
+ // POST /api/auth/logout
32
+ export const logout = route({
33
+ path: "/api/auth/logout",
34
+ auth: "public",
35
+ method: "POST",
36
+ async handler() {
37
+ const ctx = contextStore.getStore()!
38
+ const req = ctx.request
39
+
40
+ if (req) {
41
+ const sid = parseCookie(req.headers.get("Cookie") ?? "", "sid")
42
+ if (sid) {
43
+ const { logout: logoutFn } = await import("../auth/auth")
44
+ const clearCookie = await logoutFn(sid, ctx.db)
45
+ // Return Response directly to control the Set-Cookie header
46
+ return new Response(JSON.stringify({ message: "Logged out" }), {
47
+ status: 200,
48
+ headers: { "Content-Type": "application/json", "Set-Cookie": clearCookie },
49
+ })
50
+ }
51
+ }
52
+
53
+ return response.ok({ message: "Logged out" })
54
+ },
55
+ })
56
+
57
+ // GET /api/auth/csrf
58
+ export const csrf = route({
59
+ path: "/api/auth/csrf",
60
+ auth: "public",
61
+ method: "GET",
62
+ async handler() {
63
+ const ctx = contextStore.getStore()
64
+ return response.ok({ csrfToken: ctx?.session.csrfToken ?? null })
65
+ },
66
+ })
67
+
68
+ // ─── helpers ──────────────────────────────────────────────
69
+
70
+ function parseCookie(header: string, name: string): string | undefined {
71
+ for (const part of header.split(";")) {
72
+ const eq = part.indexOf("=")
73
+ if (eq === -1) continue
74
+ if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim()
75
+ }
76
+ }
@@ -0,0 +1,10 @@
1
+ export {
2
+ route,
3
+ isRouteDefinition,
4
+ inferMethod,
5
+ camelToKebab,
6
+ resolveRoutePath,
7
+ invokeRoute,
8
+ routeRegistry,
9
+ } from "./route"
10
+ export type { RouteConfig, RouteDefinition, AuthMode, HttpMethod, RouteHandler } from "./route"
@@ -0,0 +1,177 @@
1
+ /**
2
+ * /api/resource — auto-generated DocType REST CRUD.
3
+ *
4
+ * One route() export per operation. The server dispatcher selects the right
5
+ * one based on HTTP method and URL shape, then calls invokeRoute() with the
6
+ * extracted path params ({ doctype, name }).
7
+ *
8
+ * All resource endpoints require authentication (auth: "required").
9
+ * For public/guest access to a DocType, write an explicit route() endpoint
10
+ * with auth: "public" in your app's api/ directory.
11
+ */
12
+
13
+ import { contextStore } from "../context"
14
+ import { getDocTypeMeta } from "../doctype/registry"
15
+ import type { Dict, FilterInput, RestConfig } from "../doctype/types"
16
+ import { hasRolePermission } from "../permissions"
17
+ import { response } from "../response"
18
+ import { route } from "./route"
19
+
20
+ // ─── REST flag ────────────────────────────────────────────
21
+
22
+ type RestOp = "list" | "read" | "create" | "update" | "delete" | "submit" | "cancel"
23
+
24
+ function isRestEnabled(doctype: string, op: RestOp): boolean {
25
+ const rest = getDocTypeMeta(doctype)?.schema.rest
26
+ if (!rest) return false
27
+ if (rest === true) return true
28
+ return (rest as RestConfig)[op] === true
29
+ }
30
+
31
+ // ─── Shared access guard ──────────────────────────────────
32
+
33
+ function deny(doctype: string, op: RestOp): Response | null {
34
+ if (!isRestEnabled(doctype, op))
35
+ return response.forbidden(`REST ${op} not enabled for ${doctype}`)
36
+ const ctx = contextStore.getStore()!
37
+ const permAction = op === "list" || op === "read" ? "read" : op === "update" ? "write" : op
38
+ if (!hasRolePermission(doctype, permAction, ctx.user, ctx.db, ctx)) return response.forbidden()
39
+ return null
40
+ }
41
+
42
+ // ─── GET /api/resource/:doctype ───────────────────────────
43
+
44
+ export const list = route({
45
+ path: "/api/resource/:doctype",
46
+ auth: "required",
47
+ method: "GET",
48
+ async handler({
49
+ doctype,
50
+ filters,
51
+ fields,
52
+ order_by,
53
+ limit,
54
+ offset,
55
+ }: {
56
+ doctype: string
57
+ filters?: string
58
+ fields?: string
59
+ order_by?: string
60
+ limit?: string
61
+ offset?: string
62
+ }) {
63
+ const err = deny(doctype, "list")
64
+ if (err) return err
65
+
66
+ const ctx = contextStore.getStore()!
67
+ const rows = await ctx.getList(doctype, {
68
+ filters: filters ? (JSON.parse(filters) as FilterInput) : undefined,
69
+ fields: fields ? (JSON.parse(fields) as string[]) : ["name"],
70
+ orderBy: order_by,
71
+ limit: limit ? parseInt(limit) : 20,
72
+ offset: offset ? parseInt(offset) : 0,
73
+ })
74
+ return response.ok(rows)
75
+ },
76
+ })
77
+
78
+ // ─── GET /api/resource/:doctype/:name ─────────────────────
79
+
80
+ export const read = route({
81
+ path: "/api/resource/:doctype/:name",
82
+ auth: "required",
83
+ method: "GET",
84
+ async handler({ doctype, name }: { doctype: string; name: string }) {
85
+ const err = deny(doctype, "read")
86
+ if (err) return err
87
+
88
+ const doc = await contextStore.getStore()!.getDoc(doctype, name)
89
+ return response.ok(doc.asDict())
90
+ },
91
+ })
92
+
93
+ // ─── POST /api/resource/:doctype ──────────────────────────
94
+
95
+ export const create = route({
96
+ path: "/api/resource/:doctype",
97
+ auth: "required",
98
+ method: "POST",
99
+ async handler({ doctype, ...body }: { doctype: string } & Dict) {
100
+ const err = deny(doctype, "create")
101
+ if (err) return err
102
+
103
+ const doc = contextStore.getStore()!.newDoc(doctype, body as Dict)
104
+ await doc.insert()
105
+ return response.created(doc.asDict())
106
+ },
107
+ })
108
+
109
+ // ─── PUT /api/resource/:doctype/:name ─────────────────────
110
+
111
+ export const update = route({
112
+ path: "/api/resource/:doctype/:name",
113
+ auth: "required",
114
+ method: "PUT",
115
+ async handler({ doctype, name, ...body }: { doctype: string; name: string } & Dict) {
116
+ const err = deny(doctype, "update")
117
+ if (err) return err
118
+
119
+ const ctx = contextStore.getStore()!
120
+ const doc = await ctx.getDoc(doctype, name)
121
+ for (const [k, v] of Object.entries(body)) {
122
+ if (k !== "doctype") doc.set(k, v)
123
+ }
124
+ await doc.save()
125
+ return response.ok(doc.asDict())
126
+ },
127
+ })
128
+
129
+ // ─── DELETE /api/resource/:doctype/:name ──────────────────
130
+
131
+ export const remove = route({
132
+ path: "/api/resource/:doctype/:name",
133
+ auth: "required",
134
+ method: "DELETE",
135
+ async handler({ doctype, name }: { doctype: string; name: string }) {
136
+ const err = deny(doctype, "delete")
137
+ if (err) return err
138
+
139
+ await contextStore.getStore()!.deleteDoc(doctype, name)
140
+ return response.ok({ message: "ok" })
141
+ },
142
+ })
143
+
144
+ // ─── POST /api/resource/:doctype/:name/submit ─────────────
145
+
146
+ export const submit = route({
147
+ path: "/api/resource/:doctype/:name/submit",
148
+ auth: "required",
149
+ method: "POST",
150
+ async handler({ doctype, name }: { doctype: string; name: string }) {
151
+ const err = deny(doctype, "submit")
152
+ if (err) return err
153
+
154
+ const doc = await contextStore.getStore()!.getDoc(doctype, name)
155
+ await doc.submit()
156
+ return response.ok(doc.asDict())
157
+ },
158
+ })
159
+
160
+ // ─── POST /api/resource/:doctype/:name/cancel ─────────────
161
+
162
+ export const cancel = route({
163
+ path: "/api/resource/:doctype/:name/cancel",
164
+ auth: "required",
165
+ method: "POST",
166
+ async handler({ doctype, name }: { doctype: string; name: string }) {
167
+ const err = deny(doctype, "cancel")
168
+ if (err) return err
169
+
170
+ const doc = await contextStore.getStore()!.getDoc(doctype, name)
171
+ await doc.cancel()
172
+ return response.ok(doc.asDict())
173
+ },
174
+ })
175
+
176
+ // ─── helpers ──────────────────────────────────────────────
177
+ // resource.ts has no local helpers — all response construction uses frappe.response