import { defineStore } from 'pinia'
import { useLogger } from '@/composables/logger.ts'
import { getAllAccountsFromCurrentFinances } from '@/datamodel/accounts.ts'
// eslint-disable-next-line import/no-cycle
import { useAccountStore } from '@/stores/account.ts'
import { useCurrentFinancesStore } from '@/stores/current-finances.ts'
import { useNotificationsStore } from '@/stores/notifications.ts'
import { useSettingsStore } from '@/stores/settings.ts'
// eslint-disable-next-line import/no-cycle
import { useSyncStore } from '@/stores/sync.ts'
import { makeObjectSerializable } from '@/utils/objects.ts'
/**
* @module PluginAPI
* @description Welcome to the ProjectionLab Plugin API docs. After you enable support for plugins within your ProjectionLab Account Settings page, you can access the community plugin methods documented below in your browser via window.projectionlabPluginAPI.
*/
export const usePluginApiStore = defineStore('plugin-api', () => {
// setup
const accountStore = useAccountStore()
const settingsStore = useSettingsStore()
const notificationsStore = useNotificationsStore()
const currentFinancesStore = useCurrentFinancesStore()
const syncStore = useSyncStore()
const log = useLogger({
name: 'pluginApiStore',
levels: {
debug: true,
},
})
// actions
async function handleCommunityPluginsExposure({ snackbar = false } = {}) {
const { hasAllFeatures } = accountStore
const { settings } = settingsStore
const snackbarOpts = {
timeout: 2000,
position: 'bottom right',
}
if (hasAllFeatures && settings.plugins.enabled) {
// plugins should be enabled
exposePluginAPI()
if (snackbar) {
notificationsStore.successNotification({
...snackbarOpts,
text: 'Enabled Plugins',
})
}
} else {
// plugins should be disabled
disablePluginAPI()
if (snackbar) {
notificationsStore.successNotification({
...snackbarOpts,
text: 'Disabled Plugins',
iconColor: 'red-lighten-1',
})
}
}
}
function assertCommunityPluginAccess({ key = '', silent = false }) {
const { settings } = settingsStore
if (!key) {
throwPluginFailure({ details: 'Missing Plugin API Key', silent })
}
if (key !== settings.plugins.apiKey) {
throwPluginFailure({ details: 'Invalid Plugin API Key', silent })
}
}
async function updateAccount(
accountId,
data = {},
{
key = '',
force = false,
} = {},
) {
const { today } = currentFinancesStore
if (!accountId || typeof accountId !== 'string') {
throwPluginFailure({ details: 'Invalid accountId: must be a string' })
}
if (!data || typeof data !== 'object') {
throwPluginFailure({ details: 'Invalid data: must be an object' })
}
assertCommunityPluginAccess({ key })
const accounts = getAllAccountsFromCurrentFinances(today)
const account = accounts.find(x => x.id == accountId)
if (!account) {
throwPluginFailure({ details: `No accounts in Current Finances match id "${accountId}"` })
return
}
if (!force) {
for (const prop of Object.keys(data)) {
if (!(prop in account)) {
throwPluginFailure({
details: `Failed to update account: ${account.name}. No matching property for assignment: ${prop}=${data[prop]}`,
})
}
}
}
Object.assign(account, data)
log.info('updated account: ', account.name, data, account)
}
async function exportData({ key = '' } = {}) {
assertCommunityPluginAccess({ key })
const data = accountStore.aggregateAllAccountData()
return makeObjectSerializable(data)
}
async function restoreToday(newToday, { key = '' } = {}) {
if (!newToday || typeof newToday !== 'object') {
throwPluginFailure({ details: 'Invalid newToday: must be an object' })
}
assertCommunityPluginAccess({ key })
syncStore.restoreToday(newToday)
syncStore.syncToday({ visible: true })
}
async function restorePlans(newPlans, { key = '' } = {}) {
if (!newPlans || typeof newPlans !== 'object') {
throwPluginFailure({ details: 'Invalid newPlans: must be an object' })
}
assertCommunityPluginAccess({ key })
syncStore.restorePlans(newPlans)
syncStore.syncPlans({ visible: true })
}
async function restoreProgress(newProgress, { key = '' } = {}) {
if (!newProgress || typeof newProgress !== 'object') {
throwPluginFailure({ details: 'Invalid newProgress: must be an object' })
}
assertCommunityPluginAccess({ key })
syncStore.restoreProgress(newProgress)
syncStore.syncProgress({ visible: true })
}
async function restoreSettings(newSettings, { key = '' } = {}) {
if (!newSettings || typeof newSettings !== 'object') {
throwPluginFailure({ details: 'Invalid newSettings: must be an object' })
}
assertCommunityPluginAccess({ key })
syncStore.restoreSettings(newSettings)
syncStore.syncSettings({ visible: true })
}
function validateApiKey({ key = '' } = {}) {
assertCommunityPluginAccess({ key, silent: true })
}
function exposePluginAPI() {
window.projectionlabPluginAPI = {
/**
* Updates an account in Current Finances with new data.
* @memberOf module:PluginAPI
* @function updateAccount
* @param {string} accountId - The ID of the account to be updated.
* @param {Object} data - The new data for the account.
* @param {Object} options - Options for the update.
* @param {string} options.key - Your Plugin API Key from Account Settings.
* * @param {string} options.force - Allow assignment of new properties to account objects.
* @throws {Error} If the API key is invalid or missing, or if parameters are incorrect.
* @example
* window.projectionlabPluginAPI.updateAccount('12345', { balance: 1000 }, { key: 'api-key' })
*/
updateAccount,
/**
* Exports all data based on the current state.
* @memberOf module:PluginAPI
* @function exportData
* @description Exports all data based on the current state.
* @async
* @param {Object} options - Options for the export.
* @param {string} options.key - Your Plugin API Key from Account Settings.
* @returns {Promise<Object>} A promise that resolves with the exported data.
* @throws {Error} If the API key is invalid or missing.
* @example
* window.projectionlabPluginAPI.exportData({ key: 'api-key' })
* .then(data => log.info(data))
*/
exportData,
/**
* Replaces the Current Finances state with new data. Be careful: this allows you to overwrite important data structures, and it is your responsibility to ensure the data you pass is well-formed.
* @memberOf module:PluginAPI
* @function restoreCurrentFinances
* @param {Object} newState - The new Current Finances state.
* @param {Object} options - Options for the update.
* @param {string} options.key - Your Plugin API Key from Account Settings.
* @throws {Error} If the API key is invalid or missing, or if 'newToday' is incorrect.
* @example
* window.projectionlabPluginAPI.restoreCurrentFinances({ ... }, { key: 'api-key' })
*/
restoreCurrentFinances: restoreToday,
/**
* Replaces all Plans with a new set of plans. Be careful: this allows you to overwrite important data structures, and it is your responsibility to ensure the data you pass is well-formed.
* @memberOf module:PluginAPI
* @function restorePlans
* @param {Object} newPlans - The new plans data.
* @param {Object} options - Options for the update.
* @param {string} options.key - Your Plugin API Key from Account Settings.
* @throws {Error} If the API key is invalid or missing, or if 'newPlans' is incorrect.
* @example
* window.projectionlabPluginAPI.restorePlans([{ name: 'Example Plan', ... }], { key: 'api-key' })
*/
restorePlans,
/**
* Replaces the Progress state with new data. Be careful: this allows you to overwrite important data structures, and it is your responsibility to ensure the data you pass is well-formed.
* @memberOf module:PluginAPI
* @function restoreProgress
* @param {Object} newProgress - The new progress data.
* @param {Object} options - Options for the update.
* @param {string} options.key - Your Plugin API Key from Account Settings.
* @throws {Error} If the API key is invalid or missing, or if 'newProgress' is incorrect.
* @example
* window.projectionlabPluginAPI.restoreProgress({ data: [...] }, { key: 'api-key' })
*/
restoreProgress,
/**
* Replaces Settings state with new data. Be careful: this allows you to overwrite important data structures, and it is your responsibility to ensure the data you pass is well-formed.
* @memberOf module:PluginAPI
* @function restoreSettings
* @param {Object} newSettings - The new settings data.
* @param {Object} options - Options for the update.
* @param {string} options.key - Your Plugin API Key from Account Settings.
* @throws {Error} If the API key is invalid or missing, or if 'newSettings' is incorrect.
* @example
* window.projectionlabPluginAPI.restoreSettings({ ... }, { key: 'api-key' })
*/
restoreSettings,
validateApiKey,
}
log.debug('enabled plugin API')
}
function disablePluginAPI() {
delete window.projectionlabPluginAPI
log.debug('disabled plugin API')
}
function throwPluginFailure({ details, snackbarOpts = {}, silent = false }) {
const opts = {
timeout: 2000,
...snackbarOpts,
}
const text = 'Plugin Operation Failed'
if (!silent) {
notificationsStore.errorNotification({
...opts,
text,
details,
})
}
throw new Error(`${text}: ${details}`)
}
return {
handleCommunityPluginsExposure,
assertCommunityPluginAccess,
updateAccount,
exportData,
restoreToday,
restorePlans,
restoreProgress,
restoreSettings,
validateApiKey,
exposePluginAPI,
disablePluginAPI,
throwPluginFailure,
}
})