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
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Schema management — create and alter tables from DocType definitions.
3
+ */
4
+
5
+ import type { Database as BunSQLite } from "bun:sqlite"
6
+
7
+ import { toTableName, getAllDocTypes, getDocTypeMeta } from "../doctype/registry"
8
+ import type { DocTypeDefinition, FieldType } from "../doctype/types"
9
+
10
+ interface PragmaTableInfoRow {
11
+ cid: number
12
+ name: string
13
+ type: string
14
+ notnull: number
15
+ dflt_value: string | null
16
+ pk: number
17
+ }
18
+
19
+ interface SqliteMasterRow {
20
+ name: string
21
+ }
22
+
23
+ /**
24
+ * Map field type to SQLite column type.
25
+ */
26
+ function sqliteColumnType(fieldType: FieldType): string {
27
+ switch (fieldType) {
28
+ case "Int":
29
+ case "Check":
30
+ case "Rating":
31
+ return "INTEGER"
32
+ case "Float":
33
+ case "Currency":
34
+ case "Percent":
35
+ return "REAL"
36
+ case "Table":
37
+ return "" // no column — child tables are separate
38
+ default:
39
+ return "TEXT"
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Build CREATE TABLE SQL for a DocType.
45
+ */
46
+ export function buildCreateTableSQL(def: DocTypeDefinition): string[] {
47
+ const tableName = toTableName(def.name)
48
+ const statements: string[] = []
49
+
50
+ const columns: string[] = [
51
+ `"name" VARCHAR(140) PRIMARY KEY`,
52
+ `"creation" TEXT`,
53
+ `"modified" TEXT`,
54
+ `"modifiedBy" VARCHAR(140)`,
55
+ `"owner" VARCHAR(140)`,
56
+ `"docstatus" INTEGER DEFAULT 0`,
57
+ `"idx" INTEGER DEFAULT 0`,
58
+ ]
59
+
60
+ if (def.schema.isChild) {
61
+ columns.push(`"parent" VARCHAR(140)`, `"parentType" VARCHAR(140)`, `"parentField" VARCHAR(140)`)
62
+ }
63
+
64
+ for (const [fieldName, fieldDef] of Object.entries(def.schema.fields)) {
65
+ if (fieldDef.type === "Table") continue
66
+
67
+ const colType = sqliteColumnType(fieldDef.type)
68
+ let colDef = `"${fieldName}" ${colType}`
69
+
70
+ if (fieldDef.default !== undefined) {
71
+ if (typeof fieldDef.default === "string") {
72
+ colDef += ` DEFAULT '${fieldDef.default}'`
73
+ } else if (typeof fieldDef.default === "number") {
74
+ colDef += ` DEFAULT ${fieldDef.default}`
75
+ }
76
+ }
77
+
78
+ columns.push(colDef)
79
+ }
80
+
81
+ statements.push(`CREATE TABLE IF NOT EXISTS "${tableName}" (\n ${columns.join(",\n ")}\n)`)
82
+
83
+ // Indexes
84
+ for (const [fieldName, fieldDef] of Object.entries(def.schema.fields)) {
85
+ if (fieldDef.unique) {
86
+ statements.push(
87
+ `CREATE UNIQUE INDEX IF NOT EXISTS "idx_${tableName}_${fieldName}" ON "${tableName}" ("${fieldName}")`,
88
+ )
89
+ } else if (fieldDef.type === "Link" || fieldDef.inStandardFilter) {
90
+ statements.push(
91
+ `CREATE INDEX IF NOT EXISTS "idx_${tableName}_${fieldName}" ON "${tableName}" ("${fieldName}")`,
92
+ )
93
+ }
94
+ }
95
+
96
+ if (def.schema.isChild) {
97
+ statements.push(
98
+ `CREATE INDEX IF NOT EXISTS "idx_${tableName}_parent" ON "${tableName}" ("parent")`,
99
+ )
100
+ }
101
+
102
+ statements.push(
103
+ `CREATE INDEX IF NOT EXISTS "idx_${tableName}_modified" ON "${tableName}" ("modified")`,
104
+ )
105
+
106
+ return statements
107
+ }
108
+
109
+ /**
110
+ * Build ALTER TABLE statements to add missing columns.
111
+ */
112
+ export function buildAlterTableSQL(def: DocTypeDefinition, existingColumns: Set<string>): string[] {
113
+ const tableName = toTableName(def.name)
114
+ const statements: string[] = []
115
+
116
+ for (const [fieldName, fieldDef] of Object.entries(def.schema.fields)) {
117
+ if (fieldDef.type === "Table") continue
118
+ if (existingColumns.has(fieldName)) continue
119
+
120
+ const colType = sqliteColumnType(fieldDef.type)
121
+ let defaultClause = ""
122
+ if (fieldDef.default !== undefined) {
123
+ if (typeof fieldDef.default === "string") {
124
+ defaultClause = ` DEFAULT '${fieldDef.default}'`
125
+ } else if (typeof fieldDef.default === "number") {
126
+ defaultClause = ` DEFAULT ${fieldDef.default}`
127
+ }
128
+ }
129
+
130
+ statements.push(
131
+ `ALTER TABLE "${tableName}" ADD COLUMN "${fieldName}" ${colType}${defaultClause}`,
132
+ )
133
+ }
134
+
135
+ return statements
136
+ }
137
+
138
+ /**
139
+ * Get existing column names for a table.
140
+ */
141
+ export function getExistingColumns(db: BunSQLite, tableName: string): Set<string> {
142
+ const columns = new Set<string>()
143
+ try {
144
+ const rows = db.query<PragmaTableInfoRow, []>(`PRAGMA table_info("${tableName}")`).all()
145
+ for (const row of rows) {
146
+ columns.add(row.name)
147
+ }
148
+ } catch {
149
+ // Table doesn't exist
150
+ }
151
+ return columns
152
+ }
153
+
154
+ /**
155
+ * Check if a table exists.
156
+ */
157
+ export function tableExists(db: BunSQLite, tableName: string): boolean {
158
+ const result = db
159
+ .query<SqliteMasterRow, [string]>(
160
+ `SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
161
+ )
162
+ .get(tableName)
163
+ return result != null
164
+ }
165
+
166
+ /**
167
+ * Sync all registered DocTypes to the database.
168
+ * Creates tables that don't exist, adds columns that are missing.
169
+ */
170
+ export function syncSchema(db: BunSQLite): void {
171
+ for (const dtName of getAllDocTypes()) {
172
+ const def = getDocTypeMeta(dtName)
173
+ if (!def || def.schema.isVirtual) continue
174
+
175
+ const tableName = toTableName(def.name)
176
+
177
+ if (!tableExists(db, tableName)) {
178
+ for (const stmt of buildCreateTableSQL(def)) {
179
+ db.run(stmt)
180
+ }
181
+ } else {
182
+ const existingCols = getExistingColumns(db, tableName)
183
+ for (const stmt of buildAlterTableSQL(def, existingCols)) {
184
+ db.run(stmt)
185
+ }
186
+ }
187
+ }
188
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * doctype() and controller() — the two ambient globals that define a DocType.
3
+ */
4
+
5
+ import { registerDocType, registerController } from "./registry"
6
+ import type {
7
+ DocTypeSchema,
8
+ DocTypeDefinition,
9
+ ControllerHooks,
10
+ ControllerDefinition,
11
+ } from "./types"
12
+
13
+ // ─── doctype() ────────────────────────────────────────────
14
+
15
+ /**
16
+ * Define a DocType schema. Registers it in the global registry and returns
17
+ * the definition. Available as an ambient global — no import needed.
18
+ *
19
+ * export default doctype("ToDo", {
20
+ * fields: { ... },
21
+ * permissions: [...],
22
+ * })
23
+ */
24
+ export function doctype(name: string, schema: DocTypeSchema): DocTypeDefinition {
25
+ const def: DocTypeDefinition = { name, schema }
26
+ if (!schema.naming) def.schema.naming = "autoincrement"
27
+ registerDocType(def)
28
+ return def
29
+ }
30
+
31
+ // ─── controller() ─────────────────────────────────────────
32
+
33
+ /**
34
+ * Define a controller for a DocType. Registers it immediately and returns
35
+ * the definition. Available as an ambient global — no import needed.
36
+ *
37
+ * export default controller("ToDo", {
38
+ * async validate(doc) { ... },
39
+ * permissions: { scope(user) { ... } },
40
+ * })
41
+ */
42
+ export function controller(name: string, hooks: ControllerHooks): ControllerDefinition {
43
+ registerController(name, hooks)
44
+ return { name, hooks }
45
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Scan filesystem for DocType definitions and controllers.
3
+ */
4
+
5
+ import { existsSync, readdirSync, statSync } from "node:fs"
6
+ import { join } from "node:path"
7
+
8
+ import { registerDocType } from "./registry"
9
+ import type { DocTypeDefinition } from "./types"
10
+
11
+ /**
12
+ * Discover and load all DocTypes from a doctype/ directory.
13
+ * Each subdirectory containing a .ts file (not .controller.ts, not .spec.ts) is a DocType.
14
+ *
15
+ * Schema files must `export default defineDocType(...)`.
16
+ * Controller files must `export default controller(...)`.
17
+ */
18
+ export async function discoverDocTypes(doctypeDir: string): Promise<void> {
19
+ if (!existsSync(doctypeDir)) return
20
+
21
+ const entries = readdirSync(doctypeDir)
22
+
23
+ for (const entry of entries) {
24
+ const entryPath = join(doctypeDir, entry)
25
+ if (!statSync(entryPath).isDirectory()) continue
26
+
27
+ // Look for schema file: {name}.ts (not .controller.ts, not .spec.ts)
28
+ const files = readdirSync(entryPath)
29
+ const schemaFile = files.find(
30
+ (f) =>
31
+ f.endsWith(".ts") &&
32
+ !f.endsWith(".controller.ts") &&
33
+ !f.endsWith(".spec.ts") &&
34
+ !f.endsWith(".d.ts"),
35
+ )
36
+
37
+ if (!schemaFile) continue
38
+
39
+ // Import schema — the default export is the DocTypeDefinition
40
+ const schemaPath = join(entryPath, schemaFile)
41
+ const mod = await import(schemaPath)
42
+ const def: DocTypeDefinition | undefined = mod.default
43
+
44
+ // If the module was previously imported and the DocType is already registered
45
+ // (via the defineDocType side-effect), the def is what was returned.
46
+ // If the registry was cleared, we need to re-register from the returned def.
47
+ if (def?.name && def?.schema) {
48
+ registerDocType(def)
49
+ }
50
+
51
+ // Controller files self-register when imported (controller() registers as a side effect)
52
+ const controllerFile = files.find((f) => f.endsWith(".controller.ts"))
53
+ if (controllerFile) {
54
+ await import(join(entryPath, controllerFile))
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * The `field` factory — creates FieldDefinition objects for use in defineDocType().
3
+ * All methods return pure data, no side effects.
4
+ */
5
+
6
+ import type { FieldDefinition, FieldOptions } from "./types"
7
+
8
+ function makeField(type: FieldDefinition["type"], opts?: FieldOptions): FieldDefinition {
9
+ const def: FieldDefinition = { type }
10
+ if (opts) {
11
+ Object.assign(def, opts)
12
+ }
13
+ return def
14
+ }
15
+
16
+ export const field = {
17
+ // ─── Data Fields ──────────────────────────────────────────
18
+ data(opts?: FieldOptions): FieldDefinition {
19
+ return makeField("Data", opts)
20
+ },
21
+
22
+ int(opts?: FieldOptions): FieldDefinition {
23
+ return makeField("Int", opts)
24
+ },
25
+
26
+ float(opts?: FieldOptions): FieldDefinition {
27
+ return makeField("Float", opts)
28
+ },
29
+
30
+ currency(opts?: FieldOptions): FieldDefinition {
31
+ return makeField("Currency", opts)
32
+ },
33
+
34
+ percent(opts?: FieldOptions): FieldDefinition {
35
+ return makeField("Percent", opts)
36
+ },
37
+
38
+ check(opts?: FieldOptions): FieldDefinition {
39
+ return makeField("Check", opts)
40
+ },
41
+
42
+ rating(opts?: FieldOptions): FieldDefinition {
43
+ return makeField("Rating", opts)
44
+ },
45
+
46
+ // ─── Text Fields ─────────────────────────────────────────
47
+ text(opts?: FieldOptions): FieldDefinition {
48
+ return makeField("Text", opts)
49
+ },
50
+
51
+ smallText(opts?: FieldOptions): FieldDefinition {
52
+ return makeField("SmallText", opts)
53
+ },
54
+
55
+ longText(opts?: FieldOptions): FieldDefinition {
56
+ return makeField("LongText", opts)
57
+ },
58
+
59
+ textEditor(opts?: FieldOptions): FieldDefinition {
60
+ return makeField("TextEditor", opts)
61
+ },
62
+
63
+ markdownEditor(opts?: FieldOptions): FieldDefinition {
64
+ return makeField("MarkdownEditor", opts)
65
+ },
66
+
67
+ code(opts?: FieldOptions): FieldDefinition {
68
+ return makeField("Code", opts)
69
+ },
70
+
71
+ htmlEditor(opts?: FieldOptions): FieldDefinition {
72
+ return makeField("HtmlEditor", opts)
73
+ },
74
+
75
+ json(opts?: FieldOptions): FieldDefinition {
76
+ return makeField("JSON", opts)
77
+ },
78
+
79
+ // ─── Date/Time Fields ────────────────────────────────────
80
+ date(opts?: FieldOptions): FieldDefinition {
81
+ return makeField("Date", opts)
82
+ },
83
+
84
+ datetime(opts?: FieldOptions): FieldDefinition {
85
+ return makeField("Datetime", opts)
86
+ },
87
+
88
+ time(opts?: FieldOptions): FieldDefinition {
89
+ return makeField("Time", opts)
90
+ },
91
+
92
+ duration(opts?: FieldOptions): FieldDefinition {
93
+ return makeField("Duration", opts)
94
+ },
95
+
96
+ // ─── Selection Fields ────────────────────────────────────
97
+ select(options: string[], opts?: FieldOptions): FieldDefinition {
98
+ return { ...makeField("Select", opts), selectOptions: options }
99
+ },
100
+
101
+ link(doctype: string, opts?: FieldOptions): FieldDefinition {
102
+ return { ...makeField("Link", opts), doctype }
103
+ },
104
+
105
+ dynamicLink(linkField: string, opts?: FieldOptions): FieldDefinition {
106
+ return { ...makeField("DynamicLink", opts), linkField }
107
+ },
108
+
109
+ tableMultiSelect(doctype: string, opts?: FieldOptions): FieldDefinition {
110
+ return { ...makeField("TableMultiSelect", opts), doctype }
111
+ },
112
+
113
+ // ─── Table Fields ────────────────────────────────────────
114
+ table(
115
+ doctypeOrFields: string | Record<string, FieldDefinition>,
116
+ opts?: FieldOptions,
117
+ ): FieldDefinition {
118
+ if (typeof doctypeOrFields === "string") {
119
+ return { ...makeField("Table", opts), childDocType: doctypeOrFields }
120
+ }
121
+ return { ...makeField("Table", opts), childFields: doctypeOrFields }
122
+ },
123
+
124
+ // ─── Special Fields ──────────────────────────────────────
125
+ attach(opts?: FieldOptions): FieldDefinition {
126
+ return makeField("Attach", opts)
127
+ },
128
+
129
+ attachImage(opts?: FieldOptions): FieldDefinition {
130
+ return makeField("AttachImage", opts)
131
+ },
132
+
133
+ image(opts?: FieldOptions): FieldDefinition {
134
+ return makeField("Image", opts)
135
+ },
136
+
137
+ color(opts?: FieldOptions): FieldDefinition {
138
+ return makeField("Color", opts)
139
+ },
140
+
141
+ signature(opts?: FieldOptions): FieldDefinition {
142
+ return makeField("Signature", opts)
143
+ },
144
+
145
+ geolocation(opts?: FieldOptions): FieldDefinition {
146
+ return makeField("Geolocation", opts)
147
+ },
148
+
149
+ password(opts?: FieldOptions): FieldDefinition {
150
+ return makeField("Password", opts)
151
+ },
152
+
153
+ readOnly(opts?: FieldOptions): FieldDefinition {
154
+ return makeField("ReadOnly", opts)
155
+ },
156
+
157
+ htmlField(opts?: FieldOptions): FieldDefinition {
158
+ return makeField("HtmlField", opts)
159
+ },
160
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * DocType module — re-exports everything.
3
+ */
4
+
5
+ export * from "./types"
6
+ export { field } from "./field"
7
+ export { section, column, tab } from "./layout"
8
+ export { doctype, controller } from "./define"
9
+ export {
10
+ registerDocType,
11
+ registerController,
12
+ getDocTypeMeta,
13
+ getController,
14
+ getAllDocTypes,
15
+ hasDocType,
16
+ clearRegistry,
17
+ getDocTypeFields,
18
+ toTableName,
19
+ } from "./registry"
20
+ export { discoverDocTypes } from "./discovery"
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Layout helpers: section(), column(), tab()
3
+ */
4
+
5
+ import type { LayoutSection, LayoutColumn, LayoutTab } from "./types"
6
+
7
+ export function column(fields: string[]): LayoutColumn {
8
+ return { _type: "column", fields }
9
+ }
10
+
11
+ export function section(...args: any[]): LayoutSection {
12
+ // Overloads:
13
+ // section(fields: string[])
14
+ // section(title: string, fields: string[])
15
+ // section(columns: LayoutColumn[])
16
+ // section(title: string, columns: LayoutColumn[])
17
+ // section(title: string, opts: object, content: any[])
18
+
19
+ if (args.length === 1) {
20
+ const [arg] = args
21
+ if (Array.isArray(arg) && arg.length > 0 && typeof arg[0] === "string") {
22
+ return { _type: "section", fields: arg }
23
+ }
24
+ if (Array.isArray(arg) && arg.length > 0 && arg[0]?._type === "column") {
25
+ return { _type: "section", columns: arg }
26
+ }
27
+ return { _type: "section", fields: arg ?? [] }
28
+ }
29
+
30
+ if (args.length === 2) {
31
+ const [first, second] = args
32
+ if (typeof first === "string" && Array.isArray(second)) {
33
+ if (second.length > 0 && second[0]?._type === "column") {
34
+ return { _type: "section", title: first, columns: second }
35
+ }
36
+ return { _type: "section", title: first, fields: second }
37
+ }
38
+ }
39
+
40
+ if (args.length === 3) {
41
+ const [title, opts, content] = args
42
+ if (typeof title === "string" && typeof opts === "object" && !Array.isArray(opts)) {
43
+ const result: LayoutSection = {
44
+ _type: "section",
45
+ title,
46
+ ...opts,
47
+ }
48
+ if (Array.isArray(content) && content.length > 0 && content[0]?._type === "column") {
49
+ result.columns = content
50
+ } else {
51
+ result.fields = content
52
+ }
53
+ return result
54
+ }
55
+ }
56
+
57
+ return { _type: "section", fields: [] }
58
+ }
59
+
60
+ export function tab(label: string, sections: LayoutSection[]): LayoutTab {
61
+ return { _type: "tab", label, sections }
62
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Minimal interface stub for QueryBuilder — avoids circular imports
3
+ * between doctype/types.ts and database/query-builder.ts.
4
+ */
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ export interface QueryBuilderLike {
8
+ where(...args: unknown[]): QueryBuilderLike
9
+ select(...args: unknown[]): QueryBuilderLike
10
+ limit(n: number): QueryBuilderLike
11
+ offset(n: number): QueryBuilderLike
12
+ orderBy(...args: unknown[]): QueryBuilderLike
13
+ all<T = Record<string, unknown>>(): Promise<T[]>
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ [key: string]: any
16
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * DocType registry — stores all registered DocType definitions in memory.
3
+ */
4
+
5
+ import type { DocTypeDefinition, ControllerHooks, FieldDefinition } from "./types"
6
+ import { RESERVED_FIELD_NAMES as RESERVED } from "./types"
7
+
8
+ /** Global DocType registry — maps DocType name → definition */
9
+ const registry = new Map<string, DocTypeDefinition>()
10
+
11
+ /** Controller registry — maps DocType name → controller hooks */
12
+ const controllers = new Map<string, ControllerHooks>()
13
+
14
+ /**
15
+ * Convert a DocType name to its table name.
16
+ * "Sales Invoice" → "SalesInvoice"
17
+ * "ToDo" → "ToDo"
18
+ */
19
+ export function toTableName(doctypeName: string): string {
20
+ return doctypeName.replace(/\s+/g, "")
21
+ }
22
+
23
+ /**
24
+ * Register a DocType definition.
25
+ */
26
+ export function registerDocType(def: DocTypeDefinition): void {
27
+ // Validate reserved field names
28
+ for (const fieldName of Object.keys(def.schema.fields)) {
29
+ if (RESERVED.has(fieldName)) {
30
+ throw new Error(
31
+ `DocType "${def.name}": field name "${fieldName}" is reserved. ` +
32
+ `Reserved names: ${[...RESERVED].join(", ")}`,
33
+ )
34
+ }
35
+ }
36
+
37
+ // Register inline child tables (only if not already processed)
38
+ for (const [fieldName, fieldDef] of Object.entries(def.schema.fields)) {
39
+ if (fieldDef.type === "Table" && fieldDef.childFields && !fieldDef.childDocType) {
40
+ const childName = `${def.name} ${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}`
41
+ const childDef: DocTypeDefinition = {
42
+ name: childName,
43
+ schema: {
44
+ isChild: true,
45
+ naming: "autoincrement",
46
+ fields: fieldDef.childFields,
47
+ },
48
+ }
49
+ registry.set(childName, childDef)
50
+ // Update field to reference the generated child DocType
51
+ fieldDef.childDocType = childName
52
+ }
53
+ }
54
+
55
+ registry.set(def.name, def)
56
+ }
57
+
58
+ /**
59
+ * Register a controller for a DocType.
60
+ */
61
+ export function registerController(doctype: string, hooks: ControllerHooks): void {
62
+ controllers.set(doctype, hooks)
63
+ }
64
+
65
+ /**
66
+ * Get a DocType definition by name.
67
+ */
68
+ export function getDocTypeMeta(name: string): DocTypeDefinition | undefined {
69
+ return registry.get(name)
70
+ }
71
+
72
+ /**
73
+ * Get controller hooks for a DocType.
74
+ */
75
+ export function getController(name: string): ControllerHooks | undefined {
76
+ return controllers.get(name)
77
+ }
78
+
79
+ /**
80
+ * Get all registered DocType names.
81
+ */
82
+ export function getAllDocTypes(): string[] {
83
+ return [...registry.keys()]
84
+ }
85
+
86
+ /**
87
+ * Check if a DocType is registered.
88
+ */
89
+ export function hasDocType(name: string): boolean {
90
+ return registry.has(name)
91
+ }
92
+
93
+ /**
94
+ * Clear the registry (for testing).
95
+ */
96
+ export function clearRegistry(): void {
97
+ registry.clear()
98
+ controllers.clear()
99
+ }
100
+
101
+ /**
102
+ * Get all fields for a DocType including standard fields metadata.
103
+ */
104
+ export function getDocTypeFields(name: string): Record<string, FieldDefinition> | undefined {
105
+ return registry.get(name)?.schema.fields
106
+ }