npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lubusin/wp-frappe-data-store

v0.1.0

Published

A @wordpress/data store for querying and mutating Frappe resources.

Downloads

78

Readme

WP Frappe Data Store

A reusable @wordpress/data store for reading and mutating Frappe DocType resources from WordPress components.

WordPress Plugin Starter Template (wpui-frappe-plugin-starter)

We provide a complete, production-ready standalone WordPress Plugin Starter Template (wpui-frappe-plugin-starter) demonstrating how to integrate @lubusin/wp-frappe-data-store directly into WordPress admin! It features:

  • Full-Screen Admin Interface: Built with @wordpress/boot for native WordPress sidebar navigation across CRM entities.
  • Next-Generation Build Architecture: Packaged and bundled using @wordpress/build with npm workspaces (packages/* and routes/*).
  • Server-Side REST Proxy: Proxies cross-origin API calls to Frappe through a custom WordPress REST API endpoint (/wp-json/frappe-data-store/v1/proxy) using wp_remote_request() to prevent browser CORS restrictions.
  • Zero-Config WordPress Playground: Easily test locally without installing Docker, MySQL, or PHP! Just run npm run playground from the root directory to boot an instant WordPress Playground instance with the plugin pre-activated.

See the WPUI Frappe Plugin Starter Documentation for full architectural details and setup instructions.

Standalone DataViews App Starter Template (wpui-frappe-app-starter)

We provide a complete, standalone WordPress DataViews App Starter Template (wpui-frappe-app-starter) connected to Frappe CRM. Its WordPress-style app shell browses and manages Leads, Deals, Contacts, Organizations, Notes, and Tasks using the app's native DocTypes. The app also loads DocType metadata dynamically from Frappe, so record forms can surface field descriptions and placeholders when available.

See the WPUI Frappe App Starter Documentation for full architectural details and setup instructions.

Install

npm install @lubusin/wp-frappe-data-store @wordpress/data

Register a store

Register it once in your application entry point. The simplest setup connects directly to Frappe's REST API:

import { registerFrappeDataStore } from '@lubusin/wp-frappe-data-store';

const apiKey = 'YOUR_API_KEY';
const apiSecret = 'YOUR_API_SECRET';

export const frappeStore = registerFrappeDataStore({
	storeName: 'my-plugin/frappe',
	baseUrl: 'https://crm.example.com',
	apiPath: '/api/resource',
	headers: {
		Authorization: `token ${apiKey}:${apiSecret}`,
	},
});

Frappe must allow requests from the application's origin. Direct access is useful for local development and trusted internal apps, but a browser bundle cannot keep an API secret: anyone who can load the app can inspect its JavaScript and network requests. Use a restricted Frappe user and token, and do not ship privileged credentials this way.

For session-cookie authentication, omit the Authorization header and set credentials: 'include'. Frappe must then allow the exact frontend origin and credentialed cross-origin requests. The user must already have a valid Frappe session.

WordPress REST proxy pattern

For a production WordPress plugin, proxy requests through WordPress. The browser authenticates to WordPress, while the Frappe URL and API token remain server-side:

WordPress UI → /wp-json/my-plugin/v1/frappe/* → Frappe /api/resource/*

Define the Frappe connection in wp-config.php or provide the values through another server-side secrets mechanism. Do not store the token in JavaScript or commit it to the plugin:

define( 'MY_PLUGIN_FRAPPE_URL', 'https://crm.example.com' );
define( 'MY_PLUGIN_FRAPPE_TOKEN', 'token API_KEY:API_SECRET' );

Register an authenticated proxy route in the plugin. This compact example supports the resource operations used by the datastore:

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'my-plugin/v1',
			'/frappe/(?P<path>api/resource(?:/.*)?)',
			array(
				'methods'             => array( 'GET', 'POST', 'PUT', 'DELETE' ),
				'permission_callback' => function () {
					return current_user_can( 'edit_posts' );
				},
				'callback'            => function ( WP_REST_Request $request ) {
					$path = ltrim( $request['path'], '/' );
					$url  = trailingslashit( MY_PLUGIN_FRAPPE_URL ) . $path;
					$url  = add_query_arg( $request->get_query_params(), $url );

					$response = wp_remote_request(
						$url,
						array(
							'method'  => $request->get_method(),
							'headers' => array(
								'Accept'        => 'application/json',
								'Authorization' => MY_PLUGIN_FRAPPE_TOKEN,
								'Content-Type'  => 'application/json',
							),
							'body'    => $request->get_body(),
							'timeout' => 20,
						)
					);

					if ( is_wp_error( $response ) ) {
						return new WP_Error(
							'frappe_unavailable',
							$response->get_error_message(),
							array( 'status' => 502 )
						);
					}

					$status = wp_remote_retrieve_response_code( $response );
					$body   = json_decode( wp_remote_retrieve_body( $response ), true );

					return new WP_REST_Response( is_array( $body ) ? $body : array(), $status );
				},
			)
		);
	}
);

Choose a capability appropriate to the records exposed by your plugin; edit_posts is only an example. Keeping the upstream host in server configuration—and accepting only the fixed api/resource path—also prevents clients from turning the endpoint into an open proxy.

Expose a WordPress REST nonce to the plugin script when it is enqueued:

wp_localize_script(
	'my-plugin-app',
	'myPluginSettings',
	array(
		'restNonce' => wp_create_nonce( 'wp_rest' ),
	)
);

Then configure the datastore exactly as in the direct connection example, changing baseUrl to the WordPress proxy and replacing the Frappe token with the WordPress REST nonce:

export const frappeStore = registerFrappeDataStore({
	storeName: 'my-plugin/frappe',
	baseUrl: '/wp-json/my-plugin/v1/frappe',
	apiPath: '/api/resource',
	headers: {
		'X-WP-Nonce': window.myPluginSettings.restNonce,
	},
	credentials: 'same-origin',
});

The datastore still generates the normal /api/resource/{doctype}/{name} paths. The only difference is that they are sent to /wp-json/my-plugin/v1/frappe first, where WordPress authenticates the user and forwards them to Frappe. The proxy deliberately preserves Frappe's response body and HTTP status, including the standard { data: ... } payload expected by the datastore.

For a larger integration, move the route into a WP_REST_Controller, add explicit argument schemas, map capabilities per DocType or operation, rate-limit expensive requests, and avoid returning sensitive upstream error details to unauthorized users.

Use from a component

import {
	useFrappeResourceActions,
	useFrappeResourceList,
} from '@lubusin/wp-frappe-data-store';
import { frappeStore } from './store';

export function OpenTasks() {
	const { resources, isResolving, error } = useFrappeResourceList(
		frappeStore,
		'Task',
		{
			fields: ['subject', 'status'],
			filters: [['status', '=', 'Open']],
			orderBy: 'modified desc',
			limit: 20,
		}
	);
	const { saveResource, deleteResource } = useFrappeResourceActions(frappeStore);

	if (isResolving && !resources) return <p>Loading…</p>;
	if (error) return <p>{error.message}</p>;

	return (
		<ul>
			{resources?.map((task) => (
				<li key={task.name}>
					{task.subject}
					<button onClick={() => deleteResource('Task', task.name)}>
						Delete
					</button>
				</li>
			))}
			<button onClick={() => saveResource('Task', { subject: 'New task' })}>
				Add task
			</button>
		</ul>
	);
}

Calling getResource or getResourceList through useSelect triggers their resolvers automatically. The package exports createFrappeDataStore for custom registries, createFrappeRequest for standalone transport setup, and loadDocTypeDefinition for retrieving normalized Frappe DocType metadata.

DocType metadata

Frappe stores field definitions in the DocType DocType. The package can fetch that definition and normalize it into a smaller, UI-oriented shape suitable for generating forms, table columns, filters, and labels.

Use metadata in a component

Pass the registered store and DocType name to useDocTypeDefinition. The hook loads the definition when needed, caches it, and updates the component as the request progresses:

import { useDocTypeDefinition } from '@lubusin/wp-frappe-data-store';
import { frappeStore } from './store';

function LeadForm() {
	const { docTypeDefinition, isResolving, error } =
		useDocTypeDefinition(frappeStore, 'CRM Lead');

	if (isResolving && !docTypeDefinition) {
		return <p>Loading fields…</p>;
	}

	if (error) {
		return <p>Could not load fields.</p>;
	}

	if (!docTypeDefinition) {
		return null;
	}

	return (
		<form>
			{docTypeDefinition.fields.map((field) => (
				<label key={field.id}>
					{field.label}
					<input
						name={field.id}
						required={field.required}
						readOnly={field.readOnly}
						placeholder={field.placeholder}
					/>
				</label>
			))}
		</form>
	);
}

The hook uses the connection already configured on frappeStore; no second transport setup is needed. Behind the scenes it requests /api/resource/DocType/{doctype}. The authenticated Frappe user must have permission to read that DocType definition.

Normalized result

When loading succeeds, useDocTypeDefinition exposes the normalized definition as docTypeDefinition:

const { docTypeDefinition, isResolving, error } =
	useDocTypeDefinition(frappeStore, 'CRM Lead');

Its shape is:

type DocTypeDefinition = {
	name: string;
	titleField: string;
	fields: ResourceFieldDefinition[];
};

type ResourceFieldDefinition = {
	id: string;
	label: string;
	description?: string;
	placeholder?: string;
	type?: 'text' | 'textarea' | 'select' | 'checkbox' | 'date' | 'datetime' | 'number';
	options?: string[];
	required?: boolean;
	readOnly?: boolean;
};

The hook loads this metadata through the configured store and caches it by DocType. During normalization, the package:

  • Uses Frappe's title_field, falling back to the first visible field and then name.
  • Converts Frappe field types into a smaller set of UI-friendly types.
  • Splits newline-delimited Select options into an array.
  • Preserves labels, descriptions, placeholders, required state, and read-only state.
  • Excludes hidden fields and layout-only controls such as section breaks, column breaks, buttons, HTML, and child tables.
  • Generates a human-readable label from the field name when Frappe does not provide one.

For example, when the hook receives a Frappe field such as:

{
	"fieldname": "lead_name",
	"label": "Lead name",
	"fieldtype": "Data",
	"reqd": 1,
	"description": "The person or organization entering the pipeline"
}

it exposes that field in docTypeDefinition.fields as:

{
	"id": "lead_name",
	"label": "Lead name",
	"type": "text",
	"required": true,
	"readOnly": false,
	"description": "The person or organization entering the pipeline"
}

Use metadata outside React

The hook is the recommended API for components. If metadata is needed outside React, use the equivalent resolver-backed selector:

import { resolveSelect } from '@wordpress/data';

const definition = await resolveSelect(frappeStore).getDocTypeDefinition(
	'CRM Lead'
);

Both the hook and selector use the registered store's connection and share its resolved definition. Repeated reads of the same DocType use the cached value rather than making another request. Definitions currently remain cached for the lifetime of the page and have no automatic expiry, so reload the page after changing fields in Frappe when live schema changes matter.

The lower-level loadDocTypeDefinition(request, doctype) helper is only needed when using the metadata normalizer without a registered @wordpress/data store.

API

  • Store registration & creation: registerFrappeDataStore, createFrappeDataStore
  • Hooks: useFrappeResource, useFrappeResourceList, useFrappeResourceActions, useDocTypeDefinition
  • Selectors: getResource, getResourceList, getDocTypeDefinition, isRequestPending, getRequestError
  • Actions: fetchDocTypeDefinition, fetchResource, fetchResourceList, saveResource, deleteResource, invalidateResourceLists
  • Transport & Errors: createFrappeRequest, FrappeRequestError
  • Utilities & Helpers: loadDocTypeDefinition, getListKey, getResourceKey, toFrappeQuery
  • TypeScript Types: FrappeDataStore, FrappeStoreConfig, FrappeResource, FrappeResourceActions, FrappeListQuery, FrappeFilter, FrappeRequest, FrappeRequestOptions, FrappeBoundSelectors, DocTypeDefinition, ResourceFieldDefinition, RequestStatus

List responses are normalized by Frappe's name field. When an explicit fields list is supplied, the store automatically includes name.

Development

npm test
npm run typecheck
npm run build

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Support

For issues and feature requests, please use the GitHub issue tracker.

Meet Your Artisans

LUBUS is a web design agency based in Mumbai.