
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
A framework-agnostic web library for building robust client-side applications using the Model-View-ViewModel (MVVM) pattern. This library leverages the power of RxJS for reactive data flow and Zod for strong data validation, aiming to simplify state management and API interactions across various frontend frameworks like React, Angular, and Vue.
BaseModel and BaseViewModel for structured application development.RestfulApiModel simplifies CRUD operations with optimistic updates, acting as a local data store, managing loading states, and handling errors automatically.Command utility for encapsulating UI actions, including canExecute and isExecuting states, for clean UI-ViewModel separation. Implements IDisposable.ObservableCollection provides reactive list management, notifying views of granular changes (add, remove, update) for efficient rendering.BaseModel and Command implement IDisposable for proper resource cleanup (e.g., completing RxJS Subjects), helping prevent memory leaks.To install the library, you'll need npm or yarn.
npm install your-library-name rxjs zod
# or
yarn add your-library-name rxjs zod
You'll also need TypeScript configured in your project.
// src/models/user.model.ts
import { BaseModel } from 'mvvm-core/BaseModel'; // Adjust import path
import { z } from 'zod';
export const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(3),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
export type User = z.infer<typeof UserSchema>;
export class UserModel extends BaseModel<User, typeof UserSchema> {
constructor(initialData?: User) {
super({ initialData: initialData || null, schema: UserSchema });
}
}
// src/viewmodels/user.viewmodel.ts
import { BaseViewModel } from 'mvvm-core/BaseViewModel'; // Adjust import path
import { UserModel } from '../models/user.model';
export class UserViewModel extends BaseViewModel<UserModel> {
constructor(model: UserModel) {
super(model);
}
// You can add computed properties (RxJS operators) or methods here
get displayName$() {
return this.data$.pipe(map((user) => (user ? `User: ${user.name}` : 'No user selected')));
}
}
The RestfulApiModel constructor takes an input object which, in addition to baseUrl, endpoint, fetcher, schema, and initialData, also accepts an optional validateSchema boolean (defaults to true). If set to false, Zod schema validation of the API response will be skipped.
// src/models/user.api.model.ts
import { RestfulApiModel, Fetcher } from 'mvvm-core/RestfulApiModel'; // Adjust import path
import { User, UserSchema } from './user.model';
// Example fetcher (can be window.fetch, axios, etc.)
const myCustomFetcher: Fetcher = async (url, options) => {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response; // Return response object, RestfulApiModel will parse JSON
};
export class UserApiModels extends RestfulApiModel<User[], typeof UserSchema> {
constructor() {
// Assuming your API returns an array of users for the base endpoint
super({
baseUrl: 'https://api.yourapp.com',
endpoint: 'users',
fetcher: myCustomFetcher,
schema: z.array(UserSchema), // The Zod schema for data validation (e.g., for an array of Users)
initialData: null, // Optional initial data for the model
validateSchema: true, // Optional: defaults to true. Set to false to skip schema validation.
});
}
}
// Example usage in a component/service
async function loadUsers() {
const userApi = new UserApiModels();
userApi.data$.subscribe((users) => {
console.log('Current users:', users);
});
userApi.isLoading$.subscribe((loading) => {
console.log('Loading users:', loading);
});
userApi.error$.subscribe((error) => {
if (error) console.error('Error loading users:', error);
});
try {
await userApi.fetch(); // Fetches all users
// Create example
const newUserPayload = { name: 'New User', email: 'new@example.com' }; // No ID needed if server generates
const createdUser = await userApi.create(newUserPayload);
if (createdUser) {
console.log('Created User:', createdUser); // Has server-assigned ID
// Update example
const updatedUser = await userApi.update(createdUser.id, { name: 'Updated User Name' });
console.log('Updated User:', updatedUser);
// Delete example
if (updatedUser) {
await userApi.delete(updatedUser.id);
console.log('User deleted successfully.');
}
}
} catch (e) {
// Errors from create, update, delete are re-thrown after setting model.error$
// and reverting optimistic updates.
console.error('API operation failed:', e, userApi.error$.getValue());
} finally {
// It's good practice to dispose of models/commands when they are no longer needed,
// especially if they are long-lived and manage subscriptions.
// userApi.dispose();
}
}
// src/viewmodels/auth.viewmodel.ts
import { Command } from 'mvvm-core/Command'; // Adjust import path
import { BehaviorSubject } from 'rxjs';
export class AuthViewModel {
private _isLoggedIn = new BehaviorSubject(false);
public isLoggedIn$ = this._isLoggedIn.asObservable();
public loginCommand: Command<string, boolean>; // param: password, result: success boolean
constructor() {
this.loginCommand = new Command(
async (password: string) => {
console.log(`Attempting login with password: ${password}`);
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
const success = password === 'secret';
this._isLoggedIn.next(success);
resolve(success);
}, 1000);
});
},
// canExecute$ Observable - login is only possible if not already logged in
this.isLoggedIn$.pipe(map((loggedIn) => !loggedIn)),
);
}
// In a React/Vue/Angular component:
// <button
// onClick={() => authViewModel.loginCommand.execute('myPassword')}
// disabled={!(await authViewModel.loginCommand.canExecute$.pipe(first()).toPromise()) || (await authViewModel.loginCommand.isExecuting$.pipe(first()).toPromise())}
// >
// { (await authViewModel.loginCommand.isExecuting$.pipe(first()).toPromise()) ? 'Logging in...' : 'Login' }
// </button>
}
RestfulApiViewModel with ReactThis example demonstrates how to use RestfulApiModel and RestfulApiViewModel to fetch a list of items from a fake API endpoint and display them in a simple React functional component.
a. Define Item Schema and Type (e.g., src/models/todo.types.ts)
// src/models/todo.types.ts
import { z } from 'zod';
export const TodoSchema = z.object({
id: z.string(),
userId: z.number(),
title: z.string(),
completed: z.boolean(),
});
export type Todo = z.infer<typeof TodoSchema>;
b. Create the RestfulApiModel (e.g., src/models/todo.api.model.ts)
// src/models/todo.api.model.ts
import { RestfulApiModel, Fetcher } from 'your-library-name/models/RestfulApiModel'; // Adjust import path
import { z } from 'zod';
import { TodoSchema, Todo } from './todo.types';
// A mock fetcher for demonstration purposes
const mockTodoFetcher: Fetcher = async (url, options) => {
console.log(`Mock fetcher called: ${options?.method || 'GET'} ${url}`);
// Simulate API delay
await new Promise((resolve) => setTimeout(resolve, 500));
if (url.endsWith('/todos') && options?.method === 'GET') {
const mockTodos: Todo[] = [
{ id: '1', userId: 1, title: 'Fake Todo 1 from API', completed: false },
{ id: '2', userId: 1, title: 'Fake Todo 2 from API', completed: true },
];
return new Response(JSON.stringify(mockTodos), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
// Fallback for other unhandled requests by the mock
return new Response(JSON.stringify({ message: 'Mocked endpoint not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
};
export class TodoListModel extends RestfulApiModel<Todo[], z.ZodArray<typeof TodoSchema>> {
constructor() {
super({
baseUrl: 'https://jsonplaceholder.typicode.com', // Using a real base URL for structure
endpoint: 'todos', // The specific endpoint
fetcher: mockTodoFetcher, // Our mock fetcher
schema: z.array(TodoSchema), // Expect an array of Todos
initialData: null, // No initial data
});
}
}
c. Create the RestfulApiViewModel (e.g., src/viewmodels/todo.viewmodel.ts)
// src/viewmodels/todo.viewmodel.ts
import { RestfulApiViewModel } from 'your-library-name/viewmodels/RestfulApiViewModel'; // Adjust import path
import { TodoListModel } from '../models/todo.api.model';
import { Todo, TodoSchema } from '../models/todo.types';
import { z } from 'zod';
export class TodoViewModel extends RestfulApiViewModel<Todo[], z.ZodArray<typeof TodoSchema>> {
constructor() {
// Create an instance of the model
const todoListModel = new TodoListModel();
super(todoListModel);
}
// You can add specific methods or computed observables related to Todos here if needed
// For example, a command to fetch only completed todos (would require model changes)
// Or an observable that filters/maps the data$
}
d. Create a custom hook for using RxJS Observables in React (e.g., src/hooks/useObservable.ts)
This is a common pattern to bridge RxJS with React's state.
// src/hooks/useObservable.ts
import { useState, useEffect } from 'react';
import { Observable } from 'rxjs';
export function useObservable<T>(observable: Observable<T>, initialValue: T): T {
const [value, setValue] = useState<T>(initialValue);
useEffect(() => {
const subscription = observable.subscribe(setValue);
return () => subscription.unsubscribe();
}, [observable]);
return value;
}
Note: For more complex scenarios or production apps, consider robust libraries like rxjs-hooks.
e. Create the React Component (e.g., src/components/TodoList.tsx)
// src/components/TodoList.tsx
import React, { useMemo, useEffect } from 'react';
import { TodoViewModel } from '../viewmodels/todo.viewmodel'; // Adjust path
import { useObservable } from '../hooks/useObservable'; // Adjust path
import { Todo } from '../models/todo.types'; // Adjust path
const TodoListComponent: React.FC = () => {
// Instantiate the ViewModel. In a real app, you might use a context or DI.
const todoViewModel = useMemo(() => new TodoViewModel(), []);
// Subscribe to the ViewModel's observables
const todos = useObservable(todoViewModel.data$, null);
const isLoading = useObservable(todoViewModel.isLoading$, false);
const error = useObservable(todoViewModel.error$, null);
// Clean up the ViewModel when the component unmounts
useEffect(() => {
return () => {
todoViewModel.dispose();
};
}, [todoViewModel]);
const handleFetchTodos = () => {
todoViewModel.fetchCommand.execute();
};
return (
<div>
<h2>Todo List (React Example)</h2>
<button onClick={handleFetchTodos} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Fetch Todos'}
</button>
{error && <p style={{ color: 'red' }}>Error: {error.message || 'Failed to fetch'}</p>}
{isLoading && !todos && <p>Loading todos...</p>}
{todos && todos.length > 0 && (
<ul>
{todos.map((todo: Todo) => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title} (User ID: {todo.userId})
</li>
))}
</ul>
)}
{!isLoading && todos && todos.length === 0 && <p>No todos found.</p>}
</div>
);
};
export default TodoListComponent;
This example provides a complete, albeit simplified, flow from defining data structures and models to consuming them in a React UI. Remember to adjust import paths (your-library-name) as per your actual library's name and structure.
// src/viewmodels/todos.viewmodel.ts
import { ObservableCollection } from 'mvvm-cores/ObservableCollection'; // Adjust import path
import { map } from 'rxjs/operators';
interface Todo {
id: string;
text: string;
completed: boolean;
}
export class TodosViewModel {
public todos: ObservableCollection<Todo>;
constructor() {
this.todos = new ObservableCollection([
{ id: '1', text: 'Learn MVVM', completed: false },
{ id: '2', text: 'Build awesome app', completed: true },
]);
}
addTodo(text: string) {
const newTodo: Todo = { id: Date.now().toString(), text, completed: false };
this.todos.add(newTodo);
}
toggleTodo(id: string) {
this.todos.update((todo) => todo.id === id, {
...this.todos.toArray().find((t) => t.id === id)!,
completed: !this.todos.toArray().find((t) => t.id === id)!.completed,
});
}
removeCompleted() {
this.todos.remove((todo) => todo.completed);
}
// In a React/Vue/Angular component:
// <ul *ngIf="todos.items$ | async as todoList">
// <li *ngFor="let todo of todoList">
// {{ todo.text }} ({{ todo.completed ? 'Completed' : 'Pending' }})
// </li>
// </ul>
}
FAQs
A minimal MVVM framework for building reactive web applications.
The npm package mvvm-core receives a total of 0 weekly downloads. As such, mvvm-core popularity was classified as not popular.
We found that mvvm-core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.