Skip to content

Latest commit

 

History

History

README.md

NPM Version Package License NPM Downloads Commitizen friendly Coverage Badge

JSON:API Client SDK

Type-safe TypeScript/JavaScript client for consuming JSON:API endpoints built with @klerick/json-api-nestjs.

✨ Features

  • 🎯 Full Type Safety - Complete TypeScript support with type inference from your entities
  • 🔍 Advanced Filtering - Rich query builder with operators (eq, ne, in, like, gt, lt, etc.)
  • 📦 Relationship Handling - Easy include, sparse fieldsets, and relationship management
  • Atomic Operations - Batch multiple operations in a single request with rollback support
  • 📤 Meta Support - Pass additional business logic data with requests (JSON:API spec compliant)
  • 🌐 Multiple HTTP Clients - Works with Axios, Fetch API, and Angular HttpClient
  • 📄 Pagination & Sorting - Built-in support for pagination and multi-field sorting
  • 🔄 Observable or Promise - Choose your async style (RxJS Observable or native Promise)
  • 🔗 Relationship Operations - Post, patch, and delete relationships independently

📚 Table of Contents

Installation

npm install @klerick/json-api-nestjs-sdk

🚀 Quick Start

Basic Setup (Axios)

import { JsonApiJs, adapterForAxios, FilterOperand } from '@klerick/json-api-nestjs-sdk';
import axios from 'axios';
import { Users } from './entities'; // Your entity classes

// 1. Create adapter
const axiosAdapter = adapterForAxios(axios);

// 2. Configure SDK
const jsonSdk = JsonApiJs(
  {
    adapter: axiosAdapter,
    apiHost: 'http://localhost:3000',
    apiPrefix: 'api',
    dateFields: ['createdAt', 'updatedAt'],
    operationUrl: 'operation',
  },
  true // true = return Promises, false = return Observables
);

// 3. Use SDK
// Fetch all users
const users = await jsonSdk.jsonApiSdkService.getAll(Users);

// Fetch with filtering and relationships
const activeUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
  filter: {
    target: {
      isActive: { [FilterOperand.eq]: 'true' }
    }
  },
  include: ['addresses', 'roles']
});

// Get one user
const user = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
  include: ['addresses', 'comments', 'roles', 'manager']
});

// Create a user
const newUser = new Users();
newUser.firstName = 'John';
newUser.lastName = 'Doe';
newUser.login = 'johndoe';
newUser.isActive = true;

const createdUser = await jsonSdk.jsonApiSdkService.postOne(newUser);

// Update a user
createdUser.firstName = 'Jane';
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(createdUser);

// Delete a user
await jsonSdk.jsonApiSdkService.deleteOne(createdUser);

Angular Setup

import {
  provideJsonApi,
  AtomicFactory,
  JsonApiSdkService
} from '@klerick/json-api-nestjs-sdk/ngModule';
import {
  provideHttpClient,
  withFetch,
} from '@angular/common/http';
import { Component, inject } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

// 1. Configure in your main.ts or app.config.ts

// Option A: Direct configuration object
const angularConfig = {
  apiHost: 'http://localhost:3000',
  idKey: 'id',
  apiPrefix: 'api',
  operationUrl: 'operation',
  dateFields: ['createdAt', 'updatedAt']
};

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withFetch()),
    provideJsonApi(angularConfig)
  ],
}).catch((err) => console.error(err));

// Option B: Factory function (useful for dynamic configuration)
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withFetch()),
    provideJsonApi(() => {
      const env = inject(EnvironmentService);
      return {
        apiHost: env.apiUrl,
        idKey: 'id',
        apiPrefix: 'api',
        operationUrl: 'operation',
        dateFields: ['createdAt', 'updatedAt']
      };
    })
  ],
}).catch((err) => console.error(err));

// 2. Use in your components
@Component({
  standalone: true,
  selector: 'app-users',
  templateUrl: './users.component.html',
})
export class UsersComponent {
  private jsonApiService = inject(JsonApiSdkService);
  private atomicFactory = inject(AtomicFactory);

  async loadUsers() {
    const users = await this.jsonApiService.getAll(Users, {
      include: ['addresses']
    });
    return users;
  }

  async createMultipleResources() {
    const result = await this.atomicFactory()
      .postOne(newUser)
      .postOne(newAddress)
      .run();
  }
}

⚙️ Configuration

JsonConfig Type

type JsonSdkConfig = {
  apiHost: string;          // Base URL of your API (e.g., 'http://localhost:3000')
  apiPrefix?: string;       // API prefix (e.g., 'api' -> '/api/users')
  idKey?: string;           // Name of ID field (default: 'id')
  idIsNumber?: boolean;     // Parse IDs as numbers (default: false)
  operationUrl?: string;    // URL path for atomic operations (default: 'operation')
  dateFields?: string[];    // Fields to convert to Date objects (e.g., ['createdAt', 'updatedAt'])
}

type JsonConfig = JsonSdkConfig & {
  adapter?: HttpInnerClient; // HTTP client adapter (default: fetch)
}

// Angular: provideJsonApi accepts config or factory function
type JsonSdkConfigFactory = () => JsonSdkConfig;
type JsonSdkConfigOrFactory = JsonSdkConfig | JsonSdkConfigFactory;

HTTP Adapters

Axios Adapter:

import { adapterForAxios } from '@klerick/json-api-nestjs-sdk';
import axios from 'axios';

const adapter = adapterForAxios(axios);

Fetch API (default):

// No adapter needed, fetch is used by default
const jsonSdk = JsonApiJs({
  apiHost: 'http://localhost:3000',
  apiPrefix: 'api',
}, true);

Custom Adapter:

See HttpInnerClient interface for implementation details.


📖 API Methods

Fetching Resources

getAll(Entity, options?)

Fetch all resources with optional filtering, sorting, and relationships.

import { FilterOperand } from '@klerick/json-api-nestjs-sdk';

// Fetch all users
const users = await jsonSdk.jsonApiSdkService.getAll(Users);

// With filtering
const activeUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
  filter: {
    target: {
      isActive: { [FilterOperand.eq]: 'true' },
      id: { [FilterOperand.in]: ['1', '2', '3'] }
    }
  },
  include: ['addresses', 'roles']
});

// Filter by relationship
const usersWithRoles = await jsonSdk.jsonApiSdkService.getAll(Users, {
  filter: {
    target: {
      id: { [FilterOperand.in]: ['1', '2'] }
    },
    roles: {
      name: { [FilterOperand.eq]: 'admin' }
    }
  },
  include: ['roles']
});

getList(Entity, options)

Fetch resources with pagination (returns paginated results).

const firstPage = await jsonSdk.jsonApiSdkService.getList(Users, {
  page: {
    number: 1,
    size: 10
  },
  sort: {
    target: {
      id: 'ASC'
    }
  }
});

const secondPage = await jsonSdk.jsonApiSdkService.getList(Users, {
  page: {
    number: 2,
    size: 10
  },
  sort: {
    target: {
      createdAt: 'DESC'
    }
  }
});

getOne(Entity, id, options?)

Fetch a single resource by ID.

// Simple fetch
const user = await jsonSdk.jsonApiSdkService.getOne(Users, '1');

// With relationships
const userWithRelations = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
  include: ['addresses', 'comments', 'roles', 'manager']
});

// With sparse fieldsets
const userPartial = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
  fields: {
    users: ['firstName', 'lastName', 'email']
  }
});

Creating Resources

postOne(entity, options?)

Create a new resource.

// Simple create
const newUser = new Users();
newUser.firstName = 'John';
newUser.lastName = 'Doe';
newUser.login = 'johndoe';
newUser.isActive = true;

const createdUser = await jsonSdk.jsonApiSdkService.postOne(newUser);

// Create with client-generated ID
// Note: Server must have `allowSetId: true` option enabled
const userWithId = new Users();
userWithId.id = 'my-custom-uuid';
userWithId.firstName = 'Jane';
userWithId.lastName = 'Doe';
userWithId.login = 'janedoe';

const createdUserWithId = await jsonSdk.jsonApiSdkService.postOne(userWithId);

// Create with relationships
const newAddress = new Addresses();
newAddress.city = 'New York';
newAddress.state = 'NY';
newAddress.country = 'USA';

const savedAddress = await jsonSdk.jsonApiSdkService.postOne(newAddress);

const user = new Users();
user.firstName = 'Jane';
user.lastName = 'Doe';
user.login = 'janedoe';
user.addresses = savedAddress; // Set relationship

const createdUser = await jsonSdk.jsonApiSdkService.postOne(user);

Updating Resources

patchOne(entity, options?)

Update an existing resource.

// Update attributes
user.firstName = 'Updated Name';
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);

// Update relationships
const newAddress = await jsonSdk.jsonApiSdkService.postOne(addressEntity);
user.addresses = newAddress;

const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);

Deleting Resources

deleteOne(entity)

Delete a resource.

await jsonSdk.jsonApiSdkService.deleteOne(user);

Relationship Operations

deleteRelationships(entity, relationshipName)

Remove relationships without deleting the related resources.

// Remove all roles from user
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'roles');

// Remove manager from user
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'manager');

// Remove all comments from user
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'comments');

🏗️ Working with Plain Objects

In monorepo environments or when sharing types between frontend and backend, you may want to use plain TypeScript types/interfaces instead of classes. The SDK provides tools to work with plain objects while maintaining full type safety.

Using entity() Method

The entity() method creates a properly typed entity instance from a plain object. This is essential when:

  • You share types (not classes) between frontend and backend
  • The SDK needs to identify the resource type at runtime (via constructor.name)
import { JsonApiJs } from '@klerick/json-api-nestjs-sdk';

// Shared type (not a class)
interface User {
  id?: number;
  firstName: string;
  lastName: string;
  login: string;
  manager?: User | null;
}

const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);

// Create entity from plain object - chainable API
const createdUser = await jsonSdk.jsonApiSdkService
  .entity<User>('Users', {
    firstName: 'John',
    lastName: 'Doe',
    login: 'johndoe'
  })
  .postOne();

// Update entity - chainable API
const updatedUser = await jsonSdk.jsonApiSdkService
  .entity<User>('Users', {
    id: 1,
    firstName: 'Jane'
  })
  .patchOne();

// Delete entity - chainable API
await jsonSdk.jsonApiSdkService
  .entity<User>('Users', { id: 1 })
  .deleteOne();

// Work with relationships
const userRelations = await jsonSdk.jsonApiSdkService
  .entity<User>('Users', { id: 1 })
  .getRelationships('manager');

Raw mode - get the entity instance without chaining:

// Get raw entity instance (third argument = true)
const userEntity = jsonSdk.jsonApiSdkService.entity<User>('Users', {
  firstName: 'John',
  lastName: 'Doe',
  login: 'johndoe'
}, true);

// Now use it with standard SDK methods
const created = await jsonSdk.jsonApiSdkService.postOne(userEntity);

Using String Type Names

GET methods also accept string type names instead of classes:

// Using string type name
const users = await jsonSdk.jsonApiSdkService.getAll<User>('Users', {
  include: ['manager']
});

const user = await jsonSdk.jsonApiSdkService.getOne<User>('Users', '1', {
  include: ['manager']
});

const userList = await jsonSdk.jsonApiSdkService.getList<User>('Users', {
  page: { number: 1, size: 10 }
});

🔗 Nullifying Relationships

To clear a relationship (set it to null), use the nullRef() function. This is necessary because the SDK distinguishes between:

  • Missing relationship - not included in the request (no change)
  • Null relationship - explicitly set to null (clear the relationship)
import { JsonApiJs, nullRef } from '@klerick/json-api-nestjs-sdk';

interface User {
  id?: number;
  firstName: string;
  manager?: User | null;
}

const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);

// Clear the manager relationship
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
  id: 1,
  firstName: 'John',
  manager: nullRef()  // This will send { data: null } for the relationship
}, true);

const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
// Result: user.manager is now null

How it works:

  • nullRef() returns a special marker object that TypeScript sees as null
  • At runtime, the SDK detects this marker and generates { data: null } in the JSON:API request body
  • The server then clears the relationship

Without nullRef:

// This won't clear the relationship - it will be ignored
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
  id: 1,
  firstName: 'John',
  // manager is undefined - not included in request
}, true);

With nullRef:

// This explicitly clears the relationship
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
  id: 1,
  firstName: 'John',
  manager: nullRef()  // Generates: relationships: { manager: { data: null } }
}, true);

🔗 Clearing To-Many Relationships

To clear all items from a to-many relationship (ManyToMany, OneToMany), use the emptyArrayRef() function:

import { JsonApiJs, emptyArrayRef } from '@klerick/json-api-nestjs-sdk';

interface User {
  id?: number;
  firstName: string;
  roles?: Role[];
}

const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);

// Clear all roles from user
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
  id: 1,
  firstName: 'John',
  roles: emptyArrayRef()  // This will send { data: [] } for the relationship
}, true);

const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
// Result: user.roles is now an empty array

Why emptyArrayRef is needed:

  • An empty array [] would be treated as an attribute (not a relationship)
  • emptyArrayRef() marks it as a relationship that should be cleared
  • At runtime, the SDK generates { data: [] } in the JSON:API request body

Comparison:

// ❌ This won't work - empty array treated as attribute
const user = { id: 1, roles: [] };

// ✅ This works - explicitly clears the relationship
const user = { id: 1, roles: emptyArrayRef() };
// Generates: relationships: { roles: { data: [] } }

🔍 Query Options

Filtering

Available operators:

enum FilterOperand {
  eq = 'eq',     // Equal
  ne = 'ne',     // Not equal
  in = 'in',     // In array
  nin = 'nin',   // Not in array
  lt = 'lt',     // Less than
  lte = 'lte',   // Less than or equal
  gt = 'gt',     // Greater than
  gte = 'gte',   // Greater than or equal
  like = 'like', // SQL LIKE
  re = 'regexp', // Regular expression
}

Examples:

// Equal
const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
  filter: {
    target: {
      isActive: { [FilterOperand.eq]: 'true' }
    }
  }
});

// Not equal
const inactiveUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
  filter: {
    target: {
      isActive: { [FilterOperand.ne]: 'true' }
    }
  }
});

// In array
const specificUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
  filter: {
    target: {
      id: { [FilterOperand.in]: ['1', '2', '3'] }
    }
  }
});

// LIKE search
const searchUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
  filter: {
    target: {
      login: { [FilterOperand.like]: 'john' }
    }
  }
});

// Check null/not null
const usersWithManager = await jsonSdk.jsonApiSdkService.getAll(Users, {
  filter: {
    target: {
      manager: { [FilterOperand.ne]: null }
    }
  }
});

const usersWithoutManager = await jsonSdk.jsonApiSdkService.getAll(Users, {
  filter: {
    target: {
      manager: { [FilterOperand.eq]: null }
    }
  }
});

Sorting

// Sort by single field
const users = await jsonSdk.jsonApiSdkService.getList(Users, {
  sort: {
    target: {
      id: 'ASC'
    }
  }
});

// Sort by multiple fields
const sortedUsers = await jsonSdk.jsonApiSdkService.getList(Users, {
  sort: {
    target: {
      createdAt: 'DESC',
      lastName: 'ASC'
    }
  }
});

Pagination

const paginatedUsers = await jsonSdk.jsonApiSdkService.getList(Users, {
  page: {
    number: 1,  // Page number (1-indexed)
    size: 20    // Items per page
  }
});

Including Relationships

// Include single relationship
const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
  include: ['addresses']
});

// Include multiple relationships
const usersWithAll = await jsonSdk.jsonApiSdkService.getAll(Users, {
  include: ['addresses', 'roles', 'comments', 'manager']
});

// Include nested relationships
const usersWithNested = await jsonSdk.jsonApiSdkService.getAll(Users, {
  include: ['addresses', 'manager.addresses', 'roles']
});

Sparse Fieldsets

Request only specific fields to reduce payload size.

const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
  fields: {
    users: ['firstName', 'lastName', 'email'],
    addresses: ['city', 'country']
  },
  include: ['addresses']
});

⚡ Atomic Operations

Execute multiple operations in a single HTTP request. All operations succeed or fail together.

Basic Atomic Operation

const newUser = new Users();
newUser.firstName = 'John';
newUser.lastName = 'Doe';
newUser.login = 'johndoe';

const result = await jsonSdk.atomicFactory()
  .postOne(newUser)
  .run();

console.log(result[0]); // Created user

Multiple Operations

// Create multiple related resources
const address = new Addresses();
address.city = 'New York';
address.state = 'NY';
address.country = 'USA';

const role = new Roles();
role.name = 'Admin';
role.key = 'admin';

const user = new Users();
user.firstName = 'Jane';
user.lastName = 'Doe';
user.login = 'janedoe';
user.addresses = address;
user.roles = [role];

const [createdAddress, createdRole, createdUser] = await jsonSdk
  .atomicFactory()
  .postOne(address)
  .postOne(role)
  .postOne(user)
  .run();

Mixed Operations (POST, PATCH, Relationships)

// Create user first
const newUser = new Users();
newUser.firstName = 'John';
newUser.login = 'john';

const [createdUser] = await jsonSdk.atomicFactory()
  .postOne(newUser)
  .run();

// Then update and manage relationships atomically
const patchUser = Object.assign(new Users(), createdUser);
patchUser.firstName = 'John Updated';
patchUser.roles = [role1];

const patchUser2 = Object.assign(new Users(), createdUser);
patchUser2.comments = [comment1];

const patchUser3 = Object.assign(new Users(), createdUser);
patchUser3.comments = [comment2];

const result = await jsonSdk
  .atomicFactory()
  .patchOne(patchUser)                        // Update user attributes and set roles
  .patchOne(patchUser2)                       // Set comments
  .patchRelationships(patchUser2, 'comments') // Replace comments (keep only comment1)
  .postRelationships(patchUser3, 'comments')  // Add comment2 to existing comments
  .run();

// result[0] - updated user
// result[1] - updated user with comments
// result[2] - array of comment IDs after replacement
// result[3] - array of all comment IDs after addition

Using Local Identifiers (lid)

Create and reference resources within the same atomic request using local identifiers (lid). This allows you to establish relationships between resources being created in a single batch operation.

How it works:

  1. Assign a temporary ID to the resource (any unique value - number or string)
  2. Reference this ID in relationships of other resources in the same request
  3. Server automatically replaces temporary IDs with real database IDs

Example - Numeric lid:

const address = new Addresses();
address.city = 'Boston';
address.id = 10000; // Temporary ID (lid)

const user = new Users();
user.firstName = 'Alice';
user.addresses = address; // Reference resource by temporary ID

const [createdAddress, createdUser] = await jsonSdk
  .atomicFactory()
  .postOne(address)  // First operation: create address with lid=10000
  .postOne(user)     // Second operation: reference address via lid
  .run();

// Server assigns real IDs and maintains relationships
console.log(createdAddress.id); // Real ID (e.g., 42)
console.log(createdUser.addresses.id); // Same real ID (42)

Example - UUID lid (for entities with UUID primary keys):

const book1 = new BookList();
book1.id = '550e8400-e29b-41d4-a716-446655440000'; // Temporary UUID (lid)
book1.title = 'TypeScript Handbook';

const book2 = new BookList();
book2.id = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // Another temporary UUID (lid)
book2.title = 'Advanced Node.js';

const user = new Users();
user.firstName = 'John';
user.books = [book1, book2]; // Reference both books by their temporary UUIDs

const [createdBook1, createdBook2, createdUser] = await jsonSdk
  .atomicFactory()
  .postOne(book1)
  .postOne(book2)
  .postOne(user)
  .run();

// All temporary IDs are used as actual IDs (for UUID fields)
// or replaced with real IDs (for autoincrement fields)
console.log(createdBook1.id); // UUID from lid
console.log(createdBook2.id); // UUID from lid
console.log(createdUser.books.map(b => b.id)); // Both book UUIDs

Important notes:

  • Local identifiers (lid) are only valid within a single atomic request
  • The SDK automatically handles lid assignment in the request body
  • For numeric IDs: lid is replaced with the actual database-generated ID
  • For UUID IDs: lid can be used as the actual ID (if server has allowSetId: true)

📤 Meta Support

The SDK supports passing meta objects with requests to send additional business logic data that doesn't belong to the entity itself, according to JSON:API specification.

Regular Operations with Meta

// POST with meta
const newUser = new Users();
newUser.firstName = 'John';
newUser.lastName = 'Doe';
newUser.login = 'johndoe';

const createdUser = await jsonSdk.jsonApiSdkService.postOne(newUser, {
  source: 'import',
  batchId: '12345'
});

// Request body:
{
  "data": {
    "type": "users",
    "attributes": { "firstName": "John", "lastName": "Doe", "login": "johndoe" }
  },
  "meta": {
    "source": "import",
    "batchId": "12345"
  }
}

// PATCH with meta
user.firstName = 'Jane';
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user, {
  updatedBy: 'admin',
  reason: 'name correction'
});

// Relationship operations with meta
await jsonSdk.jsonApiSdkService.postRelationships(user, 'roles', {
  addedBy: 'admin',
  timestamp: Date.now()
});

await jsonSdk.jsonApiSdkService.patchRelationships(user, 'roles', {
  source: 'sync',
  syncId: 'abc123'
});

await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'roles', {
  removedBy: 'admin',
  reason: 'access revoked'
});

Important: deleteOne() does NOT support meta because HTTP DELETE for resources has no body according to JSON:API spec. However, deleteRelationships() DOES support meta because relationship deletion endpoints use a request body.

Entity Chain with Meta

// Chain mode - Observable
const user = await jsonSdk.jsonApiSdkService
  .entity('Users', {
    firstName: 'John',
    lastName: 'Doe',
    login: 'johndoe'
  })
  .postOne({ source: 'mobile-app', version: '2.0' });

// Update with meta
await jsonSdk.jsonApiSdkService
  .entity('Users', { id: 1, firstName: 'Jane' })
  .patchOne({ updatedBy: 'user-123' });

// Relationship operations with meta
await jsonSdk.jsonApiSdkService
  .entity('Users', user)
  .patchRelationships('roles', { operation: 'bulk-update' });

Atomic Operations with Meta

Each operation in an atomic request can have its own meta:

const address1 = new Addresses();
address1.id = 999; // Local identifier (lid)
address1.city = 'New York';

const address2 = new Addresses();
address2.id = 1000; // Local identifier (lid)
address2.city = 'Boston';

const user1 = new Users();
user1.firstName = 'Alice';
user1.addresses = address1;

const user2 = new Users();
user2.firstName = 'Bob';
user2.addresses = address2;

const [addr1, addr2, createdUser1, createdUser2] = await jsonSdk
  .atomicFactory()
  .postOne(address1)
  .postOne(address2)
  .postOne(user1, { source: 'import', priority: 'high' })      // User 1 with meta
  .postOne(user2, { source: 'import', priority: 'normal' })    // User 2 with different meta
  .run();

// Request body:
{
  "atomic:operations": [
    {
      "op": "add",
      "ref": { "type": "addresses", "lid": "999" },
      "data": { "type": "addresses", "attributes": { "city": "New York" } }
    },
    {
      "op": "add",
      "ref": { "type": "addresses", "lid": "1000" },
      "data": { "type": "addresses", "attributes": { "city": "Boston" } }
    },
    {
      "op": "add",
      "ref": { "type": "users" },
      "data": {
        "type": "users",
        "attributes": { "firstName": "Alice" },
        "relationships": { "addresses": { "data": { "type": "addresses", "id": "999" } } }
      },
      "meta": { "source": "import", "priority": "high" }  // Meta for operation 3
    },
    {
      "op": "add",
      "ref": { "type": "users" },
      "data": {
        "type": "users",
        "attributes": { "firstName": "Bob" },
        "relationships": { "addresses": { "data": { "type": "addresses", "id": "1000" } } }
      },
      "meta": { "source": "import", "priority": "normal" }  // Meta for operation 4
    }
  ]
}

Meta with Entity Chain in Atomic Operations

const addressEntity = jsonSdk.jsonApiSdkService.entity('Addresses', {
  id: 999,
  city: 'New York'
}, true);

const userEntity = jsonSdk.jsonApiSdkService.entity('Users', {
  firstName: 'Alice',
  addresses: jsonSdk.jsonApiSdkService.entity('Addresses', { id: 999 }, true)
}, true);

const [createdAddress, createdUser] = await jsonSdk
  .atomicFactory()
  .postOne(addressEntity)
  .postOne(userEntity, { source: 'import', batchId: '12345' })
  .run();

Operations WITHOUT Meta Support

The following operations do NOT accept meta parameter:

// ❌ deleteOne - HTTP DELETE has no body
await jsonSdk.jsonApiSdkService.deleteOne(user);
// Cannot pass meta here

// ❌ deleteOne in atomic operations
await jsonSdk.atomicFactory()
  .deleteOne(user)  // No meta parameter
  .run();

// ✅ But deleteRelationships DOES support meta (has body)
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'roles', {
  removedBy: 'admin'
});

💡 Examples

For comprehensive real-world examples, see the E2E test suite:


📝 License

MIT


🔗 Related Packages