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,593 @@
1
+ /**
2
+ * Document class — runtime instance of a DocType row.
3
+ * Handles CRUD, lifecycle hooks, child tables, and computed fields.
4
+ */
5
+
6
+ import type { FrappeDatabase } from "../database/database"
7
+ import { getDocTypeMeta, getController, toTableName } from "../doctype/registry"
8
+ import type {
9
+ Dict,
10
+ DocTypeDefinition,
11
+ ControllerHooks,
12
+ DocumentLike,
13
+ FilterInput,
14
+ } from "../doctype/types"
15
+ import {
16
+ DoesNotExistError,
17
+ DuplicateEntryError,
18
+ TimestampMismatchError,
19
+ ValidationError,
20
+ } from "../errors"
21
+ import { generateName } from "./naming"
22
+
23
+ // ─── Standard field names (set directly on the instance) ──
24
+
25
+ const STANDARD_PROPS = new Set([
26
+ "name",
27
+ "doctype",
28
+ "owner",
29
+ "creation",
30
+ "modified",
31
+ "modifiedBy",
32
+ "docstatus",
33
+ "idx",
34
+ "parent",
35
+ "parentType",
36
+ "parentField",
37
+ ])
38
+
39
+ // ─── Document ─────────────────────────────────────────────
40
+
41
+ export class Document implements DocumentLike {
42
+ // Identity
43
+ name!: string
44
+ readonly doctype: string
45
+
46
+ // Standard fields (auto-managed)
47
+ owner: string = ""
48
+ creation: string = ""
49
+ modified: string = ""
50
+ modifiedBy: string = ""
51
+ docstatus: 0 | 1 | 2 = 0
52
+ idx: number = 0
53
+
54
+ // Child table fields (only present on child documents)
55
+ parent?: string
56
+ parentType?: string
57
+ parentField?: string
58
+
59
+ // ── Internal state (prefixed to avoid field name conflicts) ──
60
+ _isNew: boolean = true
61
+ _originalValues: Dict = {}
62
+ _db!: FrappeDatabase
63
+ _user: string = "Guest"
64
+ _meta!: DocTypeDefinition
65
+ _controller?: ControllerHooks
66
+ _dynFields: Dict = {}
67
+
68
+ constructor(doctype: string) {
69
+ this.doctype = doctype
70
+ }
71
+
72
+ // ── Initialization ─────────────────────────────────────
73
+
74
+ _init(db: FrappeDatabase, user: string): void {
75
+ this._db = db
76
+ this._user = user
77
+ this._meta = getDocTypeMeta(this.doctype)!
78
+ this._controller = getController(this.doctype)
79
+ }
80
+
81
+ _loadValues(data: Dict): void {
82
+ for (const [key, value] of Object.entries(data)) {
83
+ this.set(key, value)
84
+ }
85
+ }
86
+
87
+ _snapshotOriginal(): void {
88
+ this._originalValues = this.asDict()
89
+ this._isNew = false
90
+ }
91
+
92
+ // ── State ──────────────────────────────────────────────
93
+
94
+ isNew(): boolean {
95
+ return this._isNew
96
+ }
97
+
98
+ isModified(field?: string): boolean {
99
+ if (this._isNew) return true
100
+ if (field) return this.get(field) !== this._originalValues[field]
101
+ const current = this.asDict()
102
+ return Object.keys(current).some(
103
+ (k) => !k.startsWith("_") && current[k] !== this._originalValues[k],
104
+ )
105
+ }
106
+
107
+ // ── CRUD ───────────────────────────────────────────────
108
+
109
+ async insert(): Promise<this> {
110
+ const { schema } = this._meta
111
+ const hooks = this._controller
112
+ const now = new Date().toISOString()
113
+
114
+ await hooks?.beforeInsert?.(this)
115
+ this._setDefaults()
116
+
117
+ this.owner = this._user
118
+ this.creation = now
119
+ this.modified = now
120
+ this.modifiedBy = this._user
121
+
122
+ if (!this.name) {
123
+ this.name = generateName(
124
+ schema.naming ?? "autoincrement",
125
+ this.doctype,
126
+ this.asDict(),
127
+ this._db,
128
+ )
129
+ }
130
+
131
+ this._runComputed()
132
+ await hooks?.validate?.(this)
133
+
134
+ if (await this._db.exists(this.doctype, this.name)) {
135
+ throw new DuplicateEntryError(`${this.doctype} "${this.name}" already exists`)
136
+ }
137
+
138
+ this._db.insertRow(toTableName(this.doctype), this.getValidDict())
139
+ await this._insertChildren()
140
+
141
+ await hooks?.afterInsert?.(this)
142
+ await hooks?.onUpdate?.(this)
143
+ await hooks?.afterSave?.(this)
144
+
145
+ this._snapshotOriginal()
146
+ return this
147
+ }
148
+
149
+ async save(): Promise<this> {
150
+ if (this._isNew) return this.insert()
151
+
152
+ const hooks = this._controller
153
+ const now = new Date().toISOString()
154
+
155
+ this.modified = now
156
+ this.modifiedBy = this._user
157
+
158
+ // Optimistic locking
159
+ const dbModified = await this._db.getValue(this.doctype, this.name, "modified")
160
+ if (
161
+ dbModified &&
162
+ this._originalValues["modified"] &&
163
+ dbModified !== this._originalValues["modified"]
164
+ ) {
165
+ throw new TimestampMismatchError(
166
+ `${this.doctype} "${this.name}" was modified by another user`,
167
+ )
168
+ }
169
+
170
+ await hooks?.beforeSave?.(this)
171
+ this._setDefaults()
172
+ this._runComputed()
173
+ await hooks?.validate?.(this)
174
+
175
+ this._db.updateRow(toTableName(this.doctype), this.name, this.getValidDict())
176
+ await this._updateChildren()
177
+
178
+ await hooks?.onUpdate?.(this)
179
+ await hooks?.afterSave?.(this)
180
+
181
+ this._snapshotOriginal()
182
+ return this
183
+ }
184
+
185
+ async submit(): Promise<this> {
186
+ if (!this._meta.schema.isSubmittable) {
187
+ throw new ValidationError(`${this.doctype} is not submittable`)
188
+ }
189
+
190
+ const hooks = this._controller
191
+ await hooks?.beforeSubmit?.(this)
192
+
193
+ this.docstatus = 1
194
+ this._runComputed()
195
+ await hooks?.validate?.(this)
196
+
197
+ const now = new Date().toISOString()
198
+ this.modified = now
199
+ this.modifiedBy = this._user
200
+ this._db.updateRow(toTableName(this.doctype), this.name, this.getValidDict())
201
+ await this._updateChildren()
202
+
203
+ await hooks?.onSubmit?.(this)
204
+ await hooks?.afterSave?.(this)
205
+
206
+ this._snapshotOriginal()
207
+ return this
208
+ }
209
+
210
+ async cancel(): Promise<this> {
211
+ if (!this._meta.schema.isSubmittable) {
212
+ throw new ValidationError(`${this.doctype} is not submittable`)
213
+ }
214
+
215
+ const hooks = this._controller
216
+ await hooks?.beforeCancel?.(this)
217
+
218
+ this.docstatus = 2
219
+
220
+ const now = new Date().toISOString()
221
+ this.modified = now
222
+ this.modifiedBy = this._user
223
+ this._db.updateRow(toTableName(this.doctype), this.name, this.getValidDict())
224
+
225
+ await hooks?.onCancel?.(this)
226
+ await hooks?.afterSave?.(this)
227
+
228
+ this._snapshotOriginal()
229
+ return this
230
+ }
231
+
232
+ async delete(): Promise<void> {
233
+ const hooks = this._controller
234
+ await hooks?.onTrash?.(this)
235
+ await this._deleteChildren()
236
+ this._db.deleteRow(toTableName(this.doctype), this.name)
237
+ }
238
+
239
+ async reload(): Promise<this> {
240
+ const row = this._db.getRow(toTableName(this.doctype), this.name)
241
+ if (!row) throw new DoesNotExistError(`${this.doctype} "${this.name}" does not exist`)
242
+
243
+ this._dynFields = {}
244
+ this._loadValues(row)
245
+ await this._loadChildren()
246
+ await this._controller?.onLoad?.(this)
247
+
248
+ this._snapshotOriginal()
249
+ return this
250
+ }
251
+
252
+ // ── Field access ────────────────────────────────────────
253
+
254
+ get(field: string): unknown {
255
+ if (STANDARD_PROPS.has(field)) return (this as Record<string, unknown>)[field]
256
+ return this._dynFields[field]
257
+ }
258
+
259
+ set(field: string, value: unknown): void {
260
+ if (STANDARD_PROPS.has(field)) {
261
+ ;(this as Record<string, unknown>)[field] = value
262
+ } else {
263
+ this._dynFields[field] = value
264
+ }
265
+ }
266
+
267
+ async getDbValue(field: string): Promise<unknown> {
268
+ if (this._isNew) return undefined
269
+ return this._db.getValue(this.doctype, this.name, field)
270
+ }
271
+
272
+ // ── Child table helpers ─────────────────────────────────
273
+
274
+ append(fieldName: string, data?: Dict): Document {
275
+ const fieldDef = this._meta.schema.fields[fieldName]
276
+ if (!fieldDef || fieldDef.type !== "Table") {
277
+ throw new ValidationError(`"${fieldName}" is not a table field on ${this.doctype}`)
278
+ }
279
+
280
+ const children = (this.get(fieldName) as Document[] | undefined) ?? []
281
+
282
+ const child = new Document(fieldDef.childDocType!)
283
+ child._init(this._db, this._user)
284
+ child.parent = this.name
285
+ child.parentType = this.doctype
286
+ child.parentField = fieldName
287
+ child.idx = children.length + 1
288
+ if (data) child._loadValues(data)
289
+
290
+ children.push(child)
291
+ this.set(fieldName, children)
292
+ return child
293
+ }
294
+
295
+ remove(fieldName: string, row: DocumentLike): void {
296
+ const children = this.get(fieldName) as Document[] | undefined
297
+ if (!children) return
298
+
299
+ const idx = children.indexOf(row as Document)
300
+ if (idx !== -1) {
301
+ children.splice(idx, 1)
302
+ children.forEach((child, i) => {
303
+ child.idx = i + 1
304
+ })
305
+ }
306
+ }
307
+
308
+ async addComment(_text: string, _commentType?: string): Promise<void> {
309
+ // Placeholder — will be wired to a Comment DocType in a later layer
310
+ }
311
+
312
+ // ── Serialization ───────────────────────────────────────
313
+
314
+ asDict(): Dict {
315
+ const dict: Dict = {
316
+ name: this.name,
317
+ doctype: this.doctype,
318
+ owner: this.owner,
319
+ creation: this.creation,
320
+ modified: this.modified,
321
+ modifiedBy: this.modifiedBy,
322
+ docstatus: this.docstatus,
323
+ idx: this.idx,
324
+ }
325
+
326
+ if (this.parent !== undefined) dict["parent"] = this.parent
327
+ if (this.parentType !== undefined) dict["parentType"] = this.parentType
328
+ if (this.parentField !== undefined) dict["parentField"] = this.parentField
329
+
330
+ for (const [fieldName, fieldDef] of Object.entries(this._meta?.schema.fields ?? {})) {
331
+ const val = this.get(fieldName)
332
+ if (val === undefined) continue
333
+
334
+ if (fieldDef.type === "Table" && Array.isArray(val)) {
335
+ dict[fieldName] = val.map((child) => (child instanceof Document ? child.asDict() : child))
336
+ } else {
337
+ dict[fieldName] = val
338
+ }
339
+ }
340
+
341
+ return dict
342
+ }
343
+
344
+ getValidDict(): Dict {
345
+ const dict: Dict = {
346
+ name: this.name,
347
+ owner: this.owner,
348
+ creation: this.creation,
349
+ modified: this.modified,
350
+ modifiedBy: this.modifiedBy,
351
+ docstatus: this.docstatus,
352
+ idx: this.idx,
353
+ }
354
+
355
+ if (this.parent !== undefined) dict["parent"] = this.parent
356
+ if (this.parentType !== undefined) dict["parentType"] = this.parentType
357
+ if (this.parentField !== undefined) dict["parentField"] = this.parentField
358
+
359
+ for (const [fieldName, fieldDef] of Object.entries(this._meta?.schema.fields ?? {})) {
360
+ if (fieldDef.type === "Table") continue
361
+ const val = this.get(fieldName)
362
+ if (val !== undefined) dict[fieldName] = val
363
+ }
364
+
365
+ return dict
366
+ }
367
+
368
+ // ── Private helpers ─────────────────────────────────────
369
+
370
+ private _setDefaults(): void {
371
+ for (const [fieldName, fieldDef] of Object.entries(this._meta?.schema.fields ?? {})) {
372
+ if (fieldDef.type === "Table") continue
373
+ const current = this.get(fieldName)
374
+ if (current !== undefined && current !== null) continue
375
+ if (fieldDef.default === undefined) continue
376
+
377
+ let defaultValue: unknown = fieldDef.default
378
+ if (defaultValue === "today") {
379
+ defaultValue = new Date().toISOString().split("T")[0]
380
+ } else if (defaultValue === "now") {
381
+ defaultValue = new Date().toISOString()
382
+ } else if (defaultValue === "__user") {
383
+ defaultValue = this._user
384
+ }
385
+
386
+ this.set(fieldName, defaultValue)
387
+ }
388
+ }
389
+
390
+ private _runComputed(): void {
391
+ for (const [fieldName, fieldDef] of Object.entries(this._meta?.schema.fields ?? {})) {
392
+ if (fieldDef.computed) {
393
+ this.set(fieldName, fieldDef.computed(this))
394
+ }
395
+ }
396
+ }
397
+
398
+ private async _insertChildren(): Promise<void> {
399
+ for (const [fieldName, fieldDef] of Object.entries(this._meta?.schema.fields ?? {})) {
400
+ if (fieldDef.type !== "Table") continue
401
+ const children = this.get(fieldName)
402
+ if (!Array.isArray(children)) continue
403
+
404
+ const childTableName = toTableName(fieldDef.childDocType!)
405
+
406
+ for (const child of children) {
407
+ const childDoc = this._toChildDoc(child, fieldDef.childDocType!, fieldName)
408
+ if (!childDoc.name) {
409
+ childDoc.name = generateName("autoincrement", fieldDef.childDocType!, {}, this._db)
410
+ }
411
+ this._db.insertRow(childTableName, childDoc.getValidDict())
412
+ }
413
+ }
414
+ }
415
+
416
+ private async _updateChildren(): Promise<void> {
417
+ for (const [fieldName, fieldDef] of Object.entries(this._meta?.schema.fields ?? {})) {
418
+ if (fieldDef.type !== "Table") continue
419
+ const children = this.get(fieldName)
420
+ if (!Array.isArray(children)) continue
421
+
422
+ const childTableName = toTableName(fieldDef.childDocType!)
423
+ this._db.deleteChildren(childTableName, this.name, fieldName)
424
+
425
+ for (const child of children) {
426
+ const childDoc = this._toChildDoc(child, fieldDef.childDocType!, fieldName)
427
+ if (!childDoc.name) {
428
+ childDoc.name = generateName("autoincrement", fieldDef.childDocType!, {}, this._db)
429
+ }
430
+ if (!childDoc.owner) {
431
+ childDoc.owner = this._user
432
+ childDoc.creation = this.creation
433
+ }
434
+ this._db.insertRow(childTableName, childDoc.getValidDict())
435
+ }
436
+ }
437
+ }
438
+
439
+ private async _deleteChildren(): Promise<void> {
440
+ for (const [fieldName, fieldDef] of Object.entries(this._meta?.schema.fields ?? {})) {
441
+ if (fieldDef.type !== "Table") continue
442
+ this._db.deleteChildren(toTableName(fieldDef.childDocType!), this.name, fieldName)
443
+ }
444
+ }
445
+
446
+ private async _loadChildren(): Promise<void> {
447
+ for (const [fieldName, fieldDef] of Object.entries(this._meta?.schema.fields ?? {})) {
448
+ if (fieldDef.type !== "Table") continue
449
+ const rows = this._db.getChildren(toTableName(fieldDef.childDocType!), this.name, fieldName)
450
+ const children = rows.map((row) => {
451
+ const child = new Document(fieldDef.childDocType!)
452
+ child._init(this._db, this._user)
453
+ child._loadValues(row)
454
+ child._isNew = false
455
+ return child
456
+ })
457
+ this.set(fieldName, children)
458
+ }
459
+ }
460
+
461
+ private _toChildDoc(child: unknown, childDocType: string, fieldName: string): Document {
462
+ if (child instanceof Document) {
463
+ child.parent = this.name
464
+ child.parentType = this.doctype
465
+ child.parentField = fieldName
466
+ child.modified = this.modified
467
+ child.modifiedBy = this._user
468
+ return child
469
+ }
470
+
471
+ // Plain object — wrap it
472
+ const doc = new Document(childDocType)
473
+ doc._init(this._db, this._user)
474
+ doc._loadValues(child as Dict)
475
+ doc.parent = this.name
476
+ doc.parentType = this.doctype
477
+ doc.parentField = fieldName
478
+ doc.owner = this._user
479
+ doc.creation = this.creation
480
+ doc.modified = this.modified
481
+ doc.modifiedBy = this._user
482
+ return doc
483
+ }
484
+ }
485
+
486
+ // ─── Factory functions ────────────────────────────────────
487
+
488
+ export function newDoc(
489
+ doctype: string,
490
+ data: Dict | undefined,
491
+ db: FrappeDatabase,
492
+ user: string,
493
+ ): Document {
494
+ const meta = getDocTypeMeta(doctype)
495
+ if (!meta) throw new ValidationError(`DocType "${doctype}" is not registered`)
496
+
497
+ const doc = new Document(doctype)
498
+ doc._init(db, user)
499
+ if (data) doc._loadValues(data)
500
+ return doc
501
+ }
502
+
503
+ export async function getDoc(
504
+ doctype: string,
505
+ name: string,
506
+ db: FrappeDatabase,
507
+ user: string,
508
+ ): Promise<Document> {
509
+ const meta = getDocTypeMeta(doctype)
510
+ if (!meta) throw new ValidationError(`DocType "${doctype}" is not registered`)
511
+
512
+ const row = db.getRow(toTableName(doctype), name)
513
+ if (!row) throw new DoesNotExistError(`${doctype} "${name}" does not exist`)
514
+
515
+ const doc = new Document(doctype)
516
+ doc._init(db, user)
517
+ doc._loadValues(row)
518
+
519
+ // Load child tables
520
+ for (const [fieldName, fieldDef] of Object.entries(meta.schema.fields)) {
521
+ if (fieldDef.type !== "Table") continue
522
+ const rows = db.getChildren(toTableName(fieldDef.childDocType!), name, fieldName)
523
+ doc.set(
524
+ fieldName,
525
+ rows.map((childRow) => {
526
+ const child = new Document(fieldDef.childDocType!)
527
+ child._init(db, user)
528
+ child._loadValues(childRow)
529
+ child._snapshotOriginal()
530
+ return child
531
+ }),
532
+ )
533
+ }
534
+
535
+ await getController(doctype)?.onLoad?.(doc)
536
+ doc._snapshotOriginal()
537
+ return doc
538
+ }
539
+
540
+ export async function getList(
541
+ doctype: string,
542
+ args: {
543
+ filters?: FilterInput
544
+ fields?: string[]
545
+ orderBy?: string
546
+ limit?: number
547
+ offset?: number
548
+ },
549
+ db: FrappeDatabase,
550
+ ): Promise<Dict[]> {
551
+ return db.getAll(doctype, args)
552
+ }
553
+
554
+ export async function deleteDoc(
555
+ doctype: string,
556
+ name: string,
557
+ db: FrappeDatabase,
558
+ user: string,
559
+ ): Promise<void> {
560
+ const doc = await getDoc(doctype, name, db, user)
561
+ await doc.delete()
562
+ }
563
+
564
+ export async function getSingle(
565
+ doctype: string,
566
+ fieldOrFields: string | string[] | undefined,
567
+ db: FrappeDatabase,
568
+ user: string,
569
+ ): Promise<unknown> {
570
+ const meta = getDocTypeMeta(doctype)
571
+ if (!meta?.schema.isSingle) throw new ValidationError(`"${doctype}" is not a Single DocType`)
572
+
573
+ const row = db.getRow(toTableName(doctype), doctype)
574
+ if (!row) {
575
+ const doc = newDoc(doctype, { name: doctype }, db, user)
576
+ await doc.insert()
577
+ return getSingle(doctype, fieldOrFields, db, user)
578
+ }
579
+
580
+ if (!fieldOrFields) {
581
+ const doc = new Document(doctype)
582
+ doc._init(db, user)
583
+ doc._loadValues(row)
584
+ doc._snapshotOriginal()
585
+ return doc
586
+ }
587
+
588
+ if (typeof fieldOrFields === "string") return row[fieldOrFields]
589
+
590
+ const result: Dict = {}
591
+ for (const f of fieldOrFields) result[f] = row[f]
592
+ return result
593
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Document module — re-exports everything.
3
+ */
4
+
5
+ export { Document, newDoc, getDoc, getList, deleteDoc, getSingle } from "./document"
6
+ export { generateName } from "./naming"
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Name generation strategies for documents.
3
+ */
4
+
5
+ import { randomUUIDv7 } from "bun"
6
+ import { createHash } from "node:crypto"
7
+
8
+ import type { FrappeDatabase } from "../database/database"
9
+ import { toTableName } from "../doctype/registry"
10
+ import type { NamingStrategy, Dict } from "../doctype/types"
11
+
12
+ /**
13
+ * Generate a name for a new document based on the DocType's naming strategy.
14
+ */
15
+ export function generateName(
16
+ strategy: NamingStrategy,
17
+ doctype: string,
18
+ data: Dict,
19
+ db: FrappeDatabase,
20
+ ): string {
21
+ if (strategy === "uuid") {
22
+ return randomUUIDv7()
23
+ }
24
+
25
+ if (strategy === "hash") {
26
+ const hash = createHash("sha256")
27
+ .update(`${doctype}-${Date.now()}-${Math.random()}`)
28
+ .digest("hex")
29
+ return hash.substring(0, 10)
30
+ }
31
+
32
+ if (strategy === "autoincrement") {
33
+ const tableName = toTableName(doctype)
34
+ return String(db.getNextId(tableName))
35
+ }
36
+
37
+ if (strategy === "prompt") {
38
+ const name = data["name"]
39
+ if (!name) throw new Error(`DocType "${doctype}" requires a name (naming: "prompt")`)
40
+ return String(name)
41
+ }
42
+
43
+ if (strategy.startsWith("field:")) {
44
+ const fieldName = strategy.slice(6)
45
+ const value = data[fieldName]
46
+ if (!value) throw new Error(`DocType "${doctype}" naming "field:${fieldName}": field is empty`)
47
+ return String(value)
48
+ }
49
+
50
+ if (strategy.startsWith("series:")) {
51
+ const pattern = strategy.substring(7)
52
+ return db.getNextSeries(pattern)
53
+ }
54
+
55
+ throw new Error(`Unknown naming strategy: ${strategy}`)
56
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Frappe error types that map to HTTP status codes.
3
+ */
4
+
5
+ export class FrappeError extends Error {
6
+ httpStatus: number = 500
7
+ title?: string
8
+
9
+ constructor(message: string, title?: string) {
10
+ super(message)
11
+ this.name = this.constructor.name
12
+ this.title = title
13
+ }
14
+ }
15
+
16
+ export class ValidationError extends FrappeError {
17
+ override httpStatus = 417
18
+ }
19
+
20
+ export class PermissionError extends FrappeError {
21
+ override httpStatus = 403
22
+ }
23
+
24
+ export class AuthenticationError extends FrappeError {
25
+ override httpStatus = 401
26
+ }
27
+
28
+ export class DoesNotExistError extends FrappeError {
29
+ override httpStatus = 404
30
+ }
31
+
32
+ export class DuplicateEntryError extends FrappeError {
33
+ override httpStatus = 409
34
+ }
35
+
36
+ export class TimestampMismatchError extends FrappeError {
37
+ override httpStatus = 409
38
+ }
39
+
40
+ export class RateLimitError extends FrappeError {
41
+ override httpStatus = 429
42
+ }
43
+
44
+ /** Map of error class constructors for use with frappe.throw() */
45
+ export const ErrorTypes = {
46
+ ValidationError,
47
+ PermissionError,
48
+ AuthenticationError,
49
+ DoesNotExistError,
50
+ DuplicateEntryError,
51
+ TimestampMismatchError,
52
+ RateLimitError,
53
+ } as const