node-grocy
Version:
A JavaScript wrapper for the Grocy REST API
713 lines (615 loc) • 19.8 kB
JavaScript
/**
* Grocy - A JavaScript wrapper for the Grocy REST API
*
* Authentication is done via API keys (header *GROCY-API-KEY* or same named query parameter)
*/
export default class Grocy {
/**
* @param {string} baseUrl - The base URL of your Grocy instance
* @param {string} apiKey - Your Grocy API key
*/
constructor(baseUrl, apiKey = null) {
this.baseUrl = baseUrl.endsWith('/api') ? baseUrl : `${baseUrl}/api`;
this.apiKey = apiKey;
}
/**
* Set or update the API key
* @param {string} apiKey - Your Grocy API key
*/
setApiKey(apiKey) {
this.apiKey = apiKey;
}
/**
* Make a request to the Grocy API
* @param {string} endpoint - API endpoint
* @param {string} method - HTTP method
* @param {Object} data - Request data for POST/PUT requests
* @param {Object} queryParams - URL query parameters
* @returns {Promise<Object>} - Response data
*/
async request(endpoint, method = 'GET', data = null, queryParams = {}) {
if (!this.apiKey) {
throw new Error('API key is required. Use setApiKey() to set it.');
}
const url = new URL(`${this.baseUrl}${endpoint}`);
// Add query parameters if provided
if (queryParams && Object.keys(queryParams).length > 0) {
Object.entries(queryParams).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((v) => url.searchParams.append(`${key}[]`, v));
} else if (value !== undefined && value !== null) {
url.searchParams.append(key, value.toString());
}
});
}
const options = {
method,
headers: {
'GROCY-API-KEY': this.apiKey,
'Content-Type': 'application/json',
},
};
if (data && (method === 'POST' || method === 'PUT')) {
options.body = JSON.stringify(data);
}
try {
const response = await fetch(url, options);
// Handle non-JSON responses (like file downloads or no content)
if (response.status === 204) {
return { success: true };
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const jsonData = await response.json();
if (!response.ok) {
throw new Error(jsonData.error_message || `HTTP error! status: ${response.status}`);
}
return jsonData;
} else if (contentType && contentType.includes('text/calendar')) {
const textData = await response.text();
return { calendar: textData };
} else if (response.ok) {
// Handle binary responses or other non-JSON responses
return { success: true, response };
}
throw new Error(`HTTP error! status: ${response.status}`);
} catch (error) {
throw new Error(`Grocy API request failed: ${error.message}`);
}
}
// System endpoints
/**
* Get information about the installed Grocy version
* @returns {Promise<Object>} - System information
*/
async getSystemInfo() {
return this.request('/system/info');
}
/**
* Get the time when the database was last changed
* @returns {Promise<Object>} - Database last changed time
*/
async getDbChangedTime() {
return this.request('/system/db-changed-time');
}
/**
* Get all config settings
* @returns {Promise<Object>} - Config settings
*/
async getConfig() {
return this.request('/system/config');
}
/**
* Get the current server time
* @param {number} offset - Offset of timestamp in seconds
* @returns {Promise<Object>} - Server time information
*/
async getTime(offset = null) {
const params = offset !== null ? { offset } : {};
return this.request('/system/time', 'GET', null, params);
}
// Stock endpoints
/**
* Get all products currently in stock
* @returns {Promise<Array>} - Products in stock
*/
async getStock() {
return this.request('/stock');
}
/**
* Get details of a stock entry
* @param {number} entryId - Stock entry ID
* @returns {Promise<Object>} - Stock entry details
*/
async getStockEntry(entryId) {
return this.request(`/stock/entry/${entryId}`);
}
/**
* Edit a stock entry
* @param {number} entryId - Stock entry ID
* @param {Object} data - Stock entry data
* @returns {Promise<Array>} - Stock log entries
*/
async editStockEntry(entryId, data) {
return this.request(`/stock/entry/${entryId}`, 'PUT', data);
}
/**
* Get volatile stock (due soon, overdue, expired, missing)
* @param {number} dueSoonDays - Days for due soon products
* @returns {Promise<Object>} - Volatile stock information
*/
async getVolatileStock(dueSoonDays = 5) {
return this.request('/stock/volatile', 'GET', null, { due_soon_days: dueSoonDays });
}
/**
* Get product details
* @param {number} productId - Product ID
* @returns {Promise<Object>} - Product details
*/
async getProductDetails(productId) {
return this.request(`/stock/products/${productId}`);
}
/**
* Get product by barcode
* @param {string} barcode - Product barcode
* @returns {Promise<Object>} - Product details
*/
async getProductByBarcode(barcode) {
return this.request(`/stock/products/by-barcode/${barcode}`);
}
/**
* Add product to stock
* @param {number} productId - Product ID
* @param {Object} data - Stock data
* @returns {Promise<Array>} - Stock log entries
*/
async addProductToStock(productId, data) {
return this.request(`/stock/products/${productId}/add`, 'POST', data);
}
/**
* Add product to stock by barcode
* @param {string} barcode - Product barcode
* @param {Object} data - Stock data
* @returns {Promise<Array>} - Stock log entries
*/
async addProductToStockByBarcode(barcode, data) {
return this.request(`/stock/products/by-barcode/${barcode}/add`, 'POST', data);
}
/**
* Consume product from stock
* @param {number} productId - Product ID
* @param {Object} data - Consumption data
* @returns {Promise<Array>} - Stock log entries
*/
async consumeProduct(productId, data) {
return this.request(`/stock/products/${productId}/consume`, 'POST', data);
}
/**
* Consume product from stock by barcode
* @param {string} barcode - Product barcode
* @param {Object} data - Consumption data
* @returns {Promise<Array>} - Stock log entries
*/
async consumeProductByBarcode(barcode, data) {
return this.request(`/stock/products/by-barcode/${barcode}/consume`, 'POST', data);
}
/**
* Transfer product between locations
* @param {number} productId - Product ID
* @param {Object} data - Transfer data
* @returns {Promise<Array>} - Stock log entries
*/
async transferProduct(productId, data) {
return this.request(`/stock/products/${productId}/transfer`, 'POST', data);
}
/**
* Inventory product (set new amount)
* @param {number} productId - Product ID
* @param {Object} data - Inventory data
* @returns {Promise<Array>} - Stock log entries
*/
async inventoryProduct(productId, data) {
return this.request(`/stock/products/${productId}/inventory`, 'POST', data);
}
/**
* Mark product as opened
* @param {number} productId - Product ID
* @param {Object} data - Open data
* @returns {Promise<Array>} - Stock log entries
*/
async openProduct(productId, data) {
return this.request(`/stock/products/${productId}/open`, 'POST', data);
}
// Shopping list endpoints
/**
* Add missing products to shopping list
* @param {Object} data - Shopping list data
* @returns {Promise<Object>} - Success status
*/
async addMissingProductsToShoppingList(data = {}) {
return this.request('/stock/shoppinglist/add-missing-products', 'POST', data);
}
/**
* Add overdue products to shopping list
* @param {Object} data - Shopping list data
* @returns {Promise<Object>} - Success status
*/
async addOverdueProductsToShoppingList(data = {}) {
return this.request('/stock/shoppinglist/add-overdue-products', 'POST', data);
}
/**
* Add expired products to shopping list
* @param {Object} data - Shopping list data
* @returns {Promise<Object>} - Success status
*/
async addExpiredProductsToShoppingList(data = {}) {
return this.request('/stock/shoppinglist/add-expired-products', 'POST', data);
}
/**
* Clear shopping list
* @param {Object} data - Shopping list data
* @returns {Promise<Object>} - Success status
*/
async clearShoppingList(data = {}) {
return this.request('/stock/shoppinglist/clear', 'POST', data);
}
/**
* Add product to shopping list
* @param {Object} data - Shopping list item data
* @returns {Promise<Object>} - Success status
*/
async addProductToShoppingList(data) {
return this.request('/stock/shoppinglist/add-product', 'POST', data);
}
/**
* Remove product from shopping list
* @param {Object} data - Shopping list item data
* @returns {Promise<Object>} - Success status
*/
async removeProductFromShoppingList(data) {
return this.request('/stock/shoppinglist/remove-product', 'POST', data);
}
// Generic entity interactions
/**
* Get all objects of a given entity
* @param {string} entity - Entity name
* @param {Object} options - Query options
* @returns {Promise<Array>} - Entity objects
*/
async getObjects(entity, options = {}) {
const { query, order, limit, offset } = options;
const params = {};
if (query) params.query = query;
if (order) params.order = order;
if (limit) params.limit = limit;
if (offset) params.offset = offset;
return this.request(`/objects/${entity}`, 'GET', null, params);
}
/**
* Add an object of a given entity
* @param {string} entity - Entity name
* @param {Object} data - Entity data
* @returns {Promise<Object>} - Created object info
*/
async addObject(entity, data) {
return this.request(`/objects/${entity}`, 'POST', data);
}
/**
* Get a single object of a given entity
* @param {string} entity - Entity name
* @param {number} objectId - Object ID
* @returns {Promise<Object>} - Entity object
*/
async getObject(entity, objectId) {
return this.request(`/objects/${entity}/${objectId}`);
}
/**
* Edit an object of a given entity
* @param {string} entity - Entity name
* @param {number} objectId - Object ID
* @param {Object} data - Entity data
* @returns {Promise<Object>} - Success status
*/
async editObject(entity, objectId, data) {
return this.request(`/objects/${entity}/${objectId}`, 'PUT', data);
}
/**
* Delete an object of a given entity
* @param {string} entity - Entity name
* @param {number} objectId - Object ID
* @returns {Promise<Object>} - Success status
*/
async deleteObject(entity, objectId) {
return this.request(`/objects/${entity}/${objectId}`, 'DELETE');
}
// Userfields
/**
* Get userfields for an object
* @param {string} entity - Entity name
* @param {number|string} objectId - Object ID
* @returns {Promise<Object>} - Userfields
*/
async getUserfields(entity, objectId) {
return this.request(`/userfields/${entity}/${objectId}`);
}
/**
* Set userfields for an object
* @param {string} entity - Entity name
* @param {number|string} objectId - Object ID
* @param {Object} data - Userfields data
* @returns {Promise<Object>} - Success status
*/
async setUserfields(entity, objectId, data) {
return this.request(`/userfields/${entity}/${objectId}`, 'PUT', data);
}
// File endpoints
/**
* Get a file
* @param {string} group - File group
* @param {string} fileName - File name (BASE64 encoded)
* @param {Object} options - Additional options
* @returns {Promise<Object>} - File data
*/
async getFile(group, fileName, options = {}) {
return this.request(`/files/${group}/${fileName}`, 'GET', null, options);
}
/**
* Upload a file
* @param {string} group - File group
* @param {string} fileName - File name (BASE64 encoded)
* @param {Blob|File} fileData - File data
* @returns {Promise<Object>} - Success status
*/
async uploadFile(group, fileName, fileData) {
const url = new URL(`${this.baseUrl}/files/${group}/${fileName}`);
const options = {
method: 'PUT',
headers: {
'GROCY-API-KEY': this.apiKey,
},
body: fileData,
};
try {
const response = await fetch(url, options);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_message || `HTTP error! status: ${response.status}`);
}
return { success: true };
} catch (error) {
throw new Error(`Failed to upload file: ${error.message}`);
}
}
/**
* Delete a file
* @param {string} group - File group
* @param {string} fileName - File name (BASE64 encoded)
* @returns {Promise<Object>} - Success status
*/
async deleteFile(group, fileName) {
return this.request(`/files/${group}/${fileName}`, 'DELETE');
}
// User management endpoints
/**
* Get all users
* @param {Object} options - Query options
* @returns {Promise<Array>} - Users
*/
async getUsers(options = {}) {
const { query, order, limit, offset } = options;
const params = {};
if (query) params.query = query;
if (order) params.order = order;
if (limit) params.limit = limit;
if (offset) params.offset = offset;
return this.request('/users', 'GET', null, params);
}
/**
* Create a new user
* @param {Object} data - User data
* @returns {Promise<Object>} - Success status
*/
async createUser(data) {
return this.request('/users', 'POST', data);
}
/**
* Edit a user
* @param {number} userId - User ID
* @param {Object} data - User data
* @returns {Promise<Object>} - Success status
*/
async editUser(userId, data) {
return this.request(`/users/${userId}`, 'PUT', data);
}
/**
* Delete a user
* @param {number} userId - User ID
* @returns {Promise<Object>} - Success status
*/
async deleteUser(userId) {
return this.request(`/users/${userId}`, 'DELETE');
}
// Current user endpoints
/**
* Get current user
* @returns {Promise<Object>} - Current user info
*/
async getCurrentUser() {
return this.request('/user');
}
/**
* Get current user settings
* @returns {Promise<Object>} - User settings
*/
async getUserSettings() {
return this.request('/user/settings');
}
/**
* Get a specific user setting
* @param {string} settingKey - Setting key
* @returns {Promise<Object>} - Setting value
*/
async getUserSetting(settingKey) {
return this.request(`/user/settings/${settingKey}`);
}
/**
* Set a user setting
* @param {string} settingKey - Setting key
* @param {Object} data - Setting data
* @returns {Promise<Object>} - Success status
*/
async setUserSetting(settingKey, data) {
return this.request(`/user/settings/${settingKey}`, 'PUT', data);
}
// Recipe endpoints
/**
* Add missing recipe products to shopping list
* @param {number} recipeId - Recipe ID
* @param {Object} data - Additional options
* @returns {Promise<Object>} - Success status
*/
async addRecipeProductsToShoppingList(recipeId, data = {}) {
return this.request(`/recipes/${recipeId}/add-not-fulfilled-products-to-shoppinglist`, 'POST', data);
}
/**
* Get recipe fulfillment information
* @param {number} recipeId - Recipe ID
* @returns {Promise<Object>} - Recipe fulfillment info
*/
async getRecipeFulfillment(recipeId) {
return this.request(`/recipes/${recipeId}/fulfillment`);
}
/**
* Consume all recipe ingredients
* @param {number} recipeId - Recipe ID
* @returns {Promise<Object>} - Success status
*/
async consumeRecipe(recipeId) {
return this.request(`/recipes/${recipeId}/consume`, 'POST');
}
/**
* Get all recipes fulfillment
* @param {Object} options - Query options
* @returns {Promise<Array>} - Recipes fulfillment
*/
async getAllRecipesFulfillment(options = {}) {
const { query, order, limit, offset } = options;
const params = {};
if (query) params.query = query;
if (order) params.order = order;
if (limit) params.limit = limit;
if (offset) params.offset = offset;
return this.request('/recipes/fulfillment', 'GET', null, params);
}
// Chores endpoints
/**
* Get all chores
* @param {Object} options - Query options
* @returns {Promise<Array>} - Chores
*/
async getChores(options = {}) {
const { query, order, limit, offset } = options;
const params = {};
if (query) params.query = query;
if (order) params.order = order;
if (limit) params.limit = limit;
if (offset) params.offset = offset;
return this.request('/chores', 'GET', null, params);
}
/**
* Get chore details
* @param {number} choreId - Chore ID
* @returns {Promise<Object>} - Chore details
*/
async getChoreDetails(choreId) {
return this.request(`/chores/${choreId}`);
}
/**
* Execute a chore
* @param {number} choreId - Chore ID
* @param {Object} data - Execution data
* @returns {Promise<Object>} - Chore log entry
*/
async executeChore(choreId, data = {}) {
return this.request(`/chores/${choreId}/execute`, 'POST', data);
}
// Batteries endpoints
/**
* Get all batteries
* @param {Object} options - Query options
* @returns {Promise<Array>} - Batteries
*/
async getBatteries(options = {}) {
const { query, order, limit, offset } = options;
const params = {};
if (query) params.query = query;
if (order) params.order = order;
if (limit) params.limit = limit;
if (offset) params.offset = offset;
return this.request('/batteries', 'GET', null, params);
}
/**
* Get battery details
* @param {number} batteryId - Battery ID
* @returns {Promise<Object>} - Battery details
*/
async getBatteryDetails(batteryId) {
return this.request(`/batteries/${batteryId}`);
}
/**
* Charge a battery
* @param {number} batteryId - Battery ID
* @param {Object} data - Charge data
* @returns {Promise<Object>} - Battery charge cycle entry
*/
async chargeBattery(batteryId, data = {}) {
return this.request(`/batteries/${batteryId}/charge`, 'POST', data);
}
// Tasks endpoints
/**
* Get all tasks
* @param {Object} options - Query options
* @returns {Promise<Array>} - Tasks
*/
async getTasks(options = {}) {
const { query, order, limit, offset } = options;
const params = {};
if (query) params.query = query;
if (order) params.order = order;
if (limit) params.limit = limit;
if (offset) params.offset = offset;
return this.request('/tasks', 'GET', null, params);
}
/**
* Complete a task
* @param {number} taskId - Task ID
* @param {Object} data - Completion data
* @returns {Promise<Object>} - Success status
*/
async completeTask(taskId, data = {}) {
return this.request(`/tasks/${taskId}/complete`, 'POST', data);
}
/**
* Undo a task completion
* @param {number} taskId - Task ID
* @returns {Promise<Object>} - Success status
*/
async undoTask(taskId) {
return this.request(`/tasks/${taskId}/undo`, 'POST');
}
// Calendar endpoints
/**
* Get iCal calendar
* @returns {Promise<Object>} - Calendar data
*/
async getCalendar() {
return this.request('/calendar/ical');
}
/**
* Get calendar sharing link
* @returns {Promise<Object>} - Sharing link
*/
async getCalendarSharingLink() {
return this.request('/calendar/ical/sharing-link');
}
}