Integración con Electron IPC
QuickModel proporciona una capa de datos segura y tipada para aplicaciones Electron. Gestiona la serialización en el límite IPC, elimina campos no confiables del renderer, valida en el proceso main antes de escribir en disco, y hace seguimiento de cambios no guardados en el renderer.
Arquitectura
Renderer (no confiable) Main (confiable)
DTO QModel ──► $qSerialize() ──► IPC ──► new Dto(payload)
$qCheckRules()
persistir en disco/DBLímite IPC — $qSerialize() / new Dto()
import { QModel, Quick, QRule, QField } from 'quickmodel';
import { qCheckRules } from 'quickmodel/forms';
interface IUserPrefs {
theme: string;
language: string;
fontSize: number;
notifications: boolean;
autoSave: boolean;
}
@Quick(
{
theme: 'string',
language: 'string',
fontSize: 'number',
notifications: 'boolean',
autoSave: 'boolean',
},
{ unknownPropertyPolicy: 'strip', coercionStrategy: 'loose' }
)
class UserPrefsDto extends QModel<IUserPrefs> {
@QField({ label: 'Tema', required: true })
@QRule(
(val: string) => ['light', 'dark', 'system'].includes(val),
'Tema inválido'
)
declare theme: string;
@QField({ label: 'Idioma', required: true })
declare language: string;
@QField({ label: 'Tamaño de fuente' })
@QRule((val: number) => val >= 10 && val <= 32, 'Tamaño fuera de rango')
declare fontSize: number;
@QField({ label: 'Notificaciones', widget: 'checkbox' })
declare notifications: boolean;
@QField({ label: 'Auto guardado', widget: 'checkbox' })
declare autoSave: boolean;
}
// ── Proceso renderer (preload/renderer.ts) ──────────────────────────────────
const dto = new UserPrefsDto(currentPrefs);
// Enviar JSON limpio por IPC:
await window.electron.savePrefs(dto.$qSerialize());
// ── Proceso main (main.ts) ──────────────────────────────────────────────────
ipcMain.handle('save-prefs', async (_, payload: unknown) => {
// Reconstruir DTO en main — campos desconocidos eliminados automáticamente
const dto = new UserPrefsDto(payload as IUserPrefs);
const { valid, errors } = qCheckRules(dto);
if (!valid) {
return { success: false, errors };
}
await fs.writeFile(prefsPath, JSON.stringify(dto.$qToInterface(), null, 2));
return { success: true };
});Seguridad — unknownPropertyPolicy: 'strip'
Usa siempre unknownPropertyPolicy: 'strip' para payloads del renderer. Esto protege contra datos maliciosos inyectados por el canal IPC:
// El renderer envía un payload malicioso:
const maliciousPayload = {
theme: 'dark',
language: 'en',
fontSize: 14,
notifications: true,
autoSave: false,
// Campos inyectados — eliminados automáticamente:
adminOverride: true,
__proto__: { polluted: true },
_internalToken: 'secret',
};
// En main:
const dto = new UserPrefsDto(maliciousPayload as IUserPrefs);
// dto.adminOverride → undefined ✅
// dto._internalToken → undefined ✅
// dto.theme → 'dark' ✅Fechas a través del IPC
Las fechas se convierten a ISO strings durante la serialización JSON. La coerción loose de QuickModel las restaura al otro lado:
@Quick(
{
name: 'string',
path: 'string',
size: 'number',
mimeType: 'string',
modifiedAt: Date,
},
{ unknownPropertyPolicy: 'strip', coercionStrategy: 'loose' }
)
class FileRecordDto extends QModel<IFileRecord> {
declare name: string;
declare path: string;
declare size: number;
declare mimeType: string;
declare modifiedAt: Date; // ← restaurada desde ISO string tras IPC
}
// Main → Renderer:
const dto = new FileRecordDto(fileFromDisk);
ipcRenderer.send('file-loaded', dto.$qSerialize()); // Date → ISO string
// Renderer recibe:
ipcMain.on('file-loaded', (_, payload) => {
const restored = new FileRecordDto(payload); // ISO string → Date ✅
console.log(restored.modifiedAt instanceof Date); // true
});contextBridge.exposeInMainWorld — tipos compartidos
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electron', {
savePrefs: (prefs: IUserPrefs) => ipcRenderer.invoke('save-prefs', prefs),
loadPrefs: (): Promise<IUserPrefs> => ipcRenderer.invoke('load-prefs'),
});
// renderer.ts — validar antes de enviar al main
const dto = new UserPrefsDto(userChanges);
const { valid, errors } = qCheckRules(dto);
if (valid) {
await window.electron.savePrefs(dto.$qSerialize() as IUserPrefs);
} else {
showErrors(errors);
}createMany() para importación de archivos CSV/JSON locales
// Main lee un archivo JSON y envía las filas al renderer
ipcMain.handle('import-files', async () => {
const raw = JSON.parse(
await fs.readFile(importPath, 'utf8')
) as IFileRecord[];
const { instances, errors } = FileRecordDto.createMany(raw);
if (errors.length > 0)
console.warn('Filas inválidas omitidas:', errors.length);
return instances.map((dto) => dto.$qSerialize());
});
// Renderer:
const files = await window.electron.importFiles();
const { instances } = FileRecordDto.createMany(files as IFileRecord[]);
const images = instances.filter((f) => f.isImage); // @QComputed$qIsDirty() — diálogo de confirmación de cambios no guardados
Usa $qIsDirty() en el renderer para avisar al usuario antes de cerrar una ventana con cambios pendientes:
// renderer.ts
const prefs = new UserPrefsDto(await window.electron.loadPrefs());
// El usuario edita un ajuste:
prefs.theme = 'dark';
prefs.fontSize = 18;
console.log(prefs.$qIsDirty()); // true → mostrar diálogo de guardado
window.addEventListener('beforeunload', (e) => {
if (prefs.$qIsDirty()) {
e.preventDefault();
e.returnValue = ''; // Electron muestra "¿Salir sin guardar?"
}
});
// Tras guardar:
const saved = prefs.$qCopy({ theme: 'dark', fontSize: 18 });
console.log(saved.$qIsDirty()); // false — snapshot fresco