Schematize JSONAPI Client Library
npm install @schematize/client-jsonapiA client-side JSON:API library that provides object-relational mapping (ORM) functionality for UML metamodel instances over HTTP. This package extends the Metamodel library with JSON:API persistence capabilities, allowing you to save, fetch, and manage instances with automatic change tracking, lazy loading, and authentication support.
- JSON:API Compliance: Full support for JSON:API specification
- Instance Management: Create, save, delete, and track changes to UML instances
- Lazy Loading: Properties are loaded on-demand with caching and expiration
- Change Tracking: Automatic detection of dirty instances that need saving
- Advanced Link Management: Enhanced association link tracking with automatic cleanup and bidirectional synchronization
- Query Interface: Find instances by ID or with complex filters
- Authentication Support: Built-in OAuth and authorization handling
- Event System: Listen to instance changes, saves, and deletions
- Symbol-based Configuration: Use symbols to configure base URLs and authentication
- Cross-platform: Works in both browser and Node.js environments
- @schematize/refs
- @schematize/instance.js
- @schematize/metamodel
``bash`
npm install @schematize/client-jsonapi
`javascript
import {
attachClassifierMethods,
attachInstanceMethodsAndListeners,
SYMBOL_BASE_URL,
SYMBOL_AUTH
} from '@schematize/client-jsonapi';
import { resurrect } from '@schematize/metamodel';
// Load your UML metamodel
const package = await resurrect(metamodelJson);
// Attach JSON:API methods to classifiers
attachClassifierMethods({ package });
// Attach instance methods and event listeners
attachInstanceMethodsAndListeners();
// Configure base URL for a classifier
UserClass[SYMBOL_BASE_URL] = 'https://api.example.com';
// Configure authentication (optional)
UserClass[SYMBOL_AUTH] = authProvider;
`
`javascript
// Find all users
const users = await UserClass.find({
filter: {
symbol: '&&',
operand: [
{
language: ['SQL'],
body: ['status = "active"']
}
]
},
include: ['posts', 'profile'],
fields: {
'User': ['name', 'email'],
'Post': ['title', 'content']
}
});
// Find user by ID
const user = await UserClass.findById({
__id__: 'user123',
include: ['posts', 'posts.comments']
});
// Find related instances
const userPosts = await user.findRelated({
propertyName: 'posts',
filter: {
language: ['SQL'],
body: ['published = true']
}
});
`
`javascript
// Create new user
const user = new UserClass({
name: 'John Doe',
email: 'john@example.com'
});
// Save to server
await user.save();
// Update existing user
user.name = 'Jane Doe';
await user.save(); // Only sends changed attributes
`
`javascript
// Load property on demand
const posts = await user.$('posts');
// Force reload (bypass cache)
const freshPosts = await user.$('posts', { force: true });
`
The library provides advanced link management with automatic tracking and cleanup:
`javascript
// Commit added links
await user.commitLinksChanged({
associationEnds: ['posts', 'comments']
});
// Commit removed links
await user.commitLinksRemoved({
associationEnds: ['oldPosts']
});
`
Enhanced Features:
- Automatic bidirectional link synchronization
- Support for complex association hierarchies
- Event notifications: Fires jsonapi:change events when links are committed
#### attachClassifierMethods
Attaches find and findById methods to UML classifiers.
Parameters:
- package (Object): UML package containing classifiers
Usage:
`javascript`
attachClassifierMethods({ package });
#### attachInstanceMethodsAndListeners
Attaches instance methods and sets up event listeners for change tracking.
Usage:
`javascript`
attachInstanceMethodsAndListeners();
#### find
Finds instances of a classifier with optional filtering, includes, and field selection.
Parameters:
- __type__ (Object): UML classifier (defaults to this)fields
- (Object): Field selection by type (JSON:API sparse fieldsets)filter
- (Object): Filter conditions using symbol/operand formatinclude
- (Array): Related data to include (JSON:API includes)
Returns:
- Promise: Array of found instances
Usage:
`javascript`
const instances = await UserClass.find({
filter: {
symbol: '&&',
operand: [
{
language: ['SQL'],
body: ['age >= 18']
}
]
},
include: ['posts', 'profile'],
fields: {
'User': ['name', 'email'],
'Post': ['title']
}
});
#### findById
Finds a single instance by its ID.
Parameters:
- __type__ (Object): UML classifier (defaults to this)__id__
- (String): Instance ID to findfields
- (Object): Field selection by typefilter
- (Object): Additional filter conditionsinclude
- (Array): Related data to include
Returns:
- Promise
Usage:
`javascript`
const user = await UserClass.findById({
__id__: 'user123',
include: ['posts', 'posts.comments'],
fields: {
'User': ['name', 'email'],
'Post': ['title', 'content']
}
});
#### save
Saves an instance to the server, handling both creates and updates.
Parameters:
- instance (Object): Instance to save (defaults to this)skipCompositeProperties
- (Boolean): Skip saving composite properties. When true, composite properties (owned relationships) will not be automatically saved during the save operation. Useful when you want to handle composite saves manually or in a different order. Default's to false.skipStructuralValues
- (Boolean): Skip saving structural values. When true, structured values (complex objects that are part of the instance) will not be automatically saved. Use this when you want to handle structured value saves separately. Default's to false.commitLinksChanged
- (Boolean|Object): Commit added links to the server. When true, commits all added association links. When an object, should contain associationEnds array specifying which association ends to commit. Links are committed via POST requests to the relationships endpoint.commitLinksRemoved
- (Boolean|Object): Commit removed links to the server. When true, commits all removed association links. When an object, should contain associationEnds array specifying which association ends to commit. Links are committed via DELETE requests to the relationships endpoint.parametersProvider
- (Function): Optional function that provides additional parameters for cascading save operations. When provided, this function is called for each nested instance being saved (e.g., structured values, composite properties) and should return an object with additional save parameters. Useful for dynamically configuring save behavior for nested instances.
Returns:
- Promise
Usage:
`javascript
// Listen for changes saved
user.on('jsonapi:change', (event) => {
if (event.detail.type === 'saved') {
console.log('Successfully saved with authentication');
}
});
// save
await user.save({
commitLinksChanged: true,
commitLinksRemoved: {
associationEnds: ['oldPosts']
},
parametersProvider: ({ instance }) => ({
// Provide additional parameters for nested saves
commitLinksChanged: true,
commitLinksRemoved: true
})
});
`
Features:
- Change Tracking: Only sends modified attributes
- Cascading Saves: Automatically saves composite properties
- Link Management: Handles association links and structured values
- Event Dispatching: Fires jsonapi:change events with type: 'saved' on successful save
- Authentication: Automatic OAuth token handling
#### delete
Deletes an instance from the server.
Parameters:
- instance (Object): Instance to delete (defaults to this)
Returns:
- Promise
Usage:
`javascript`
await user.delete();
#### $
Lazy loads a property value from the server.
Parameters:
- instance (Object): Instance to load property for (defaults to this)propertyName
- (String): Name of property to loadforce
- (Boolean): Force reload, bypassing cache (default: false)
Returns:
- Promise: Property value
Usage:
`javascript
// Load posts property
const posts = await user.$('posts');
// Force reload
const freshPosts = await user.$('posts', { force: true });
`
Features:
- Caching: Properties are cached for 60 seconds
- Promise-based: Returns promises to prevent duplicate requests
- Collection Handling: Properly manages array properties
- Change Integration: Integrates with change tracking system
#### needsSave
Checks if an instance has unsaved changes and needs to be saved to the server.
Parameters:
- instance (Object): Instance to check (defaults to this)
Returns:
- Boolean: true if the instance needs to be saved, false otherwise
Usage:
`javascript
const user = new UserClass({ name: 'John' });
console.log(user.needsSave()); // true (new instance)
await user.save();
console.log(user.needsSave()); // false (clean instance)
user.name = 'Jane';
console.log(user.needsSave()); // true (has changes)
`
What triggers needsSave:
- New instances: Instances that haven't been saved yet
- Changed attributes: Modified primitive properties or enumerations
- Structured values: Added, changed, or removed composite properties
- Association links: Added or removed association relationships
Features:
- Automatic tracking: No manual intervention required
- Comprehensive detection: Covers all types of changes
- Performance optimized: Quick boolean check without side effects
- Event integration: Triggers jsonapi:change events with type: 'unclean' when changes are detected
#### findRelated
Finds instances related to this instance through a specific property.
Parameters:
- instance (Object): Source instance (defaults to this)__type__
- (Object): Instance type (auto-detected)propertyName
- (String): Property name to followfields
- (Object): Field selection by typefilter
- (Object): Additional filter conditionsinclude
- (Array): Related data to include
Returns:
- Promise
Usage:
`javascript`
const userPosts = await user.findRelated({
propertyName: 'posts',
filter: {
language: ['SQL'],
body: ['published = true']
},
include: ['comments']
});
#### findLinks
Finds association links from an instance.
Parameters:
- instance (Object): Source instance (defaults to this)associationEnds
- (Array): Association end names to search
Returns:
- Promise: Array of association links
Usage:
`javascript`
const links = await user.findLinks({
associationEnds: ['userRoles', 'userGroups']
});
#### commitLinksChanged
Commits added association links to the server.
Parameters:
- instance (Object): Source instance (defaults to this)associationEnds
- (Array): Association end names to commit
Returns:
- Promise: Array of commit promises
Usage:
`javascript`
await user.commitLinksChanged({
associationEnds: ['posts', 'comments']
});
#### commitLinksRemoved
Commits removed association links to the server.
Parameters:
- instance (Object): Source instance (defaults to this)associationEnds
- (Array): Association end names to commit
Returns:
- Promise: Array of commit promises
Usage:
`javascript`
await user.commitLinksRemoved({
associationEnds: ['oldPosts']
});
#### SYMBOL_BASE_URL
Symbol for configuring the base URL for JSON:API endpoints.
Usage:
`javascript`
UserClass[SYMBOL_BASE_URL] = 'https://api.example.com';
#### SYMBOL_AUTH
Symbol for configuring authentication provider.
Usage:
`javascript`
UserClass[SYMBOL_AUTH] = authProvider;
#### SYMBOL_NEW
Internal symbol tracking whether an instance is new (not yet saved).
#### SYMBOL_ATTRIBUTES
Internal symbol storing changed attributes.
#### SYMBOL_LINKS_CHANGED
Internal symbol tracking changed association links.
#### SYMBOL_LINKS_REMOVED
Internal symbol tracking removed association links.
#### SYMBOL_LINK
Internal symbol used to store link references in resource objects.
#### SYMBOL_STRUCTURED_VALUES_CHANGED
Internal symbol tracking changed structured values.
#### SYMBOL_STRUCTURED_VALUES_REMOVED
Internal symbol tracking removed structured values.
#### SYMBOL_OWNER
Internal symbol storing owner information for composite instances.
#### SYMBOL_PROPERTY_EXPIRES
Internal symbol tracking property cache expiration times.
#### SYMBOL_UNLINKED
Internal symbol tracking whether an instance has been unlinked from an association.
#### SYMBOL_DESTROYED
Internal symbol tracking whether an instance has been destroyed.
#### JSONAPI_CONTENT_TYPE
The JSON:API content type header value: application/vnd.api+json
#### EXPIRES
Default cache expiration time in milliseconds (60 seconds by default): 60000
#### OWNING_INSTANCE
Constant string (schematize:__owningInstance__) used to identify the owning instance in resource identifiers for structured values.
#### OWNING_PROPERTY
Constant string (schematize:__owningProperty__) used to identify the owning property in resource identifiers for structured values.
#### OWNING_TYPE
Constant string (schematize:__owningType__) used to identify the owning type in resource identifiers for structured values.
The library provides a comprehensive event system for tracking instance changes:
#### Instance Events
- change: Fired when instance properties change
- jsonapi:change: Fired for JSON:API specific changes
- type: 'unclean': Instance marked as dirtytype: 'saved'
- : Instance successfully savedinstance
- : Fired when new instances are createddestroy
- : Fired when instances are destroyedget
- : Fired when properties are accessed
#### Collection Events
- change: Fired when collection items are added/removed
Usage:
`javascript
// Listen to instance changes
user.on('change', (event) => {
console.log('Instance changed:', event.detail);
});
// Listen to save events
user.on('jsonapi:change', (event) => {
if (event.detail.type === 'saved') {
console.log('Instance saved successfully');
}
});
`
The library automatically tracks changes to instances:
`javascript
const user = new UserClass({ name: 'John' });
console.log(user.needsSave()); // true (new instance)
await user.save(); // Initial save
console.log(user.needsSave()); // false (clean)
user.name = 'Jane'; // Automatically marked as dirty
console.log(user.needsSave()); // true (has changes)
await user.save(); // Only sends the changed 'name' attribute
`
Change Detection:
The needsSave() method checks for:
- New instances that haven't been saved
- Modified primitive attributes and enumerations
- Added, changed, or removed structured values
- Added or removed association links
This enables efficient saving by only sending changed data to the server.
Event Integration:
`javascript`
// Listen for change events
user.on('jsonapi:change', (event) => {
if (event.detail.type === 'unclean') {
console.log('Instance has unsaved changes');
} else if (event.detail.type === 'saved') {
console.log('Instance saved successfully');
}
});
Handles composite (owned) properties with automatic cascading:
`javascript
const user = new UserClass({ name: 'John' });
const profile = new ProfileClass({ bio: 'Developer' });
user.profile = profile; // Composite relationship
await user.save(); // Automatically saves profile too
`
Properties are loaded on-demand and cached:
`javascript
// First access - loads from server
const posts = await user.$('posts');
// Subsequent access - uses cache (within 60 seconds)
const cachedPosts = await user.$('posts');
// Force reload - bypasses cache
const freshPosts = await user.$('posts', { force: true });
`
Automatic OAuth token handling:
`javascript
// Configure authentication
UserClass[SYMBOL_AUTH] = authProvider;
// All requests automatically include authorization headers
const users = await UserClass.find();
`
Major features of JSON:API specification supported:
`javascript
// Sparse fieldsets
const users = await UserClass.find({
fields: {
'User': ['name', 'email'],
'Post': ['title']
}
});
// Includes
const user = await UserClass.findById({
__id__: '123',
include: ['posts', 'posts.comments']
});
// Filtering
const users = await UserClass.find({
filter: {
symbol: '&&',
operand: [
{
language: ['SQL'],
body: ['age >= 18']
}
]
}
});
`
Works in both browser and Node.js environments:
`javascript`
// Automatically detects environment and uses appropriate HTTP client
// Browser: Uses fetch API
// Node.js: Uses https module
const users = await UserClass.find();
The library provides comprehensive error handling with automatic retry for authentication:
`javascript``
try {
const user = await UserClass.findById({ __id__: 'nonexistent' });
} catch (error) {
console.error('Error:', error.message);
// Automatic 401 handling with auth retry
}
- Lazy Loading: Properties are only loaded when accessed
- Change Tracking: Only modified attributes are sent
- Caching: Properties are cached for 60 seconds by default
- Batch Operations: Multiple saves are batched when possible
MIT
Benjamin Bytheway