📄 src/models/draft.ts
D-OPEN SOVEREIGN

Documentation & Insights

Creates a draft proxy of any Orm model instance.
The returned proxy has the exact same type signature as the original model.

Setters on the draft proxy are intercepted and buffered locally in a Map.
Getters return the buffered value if it exists, otherwise falling back to the original model.
Call `commitDraft(draft)` to apply the changes to the model.

⚠️ METHOD BYPASS LIMITATION:
The draft proxy intercepts property accessors/getters/setters (e.g. `draft.title`).
Localized model methods (e.g. `draft.getTitle('eng')`) bind directly to the target instance
and bypass the draft proxy buffer. When editing a draft, bind UI forms to property accessors,
not to helper methods.
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 * 
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 * 
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15import type { Orm } from "./orm.ts";
16
17// Symbols for internal draft metadata, avoiding naming collisions with model fields
18const IS_DRAFT = Symbol("isDraft");
19const COMMIT = Symbol("commit");
20const ROLLBACK = Symbol("rollback");
21const IS_DIRTY = Symbol("isDirty");
22const CHANGES = Symbol("changes");
23const TARGET = Symbol("target");
24
25/**
26 * Creates a draft proxy of any Orm model instance.
27 * The returned proxy has the exact same type signature as the original model.
28 * 
29 * Setters on the draft proxy are intercepted and buffered locally in a Map.
30 * Getters return the buffered value if it exists, otherwise falling back to the original model.
31 * Call `commitDraft(draft)` to apply the changes to the model.
32 * 
33 * ⚠️ METHOD BYPASS LIMITATION:
34 * The draft proxy intercepts property accessors/getters/setters (e.g. `draft.title`).
35 * Localized model methods (e.g. `draft.getTitle('eng')`) bind directly to the target instance
36 * and bypass the draft proxy buffer. When editing a draft, bind UI forms to property accessors,
37 * not to helper methods.
38 */
39export function createDraft<T extends Orm>(model: T, onChange?: () => void): T {
40    const changes = new Map<string | symbol, any>();
41
42    const proxy = new Proxy(model, {
43        get(target, prop, receiver) {
44            if (prop === IS_DRAFT) return true;
45            if (prop === TARGET) return target;
46            if (prop === IS_DIRTY) return changes.size > 0;
47            if (prop === CHANGES) return changes;
48
49            if (prop === COMMIT) {
50                return () => {
51                    if (changes.size === 0) return;
52                    // Apply all edits in a single synchronous batch
53                    for (const [key, val] of changes.entries()) {
54                        if (!(key in target)) {
55                            console.warn(
56                                `[models_v2 Draft] Failed to commit property "${String(key)}" to target instance of "${target.constructor.name}". ` +
57                                `This property does not exist on the target (possible typo or misspelled field).`
58                            );
59                            continue;
60                        }
61                        const success = Reflect.set(target, key, val);
62                        if (!success) {
63                            console.warn(
64                                `[models_v2 Draft] Failed to commit property "${String(key)}" to target instance of "${target.constructor.name}". ` +
65                                `This property might be read-only (getter-only relations/IDs).`
66                            );
67                        }
68                    }
69                    changes.clear();
70                    onChange?.();
71                };
72            }
73
74            if (prop === ROLLBACK) {
75                return () => {
76                    if (changes.size > 0) {
77                        changes.clear();
78                        onChange?.();
79                    }
80                };
81            }
82
83            // Return buffered draft edits first
84            if (changes.has(prop)) {
85                return changes.get(prop);
86            }
87
88            // Fallback to the real model
89            const val = Reflect.get(target, prop, receiver);
90            return typeof val === "function" ? val.bind(target) : val;
91        },
92
93        set(target, prop, value, receiver) {
94            // Short-circuit if value is unchanged from target model value
95            const currentVal = Reflect.get(target, prop, receiver);
96            if (currentVal === value) {
97                if (changes.has(prop)) {
98                    changes.delete(prop);
99                    onChange?.();
100                }
101                return true;
102            }
103            // Buffer the edit instead of modifying the underlying target model
104            changes.set(prop, value);
105            onChange?.();
106            return true;
107        }
108    });
109
110    return proxy as T;
111}
112
113// ─── Framework-agnostic helper utilities ──────────────────────────────────────
114
115export const isDraft = (obj: any): boolean => !!obj?.[IS_DRAFT];
116export const isDraftDirty = (obj: any): boolean => !!obj?.[IS_DIRTY];
117export const getDraftChanges = (obj: any): Map<string | symbol, any> | undefined => obj?.[CHANGES];
118export const commitDraft = (obj: any): void => obj?.[COMMIT]?.();
119export const rollbackDraft = (obj: any): void => obj?.[ROLLBACK]?.();
120export const getDraftTarget = <T extends Orm>(obj: any): T | undefined => obj?.[TARGET];
121