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.
- package/README.md +72 -0
- package/package.json +59 -0
- package/src/api/auth.ts +76 -0
- package/src/api/index.ts +10 -0
- package/src/api/resource.ts +177 -0
- package/src/api/route.ts +301 -0
- package/src/app/index.ts +6 -0
- package/src/app/loader.ts +218 -0
- package/src/auth/auth.ts +247 -0
- package/src/auth/index.ts +2 -0
- package/src/cli/args.ts +40 -0
- package/src/cli/bin.ts +12 -0
- package/src/cli/commands/add-api.ts +32 -0
- package/src/cli/commands/add-doctype.ts +43 -0
- package/src/cli/commands/add-page.ts +33 -0
- package/src/cli/commands/add-user.ts +96 -0
- package/src/cli/commands/dev.ts +71 -0
- package/src/cli/commands/drop-site.ts +27 -0
- package/src/cli/commands/init.ts +98 -0
- package/src/cli/commands/migrate.ts +110 -0
- package/src/cli/commands/new-site.ts +61 -0
- package/src/cli/commands/routes.ts +56 -0
- package/src/cli/commands/use.ts +30 -0
- package/src/cli/index.ts +73 -0
- package/src/cli/log.ts +13 -0
- package/src/cli/scaffold/templates.ts +189 -0
- package/src/context.ts +162 -0
- package/src/core/doctype/migration/migration.ts +17 -0
- package/src/core/doctype/role/role.ts +7 -0
- package/src/core/doctype/session/session.ts +16 -0
- package/src/core/doctype/user/user.controller.ts +11 -0
- package/src/core/doctype/user/user.ts +22 -0
- package/src/core/doctype/user_role/user_role.ts +9 -0
- package/src/core/doctypes.ts +25 -0
- package/src/core/index.ts +1 -0
- package/src/database/database.ts +359 -0
- package/src/database/filters.ts +131 -0
- package/src/database/index.ts +30 -0
- package/src/database/query-builder.ts +1118 -0
- package/src/database/schema.ts +188 -0
- package/src/doctype/define.ts +45 -0
- package/src/doctype/discovery.ts +57 -0
- package/src/doctype/field.ts +160 -0
- package/src/doctype/index.ts +20 -0
- package/src/doctype/layout.ts +62 -0
- package/src/doctype/query-builder-stub.ts +16 -0
- package/src/doctype/registry.ts +106 -0
- package/src/doctype/types.ts +407 -0
- package/src/document/document.ts +593 -0
- package/src/document/index.ts +6 -0
- package/src/document/naming.ts +56 -0
- package/src/errors.ts +53 -0
- package/src/frappe.d.ts +128 -0
- package/src/globals.ts +72 -0
- package/src/index.ts +112 -0
- package/src/migrations/index.ts +11 -0
- package/src/migrations/runner.ts +256 -0
- package/src/permissions/index.ts +265 -0
- package/src/response.ts +100 -0
- package/src/server.ts +210 -0
- package/src/site.ts +126 -0
- package/src/ssr/handler.ts +56 -0
- package/src/ssr/index.ts +11 -0
- package/src/ssr/page-loader.ts +200 -0
- package/src/ssr/renderer.ts +94 -0
- 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
|
+
}
|