New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

clockvine

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

clockvine - npm Package Compare versions

Comparing version

to
2.0.0-alpha.10

.editorconfig

5

package.json
{
"name": "clockvine",
"version": "2.0.0-alpha.8",
"version": "2.0.0-alpha.10",
"description": "",

@@ -37,3 +37,6 @@ "author": "Kevin Hamer [kh] <kevin@imarc.com>",

"vitest": "^0.24.3"
},
"dependencies": {
"path-to-regexp": "^6.2.1"
}
}

6

src/Clockvine.js

@@ -1,7 +0,3 @@

export { default as DefaultUrlFormatter } from './DefaultUrlFormatter.js'
export { default as JsonApi } from './JsonApi.js'
export { default as JsonSingletonApi } from './JsonSingletonApi.js'
export { default as SingletonUrlFormatter } from './SingletonUrlFormatter.js'
export { default as UrlFormatter } from './UrlFormatter.js'
export { default as XsrfFetch } from './XsrfFetch.js'
export { default as defineApiStore } from './defineApiStore.js'
export { default as useXsrfTokens } from './useXsrfTokens.js'
import { defineStore } from 'pinia'
import { reactive, toRef, computed, unref } from 'vue'
import { reactive, toRef, computed, toValue } from 'vue'
import JsonApi from './JsonApi'
const nestedUnref = obj => {
const result = Object.fromEntries(Object.entries(unref(obj)).map(([k, v]) => [k, unref(v)]))
const nestedToValue = (obj) => {
const result = Object.fromEntries(
Object.entries(toValue(obj)).map(([k, v]) => [k, toValue(v)])
)
return result

@@ -15,15 +17,20 @@ }

export default function defineApiStore (
const defineApiStore = function defineApiStore (
name,
api,
{
idField = 'id',
indexDataField = 'data',
showRequiresKey = true
} = {}
idField = defineApiStore.config.idField,
indexDataField = defineApiStore.config.indexDataField,
showRequiresKey = defineApiStore.config.showRequiresKey
} = {},
apiActions = {}
) {
if (typeof api === 'string' || typeof api === 'function') {
api = new JsonApi(api)
if (typeof name !== 'string') {
throw new Error('Store name must be a string.')
}
if (typeof api === 'string') {
api = new JsonApi(api, defineApiStore.config.ApiOptions)
}
return defineStore(name, () => {

@@ -46,2 +53,7 @@ /**

/**
* Custom actions.
*/
const actions = {}
// =========================================================================

@@ -74,4 +86,3 @@ // = Low Level

const mergeElement = (key, element) => {
const oldElement = unref(elements[key]) === undefined ? {} : unref(elements[key])
elements[key] = Object.assign(oldElement, element)
elements[key] = Object.assign(elements[key] ?? {}, element)
elementState[key] = VALID

@@ -87,7 +98,4 @@ return toRef(elements, key)

*/
const mergeElements = elements => {
if (!elements.map) {
console.error('elements.map not defined', elements)
}
return elements.map(element => mergeElement(element[idField], element))
const mergeElements = (elements) => {
return elements.map((element) => mergeElement(element[idField], element))
}

@@ -104,3 +112,8 @@

const setIndex = (key, index) => {
index[indexDataField] = mergeElements(index[indexDataField]).map(unref)
if (!index[indexDataField]) {
throw new Error(`Index must have a '${indexDataField}' field`)
} else if (typeof index[indexDataField].map !== 'function') {
throw new Error(`Index '${indexDataField}' field must be an array`)
}
index[indexDataField] = mergeElements(index[indexDataField]).map(toValue)
indexes[key] = index

@@ -115,8 +128,8 @@ return toRef(indexes, key)

/**
* @param {ref|mixed} idRef
* @return {ref} computed reference to elements[id]
*/
const show = idRef => {
* @param {ref|mixed} idRef
* @return {ref} computed reference to elements[id]
*/
const show = (idRef) => {
return computed(() => {
const id = unref(idRef)
const id = toValue(idRef)
if (showRequiresKey && (id === undefined || id === null)) {

@@ -129,3 +142,3 @@ return

elementState[id] = LOADING
api.show(id).then(element => {
api.get({ [idField]: id }).then((element) => {
const newElement = mergeElement(id, element)

@@ -144,4 +157,4 @@ elementState[id] = VALID

*/
const invalidate = elementOrIdRef => {
let elementOrId = unref(elementOrIdRef)
const invalidate = (elementOrIdRef) => {
let elementOrId = toValue(elementOrIdRef)
if (typeof elementOrId === 'object' && idField in elementOrId) {

@@ -152,11 +165,13 @@ elementOrId = elementOrId[idField]

elementState[elementOrId] = INVALID
return Promise.resolve()
}
/**
* @param {ref|object<ref>} params
* @return {ref} computed reference to elements[id]
*/
* @param {ref|object<ref>} params
* @return {ref} computed reference to elements[id]
*/
const indexAsRef = (paramsRef = {}) => {
return computed(() => {
const params = nestedUnref(paramsRef)
const params = nestedToValue(paramsRef)
const key = api.key('index', params)

@@ -167,3 +182,3 @@

indexState[key] = LOADING
api.index(params).then(index => {
api.index(params).then((index) => {
const newIndex = setIndex(key, index)

@@ -180,6 +195,8 @@ indexState[key] = VALID

const invalidateIndex = (paramsRef = {}) => {
const params = nestedUnref(paramsRef)
const params = nestedToValue(paramsRef)
const key = api.key('index', params)
indexState[key] = INVALID
return Promise.resolve()
}

@@ -191,25 +208,30 @@

}
return Promise.resolve()
}
const index = (paramsRef = {}) => {
return new Proxy({}, {
get (_, prop) {
return computed(() => {
const params = nestedUnref(paramsRef)
const key = api.key('index', params)
return new Proxy(
{},
{
get (_, prop) {
return computed(() => {
const params = nestedToValue(paramsRef)
const key = api.key('index', params)
if (!(key in indexState) || indexState[key] === INVALID) {
indexes[key] = indexes[key] || reactive({})
indexState[key] = LOADING
api.index(params).then(index => {
const newIndex = setIndex(key, index)
indexState[key] = VALID
return newIndex
})
}
if (!(key in indexState) || indexState[key] === INVALID) {
indexes[key] = indexes[key] || reactive({})
indexState[key] = LOADING
api.index(params).then((index) => {
const newIndex = setIndex(key, index)
indexState[key] = VALID
return newIndex
})
}
return indexes[key][prop]
})
return indexes[key][prop]
})
}
}
})
)
}

@@ -222,3 +244,3 @@

const store = async (element, params = {}) => {
const newElement = await api.store(nestedUnref(element), params)
const newElement = await api.post(nestedToValue(element), params)
invalidateAllIndexes()

@@ -229,4 +251,5 @@ return mergeElement(newElement[idField], newElement)

const update = async (element, params = {}) => {
const updatedElement = await api.update(nestedUnref(element), params)
const id = idField in updatedElement ? updatedElement[idField] : element[idField]
const updatedElement = await api.put(nestedToValue(element), params)
const id =
idField in updatedElement ? updatedElement[idField] : element[idField]
invalidateAllIndexes()

@@ -237,3 +260,3 @@ return mergeElement(id, updatedElement)

const destroy = async (element, params = {}) => {
await api.destroy(nestedUnref(element), params)
await api.destroy(nestedToValue(element), params)
invalidateAllIndexes()

@@ -243,2 +266,30 @@ return deleteElement(element)

const defineAction = async (
action,
{ apiAction = action, invalidateIndexes = false, mergeElement = true }
) => {
actions[action] = async (element, params = {}) => {
const updatedElement = await api[apiAction](
nestedToValue(element),
params
)
if (invalidateIndexes) {
invalidateAllIndexes()
}
const id =
idField in updatedElement
? updatedElement[idField]
: element[idField]
if (mergeElement && id) {
return mergeElement(id, updatedElement)
} else {
return updatedElement
}
}
}
Object.entries(apiActions).forEach(([action, options]) =>
defineAction(action, options)
)
return {

@@ -254,2 +305,3 @@ /**

indexState,
actions,

@@ -264,5 +316,18 @@ destroy,

store,
update
update,
...actions
}
})
}
defineApiStore.config = {
idField: 'id',
indexDataField: 'data',
showRequiresKey: true,
ApiOptions: undefined
}
defineApiStore.use = (plugin) => plugin(defineApiStore)
export default defineApiStore

@@ -1,57 +0,62 @@

import DefaultUrlFormatter from './DefaultUrlFormatter.js'
import UrlExp from './UrlExp.js'
export default function JsonApi (baseUrl, {
fetch = window.fetch,
Formatter = DefaultUrlFormatter,
serialize = JSON.stringify
const JsonApi = function JsonApi (urlExp, {
fetch = JsonApi.config.fetch,
headers = JsonApi.config.headers,
serialize = JsonApi.config.serialize
} = {}) {
const createQueryUrl = (new Formatter(baseUrl)).format
this.key = createQueryUrl
this.index = async function (params) {
const url = createQueryUrl('index', params)
return fetch(url).then(r => r.json())
if (!(urlExp instanceof UrlExp)) {
if (typeof urlExp === 'string') {
urlExp = new UrlExp(urlExp)
} else {
throw new TypeError('urlExp must be a UrlExp or string')
}
}
this.show = async function (id) {
const url = createQueryUrl('show', { id })
return fetch(url).then(r => r.json()).then(r => r.data)
}
this.store = async function (element, params = {}) {
const url = createQueryUrl('store', params, element)
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: serialize(element)
const makeAction = function (
method,
{
beforeFetch = options => options,
afterFetch = r => r.json()
} = {}
) {
return async (element, params = {}) => {
const url = urlExp.format(params, element)
const options = { url, method, headers }
if (!['get', 'head'].includes(method.toLowerCase())) {
options.body = serialize(element)
}
beforeFetch(options)
return fetch(options.url, options).then(afterFetch)
}
return fetch(url, options).then(r => r.json()).then(r => r.data)
}
this.update = async function (element, params = {}) {
const url = createQueryUrl('update', params, element)
const options = {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: serialize(element)
}
return fetch(url, options).then(r => r.json()).then(r => r.data)
this.defineAction = function (action, method = action, ...args) {
this[action] = (element, params = {}) => makeAction(method, ...args)(element, params).then(r => r.data)
}
this.destroy = async function (element, params = {}) {
const url = createQueryUrl('destroy', params, element)
const options = {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: serialize(element)
}
return fetch(url, options).then(r => r.json()).then(r => r.data)
}
this.key = params => urlExp.format(params)
this.defineAction('delete')
this.destroy = this.delete
this.defineAction('put')
this.update = this.put
this.defineAction('post')
this.store = this.post
this.defineAction('get')
this.show = this.get
this.index = this.get
}
JsonApi.config = {
fetch,
headers: { 'Content-Type': 'application/json' },
serialize: JSON.stringify
}
JsonApi.use = plugin => plugin(JsonApi)
export default JsonApi

@@ -1,147 +0,41 @@

import { ref, computed, isRef, watch } from 'vue'
import { beforeEach, expect, test, vi } from 'vitest'
import { userApiReset, mockUserApi, testUserStore, vueUpdates, ensureLoaded } from './testHelpers.js'
import { beforeEach, expect, test } from 'vitest'
import { ref, toValue, isRef, isReactive } from 'vue'
import { setActivePinia, createPinia } from 'pinia'
import { flushPromises } from '@vue/test-utils'
beforeEach(userApiReset)
import defineApiStore from '../src/defineApiStore.js'
import mockJsonApi from './JsonApi.mock.js'
test('loading an index calls api.index', () => {
const indexSpy = vi.spyOn(mockUserApi, 'index')
const store = testUserStore()
const useMockStore = defineApiStore('testUsers', mockJsonApi)
let store = null
expect(indexSpy).toHaveBeenCalledTimes(0)
// necessary for to trigger computing the index at all
ensureLoaded(store.index().data)
expect(indexSpy).toHaveBeenCalledTimes(1)
beforeEach(() => {
setActivePinia(createPinia())
mockJsonApi.reset()
store = useMockStore()
})
test('loading an index more than once only calls api.index once', () => {
const indexSpy = vi.spyOn(mockUserApi, 'index')
const store = testUserStore()
test('can load an index', async () => {
const index = store.index()
ensureLoaded(store.index().data)
ensureLoaded(store.index().data)
toValue(index.data)
await flushPromises()
expect(indexSpy).toHaveBeenCalledTimes(1)
expect(toValue(index.data).length).toBe(2)
})
test('invalidating an index will recall api.index', async () => {
const indexSpy = vi.spyOn(mockUserApi, 'index')
const userStore = testUserStore()
test('index is reactive', async () => {
const index = store.index()
const { data: users } = userStore.index()
toValue(index.data)
await flushPromises()
watch(users, () => {})
userStore.invalidateIndex()
await vueUpdates()
expect(toValue(index.data).length).toBe(2)
expect(indexSpy).toHaveBeenCalledTimes(2)
})
store.store({ id: 3, name: 'Chuck', full_name: 'Chuck Norris' })
await flushPromises()
toValue(index.data)
await flushPromises()
test('parameters are passed through to api.index', () => {
const indexSpy = vi.spyOn(mockUserApi, 'index')
const store = testUserStore()
ensureLoaded(store.index({ foo: 'bar', bin: 'baz' }).data)
expect(indexSpy).toHaveBeenCalledWith({ foo: 'bar', bin: 'baz' })
expect(toValue(index.data).length).toBe(3)
})
test('can call with different sets of parameters', async () => {
const indexSpy = vi.spyOn(mockUserApi, 'index')
const store = testUserStore()
ensureLoaded(store.index({ foo: 'bar', bin: 'baz' }).data)
expect(indexSpy).toHaveBeenCalledWith({ foo: 'bar', bin: 'baz' })
ensureLoaded(store.index({ foo: 'alpha' }).data)
await vueUpdates()
expect(indexSpy).toHaveBeenCalledWith({ foo: 'alpha' })
})
test('parameter properties can be reactive', async () => {
const indexSpy = vi.spyOn(mockUserApi, 'index')
const store = testUserStore()
const foo = ref('bar')
const { users } = store.index({ foo })
ensureLoaded(users)
expect(indexSpy).toHaveBeenCalledWith({ foo: 'bar' })
foo.value = 'biz'
ensureLoaded(users)
expect(indexSpy).toHaveBeenCalledWith({ foo: 'biz' })
})
test('Parameters itself can be reactive', async () => {
const indexSpy = vi.spyOn(mockUserApi, 'index')
const store = testUserStore()
const foo = ref('bar')
const params = computed(() => ({ foo }))
const { data: users } = store.index(params)
ensureLoaded(users)
expect(indexSpy).toHaveBeenCalledWith({ foo: 'bar' })
foo.value = 'biz'
ensureLoaded(users)
expect(indexSpy).toHaveBeenCalledWith({ foo: 'biz' })
})
test('the returned value is reactive', async () => {
const indexSpy = vi.spyOn(mockUserApi, 'index')
const store = testUserStore()
const { data: users } = store.index()
expect(users.value).toBe(undefined)
await vueUpdates()
expect(indexSpy).toHaveBeenCalledTimes(1)
expect(users.value.length).toBe(2)
})
test('api.show results merge over api.index', async () => {
const store = testUserStore()
ensureLoaded(store.show(1))
const { data: users } = store.index()
ensureLoaded(users)
await vueUpdates()
expect(users.value[0].shown).toBeTruthy()
})
test('index returns references', async () => {
const store = testUserStore()
const { data } = store.index()
expect(isRef(data)).toBeTruthy()
ensureLoaded(data)
await vueUpdates()
expect(data.value.length).toBe(2)
})
test('index references work properly', async () => {
const store = testUserStore()
const { data } = store.index()
watch(data, () => {})
await vueUpdates()
expect(data.value.length).toBe(2)
await store.store({ id: 10, name: 'Cheese', full_name: 'Brie' })
await vueUpdates()
expect(data.value.length).toBe(3)
})

@@ -1,59 +0,70 @@

import { watch } from 'vue'
import { beforeEach, expect, test, vi } from 'vitest'
import { userApiReset, mockUserApi, testUserStore, vueUpdates, ensureLoaded } from './testHelpers.js'
import { beforeEach, expect, test } from 'vitest'
import { ref, toValue } from 'vue'
import { setActivePinia, createPinia } from 'pinia'
import { flushPromises } from '@vue/test-utils'
beforeEach(userApiReset)
import defineApiStore from '../src/defineApiStore.js'
import mockJsonApi from './JsonApi.mock.js'
test('ref is reactive', async () => {
const showSpy = vi.spyOn(mockUserApi, 'show')
const store = testUserStore()
const useMockStore = defineApiStore('testUsers', mockJsonApi)
let store = null
beforeEach(() => {
setActivePinia(createPinia())
mockJsonApi.reset()
store = useMockStore()
})
test('can get by ID', async () => {
const person1 = store.show(1)
expect(person1.value).toBe(undefined)
toValue(person1)
await flushPromises()
await vueUpdates()
expect(person1.value.name).toBe('Kevin')
})
expect(showSpy).toHaveBeenCalledTimes(1)
test('calls API', async () => {
const person1 = store.show(1)
expect(person1?.value.name).toBe('Kevin')
await flushPromises()
})
test('Only calls show on Api once', async () => {
const show = vi.spyOn(mockUserApi, 'show')
const store = testUserStore()
test('get same value for multiple refs', async () => {
const person1 = store.show(1)
const person2 = store.show(1)
ensureLoaded(store.show(1))
ensureLoaded(store.show(1))
await flushPromises()
expect(show).toHaveBeenCalledTimes(1) // TODO
expect(person1 === person2).toBeFalsy()
expect(person1.value === person2.value).toBeTruthy()
})
test('ref is mutable', async () => {
const store = testUserStore()
const person1 = store.show(1)
const another1 = store.show(1)
const person2 = store.show(1)
ensureLoaded(person1)
await vueUpdates()
toValue(person1)
toValue(person2)
await flushPromises()
another1.value.name = 'Chuck'
expect(person1?.value.name).toBe('Chuck')
person1.value.name = 'Chuck'
expect(person2.value.name).toBe('Chuck')
})
test('invalidating an element will recall api.show', async () => {
const showSpy = vi.spyOn(mockUserApi, 'show')
const store = testUserStore()
const person1 = store.show(1)
test('ref is reactive to ID changes', async () => {
mockJsonApi.reset()
const id = ref(1)
const person1 = store.show(id)
watch(person1, () => {})
toValue(person1)
await flushPromises()
await vueUpdates()
expect(person1.value.name).toBe('Kevin')
expect(showSpy).toHaveBeenCalledTimes(1)
id.value = 2
toValue(person1)
await flushPromises()
store.invalidate(person1)
await vueUpdates()
expect(showSpy).toHaveBeenCalledTimes(2)
expect(person1.value.name).toBe('Test')
})

@@ -13,3 +13,3 @@ const mockUsersInitialState = {

index: (params) => Promise.resolve({ data: Object.values(mockUsers).map(e => Object.assign({}, e)) }),
show: id => Promise.resolve(id in mockUsers ? Object.assign({ shown: (new Date()) }, mockUsers[id]) : null),
get: id => Promise.resolve(id in mockUsers ? mockUsers[id] : null),
update: element => {

@@ -16,0 +16,0 @@ mockUsers[element.id] = element