@symbo.ls/fetch 3.6.4 → 3.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +724 -0
- package/adapters/local.js +148 -0
- package/adapters/rest.js +147 -0
- package/adapters/supabase.js +153 -0
- package/index.js +940 -68
- package/package.json +23 -19
- package/LICENSE +0 -21
- package/dist/cjs/index.js +0 -103
- package/dist/esm/index.js +0 -73
- package/dist/iife/index.js +0 -2992
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const matchesParams = (item, params) => {
|
|
4
|
+
if (!params) return true
|
|
5
|
+
for (const key in params) {
|
|
6
|
+
const val = params[key]
|
|
7
|
+
if (val === null) {
|
|
8
|
+
if (item[key] !== null && item[key] !== undefined) return false
|
|
9
|
+
} else if (Array.isArray(val)) {
|
|
10
|
+
if (!val.includes(item[key])) return false
|
|
11
|
+
} else if (typeof val === 'object') {
|
|
12
|
+
for (const op in val) {
|
|
13
|
+
if (op === 'gt' && !(item[key] > val[op])) return false
|
|
14
|
+
if (op === 'gte' && !(item[key] >= val[op])) return false
|
|
15
|
+
if (op === 'lt' && !(item[key] < val[op])) return false
|
|
16
|
+
if (op === 'lte' && !(item[key] <= val[op])) return false
|
|
17
|
+
if (op === 'neq' && item[key] === val[op]) return false
|
|
18
|
+
if (op === 'like' && !String(item[key]).includes(val[op])) return false
|
|
19
|
+
}
|
|
20
|
+
} else if (item[key] !== val) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const applyModifiers = (items, { order, limit, offset, single }) => {
|
|
28
|
+
let result = [...items]
|
|
29
|
+
|
|
30
|
+
if (order) {
|
|
31
|
+
const by = typeof order === 'string' ? order : order.by
|
|
32
|
+
const asc = order.asc !== false
|
|
33
|
+
result.sort((a, b) => {
|
|
34
|
+
if (a[by] < b[by]) return asc ? -1 : 1
|
|
35
|
+
if (a[by] > b[by]) return asc ? 1 : -1
|
|
36
|
+
return 0
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (offset) result = result.slice(offset)
|
|
41
|
+
if (limit) result = result.slice(0, limit)
|
|
42
|
+
if (single) return { data: result[0] || null, error: null }
|
|
43
|
+
|
|
44
|
+
return { data: result, error: null }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const setup = async ({ data, ...options }) => localAdapter(data, options)
|
|
48
|
+
|
|
49
|
+
export const localAdapter = (initialData = {}, options = {}) => {
|
|
50
|
+
const store = {}
|
|
51
|
+
const listeners = {}
|
|
52
|
+
const persist = options.persist !== false && typeof localStorage !== 'undefined'
|
|
53
|
+
const prefix = options.prefix || 'smbls_db_'
|
|
54
|
+
|
|
55
|
+
const getTable = (name) => {
|
|
56
|
+
if (!store[name]) {
|
|
57
|
+
if (persist) {
|
|
58
|
+
try {
|
|
59
|
+
const saved = localStorage.getItem(prefix + name)
|
|
60
|
+
store[name] = saved ? JSON.parse(saved) : (initialData[name] || [])
|
|
61
|
+
} catch { store[name] = initialData[name] || [] }
|
|
62
|
+
} else {
|
|
63
|
+
store[name] = initialData[name] || []
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return store[name]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const save = (name) => {
|
|
70
|
+
if (persist) {
|
|
71
|
+
try { localStorage.setItem(prefix + name, JSON.stringify(store[name])) } catch {}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const notify = (name, event, newItem, oldItem) => {
|
|
76
|
+
const fns = listeners[name]
|
|
77
|
+
if (!fns) return
|
|
78
|
+
for (const fn of fns) fn(newItem, oldItem, { event, table: name })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let idCounter = Date.now()
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
name: 'local',
|
|
85
|
+
store,
|
|
86
|
+
|
|
87
|
+
select: async ({ from, params, ...modifiers }) => {
|
|
88
|
+
const table = getTable(from)
|
|
89
|
+
const filtered = table.filter(item => matchesParams(item, params))
|
|
90
|
+
return applyModifiers(filtered, modifiers)
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
insert: async ({ from, data }) => {
|
|
94
|
+
const table = getTable(from)
|
|
95
|
+
const items = Array.isArray(data) ? data : [data]
|
|
96
|
+
const inserted = items.map(item => {
|
|
97
|
+
const row = { id: item.id || ++idCounter, ...item }
|
|
98
|
+
table.push(row)
|
|
99
|
+
return row
|
|
100
|
+
})
|
|
101
|
+
save(from)
|
|
102
|
+
for (const row of inserted) notify(from, 'INSERT', row, null)
|
|
103
|
+
return { data: Array.isArray(data) ? inserted : inserted[0], error: null }
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
update: async ({ from, data, params }) => {
|
|
107
|
+
const table = getTable(from)
|
|
108
|
+
const updated = []
|
|
109
|
+
for (let i = 0; i < table.length; i++) {
|
|
110
|
+
if (matchesParams(table[i], params)) {
|
|
111
|
+
const old = { ...table[i] }
|
|
112
|
+
Object.assign(table[i], data)
|
|
113
|
+
updated.push(table[i])
|
|
114
|
+
notify(from, 'UPDATE', table[i], old)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
save(from)
|
|
118
|
+
return { data: updated, error: null }
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
delete: async ({ from, params }) => {
|
|
122
|
+
const table = getTable(from)
|
|
123
|
+
const removed = []
|
|
124
|
+
for (let i = table.length - 1; i >= 0; i--) {
|
|
125
|
+
if (matchesParams(table[i], params)) {
|
|
126
|
+
removed.push(table[i])
|
|
127
|
+
table.splice(i, 1)
|
|
128
|
+
notify(from, 'DELETE', null, removed[removed.length - 1])
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
save(from)
|
|
132
|
+
return { data: removed, error: null }
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
subscribe: ({ from, on }, callback) => {
|
|
136
|
+
if (!listeners[from]) listeners[from] = []
|
|
137
|
+
const fn = (newItem, oldItem, payload) => {
|
|
138
|
+
if (on && on !== '*' && on !== payload.event) return
|
|
139
|
+
callback(newItem, oldItem, payload)
|
|
140
|
+
}
|
|
141
|
+
listeners[from].push(fn)
|
|
142
|
+
return () => {
|
|
143
|
+
const idx = listeners[from].indexOf(fn)
|
|
144
|
+
if (idx > -1) listeners[from].splice(idx, 1)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
package/adapters/rest.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const buildUrl = (base, from, params) => {
|
|
4
|
+
const slash = from && !from.startsWith('/') ? '/' : ''
|
|
5
|
+
const url = new URL(`${base}${slash}${from}`)
|
|
6
|
+
if (params) {
|
|
7
|
+
for (const key in params) {
|
|
8
|
+
const val = params[key]
|
|
9
|
+
if (val !== undefined && val !== null) {
|
|
10
|
+
url.searchParams.set(key, typeof val === 'object' ? JSON.stringify(val) : val)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return url.toString()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const request = async (url, options) => {
|
|
18
|
+
const res = await globalThis.fetch(url, options)
|
|
19
|
+
const contentType = res.headers.get('content-type')
|
|
20
|
+
const data = contentType && contentType.includes('json')
|
|
21
|
+
? await res.json()
|
|
22
|
+
: await res.text()
|
|
23
|
+
return { data, error: res.ok ? null : (data?.message || data?.error || res.statusText), status: res.status }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const setup = async ({ url, headers, auth, fetchOptions }) => {
|
|
27
|
+
if (!url) throw new Error('@symbo.ls/fetch rest: url is required')
|
|
28
|
+
return restAdapter(url, headers, auth, fetchOptions)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const restAdapter = (baseUrl, defaultHeaders = {}, authConfig, defaultFetchOptions = {}) => {
|
|
32
|
+
let token = authConfig?.token || null
|
|
33
|
+
|
|
34
|
+
const getHeaders = (extra) => {
|
|
35
|
+
const h = { ...defaultHeaders, ...extra }
|
|
36
|
+
if (token) h.Authorization = `Bearer ${token}`
|
|
37
|
+
return h
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getJsonHeaders = (extra) => ({ ...getHeaders(extra), 'Content-Type': 'application/json' })
|
|
41
|
+
|
|
42
|
+
const getFetchOptions = (opts) => ({ ...defaultFetchOptions, ...opts })
|
|
43
|
+
|
|
44
|
+
const resolveUrl = (from, base) => {
|
|
45
|
+
const b = base || baseUrl
|
|
46
|
+
if (from && (from.startsWith('http://') || from.startsWith('https://'))) return from
|
|
47
|
+
const slash = from && !from.startsWith('/') ? '/' : ''
|
|
48
|
+
return `${b}${slash}${from || ''}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const adapter = {
|
|
52
|
+
name: 'rest',
|
|
53
|
+
|
|
54
|
+
// Auth
|
|
55
|
+
setToken: (t) => { token = t },
|
|
56
|
+
|
|
57
|
+
getSession: async () => {
|
|
58
|
+
if (!authConfig?.sessionUrl) {
|
|
59
|
+
return token ? { token } : null
|
|
60
|
+
}
|
|
61
|
+
const result = await request(
|
|
62
|
+
resolveUrl(authConfig.sessionUrl, authConfig.baseUrl),
|
|
63
|
+
getFetchOptions({ headers: getHeaders() })
|
|
64
|
+
)
|
|
65
|
+
return result.error ? null : result.data
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
signIn: async (credentials) => {
|
|
69
|
+
if (!authConfig?.signInUrl) throw new Error('rest: auth.signInUrl not configured')
|
|
70
|
+
const result = await request(resolveUrl(authConfig.signInUrl, authConfig.baseUrl), getFetchOptions({
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: getJsonHeaders(),
|
|
73
|
+
body: JSON.stringify(credentials)
|
|
74
|
+
}))
|
|
75
|
+
if (result.data?.token) token = result.data.token
|
|
76
|
+
return result
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
signOut: async () => {
|
|
80
|
+
if (authConfig?.signOutUrl) {
|
|
81
|
+
await request(resolveUrl(authConfig.signOutUrl, authConfig.baseUrl), getFetchOptions({
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: getHeaders()
|
|
84
|
+
}))
|
|
85
|
+
}
|
|
86
|
+
token = null
|
|
87
|
+
return { error: null }
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// CRUD
|
|
91
|
+
select: ({ from, params, select, limit, offset, order, single, headers, baseUrl: fromBase }) => {
|
|
92
|
+
const allParams = { ...params }
|
|
93
|
+
if (select) allParams.select = select
|
|
94
|
+
if (limit) allParams.limit = limit
|
|
95
|
+
if (offset) allParams.offset = offset
|
|
96
|
+
if (single) allParams.single = true
|
|
97
|
+
if (order) {
|
|
98
|
+
if (typeof order === 'string') {
|
|
99
|
+
allParams.order = order
|
|
100
|
+
} else if (Array.isArray(order)) {
|
|
101
|
+
allParams.order = order.map(o => `${o.by}:${o.asc === false ? 'desc' : 'asc'}`).join(',')
|
|
102
|
+
} else if (order.by) {
|
|
103
|
+
allParams.order = `${order.by}:${order.asc === false ? 'desc' : 'asc'}`
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return request(
|
|
107
|
+
buildUrl(fromBase || baseUrl, from || '', allParams),
|
|
108
|
+
getFetchOptions({ headers: getHeaders(headers) })
|
|
109
|
+
)
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
rpc: ({ from, params, headers, baseUrl: fromBase }) =>
|
|
113
|
+
request(resolveUrl(`rpc/${from}`, fromBase), getFetchOptions({
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: getJsonHeaders(headers),
|
|
116
|
+
body: JSON.stringify(params)
|
|
117
|
+
})),
|
|
118
|
+
|
|
119
|
+
insert: ({ from, data, headers, baseUrl: fromBase }) =>
|
|
120
|
+
request(resolveUrl(from, fromBase), getFetchOptions({
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: getJsonHeaders(headers),
|
|
123
|
+
body: JSON.stringify(data)
|
|
124
|
+
})),
|
|
125
|
+
|
|
126
|
+
update: ({ from, data, params, method, headers, baseUrl: fromBase }) => {
|
|
127
|
+
const id = params?.id || data?.id
|
|
128
|
+
const path = id ? `${from}/${id}` : from
|
|
129
|
+
return request(resolveUrl(path, fromBase), getFetchOptions({
|
|
130
|
+
method: method || 'PATCH',
|
|
131
|
+
headers: getJsonHeaders(headers),
|
|
132
|
+
body: JSON.stringify(data)
|
|
133
|
+
}))
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
delete: ({ from, params, headers, baseUrl: fromBase }) => {
|
|
137
|
+
const id = params?.id
|
|
138
|
+
const path = id ? `${from}/${id}` : from
|
|
139
|
+
return request(resolveUrl(path, fromBase), getFetchOptions({
|
|
140
|
+
method: 'DELETE',
|
|
141
|
+
headers: getHeaders(headers)
|
|
142
|
+
}))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return adapter
|
|
147
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
export const setup = async ({ url, key, projectId, createClient, ...options }) => {
|
|
4
|
+
const supabaseUrl = url || (projectId && `https://${projectId}.supabase.co`)
|
|
5
|
+
if (!supabaseUrl || !key) {
|
|
6
|
+
throw new Error('@symbo.ls/fetch supabase: url (or projectId) and key are required')
|
|
7
|
+
}
|
|
8
|
+
if (!createClient) {
|
|
9
|
+
const pkg = '@supabase/' + 'supabase-js'
|
|
10
|
+
const mod = await import(/* webpackIgnore: true */ pkg)
|
|
11
|
+
createClient = mod.createClient
|
|
12
|
+
}
|
|
13
|
+
return supabaseAdapter(createClient(supabaseUrl, key, options))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const applyFilters = (query, params) => {
|
|
17
|
+
if (!params) return query
|
|
18
|
+
for (const key in params) {
|
|
19
|
+
const val = params[key]
|
|
20
|
+
if (val === null) {
|
|
21
|
+
query = query.is(key, null)
|
|
22
|
+
} else if (Array.isArray(val)) {
|
|
23
|
+
query = query.in(key, val)
|
|
24
|
+
} else if (typeof val === 'object') {
|
|
25
|
+
for (const op in val) {
|
|
26
|
+
if (op === 'gt') query = query.gt(key, val[op])
|
|
27
|
+
else if (op === 'gte') query = query.gte(key, val[op])
|
|
28
|
+
else if (op === 'lt') query = query.lt(key, val[op])
|
|
29
|
+
else if (op === 'lte') query = query.lte(key, val[op])
|
|
30
|
+
else if (op === 'neq') query = query.neq(key, val[op])
|
|
31
|
+
else if (op === 'like') query = query.like(key, val[op])
|
|
32
|
+
else if (op === 'ilike') query = query.ilike(key, val[op])
|
|
33
|
+
else if (op === 'in') query = query.in(key, val[op])
|
|
34
|
+
else if (op === 'is') query = query.is(key, val[op])
|
|
35
|
+
else if (op === 'contains') query = query.contains(key, val[op])
|
|
36
|
+
else if (op === 'containedBy') query = query.containedBy(key, val[op])
|
|
37
|
+
else if (op === 'textSearch') query = query.textSearch(key, val[op])
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
query = query.eq(key, val)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return query
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const applyModifiers = (query, { limit, offset, order, single } = {}) => {
|
|
47
|
+
if (order) {
|
|
48
|
+
if (Array.isArray(order)) {
|
|
49
|
+
for (const o of order) {
|
|
50
|
+
const by = typeof o === 'string' ? o : o.by
|
|
51
|
+
query = query.order(by, { ascending: o.asc !== false })
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
const orderBy = typeof order === 'string' ? order : order.by
|
|
55
|
+
query = query.order(orderBy, { ascending: order.asc !== false })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (limit) query = query.limit(limit)
|
|
59
|
+
if (offset) query = query.range(offset, offset + (limit || 20) - 1)
|
|
60
|
+
if (single) query = query.single()
|
|
61
|
+
return query
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const supabaseAdapter = (client) => ({
|
|
65
|
+
name: 'supabase',
|
|
66
|
+
client,
|
|
67
|
+
|
|
68
|
+
// Auth
|
|
69
|
+
getSession: async () => {
|
|
70
|
+
const { data } = await client.auth.getSession()
|
|
71
|
+
return data?.session || null
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
getUser: async () => {
|
|
75
|
+
const { data } = await client.auth.getUser()
|
|
76
|
+
return data?.user || null
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
signIn: (credentials) => {
|
|
80
|
+
if (credentials.provider) {
|
|
81
|
+
return client.auth.signInWithOAuth({ provider: credentials.provider })
|
|
82
|
+
}
|
|
83
|
+
if (credentials.token) {
|
|
84
|
+
return client.auth.signInWithIdToken(credentials)
|
|
85
|
+
}
|
|
86
|
+
return client.auth.signInWithPassword(credentials)
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
signUp: (credentials) => client.auth.signUp(credentials),
|
|
90
|
+
|
|
91
|
+
signOut: () => client.auth.signOut(),
|
|
92
|
+
|
|
93
|
+
onAuthStateChange: (callback) => {
|
|
94
|
+
const { data: { subscription } } = client.auth.onAuthStateChange(
|
|
95
|
+
(event, session) => callback(event, session)
|
|
96
|
+
)
|
|
97
|
+
return () => subscription.unsubscribe()
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// CRUD
|
|
101
|
+
select: async ({ from, select, params, ...modifiers }) => {
|
|
102
|
+
let q = client.from(from).select(select || '*')
|
|
103
|
+
q = applyFilters(q, params)
|
|
104
|
+
q = applyModifiers(q, modifiers)
|
|
105
|
+
return q
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
rpc: ({ from, params }) => client.rpc(from, params),
|
|
109
|
+
|
|
110
|
+
insert: ({ from, data, select }) =>
|
|
111
|
+
client.from(from).insert(data).select(select || '*'),
|
|
112
|
+
|
|
113
|
+
update: ({ from, data, params, select }) => {
|
|
114
|
+
let q = client.from(from).update(data)
|
|
115
|
+
q = applyFilters(q, params)
|
|
116
|
+
return q.select(select || '*')
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
upsert: ({ from, data, select }) =>
|
|
120
|
+
client.from(from).upsert(data).select(select || '*'),
|
|
121
|
+
|
|
122
|
+
delete: ({ from, params }) => {
|
|
123
|
+
let q = client.from(from).delete()
|
|
124
|
+
q = applyFilters(q, params)
|
|
125
|
+
return q
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Realtime
|
|
129
|
+
subscribe: ({ from, params, on }, callback) => {
|
|
130
|
+
const event = on || '*'
|
|
131
|
+
const channel = client
|
|
132
|
+
.channel(`db-${from}-${Date.now()}`)
|
|
133
|
+
.on('postgres_changes', {
|
|
134
|
+
event,
|
|
135
|
+
schema: 'public',
|
|
136
|
+
table: from,
|
|
137
|
+
...(params?.id ? { filter: `id=eq.${params.id}` } : {})
|
|
138
|
+
}, (payload) => callback(payload.new, payload.old, payload))
|
|
139
|
+
.subscribe()
|
|
140
|
+
|
|
141
|
+
return () => client.removeChannel(channel)
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// Storage
|
|
145
|
+
upload: ({ bucket, path, file, options }) =>
|
|
146
|
+
client.storage.from(bucket).upload(path, file, options),
|
|
147
|
+
|
|
148
|
+
download: ({ bucket, path }) =>
|
|
149
|
+
client.storage.from(bucket).download(path),
|
|
150
|
+
|
|
151
|
+
getPublicUrl: ({ bucket, path }) =>
|
|
152
|
+
client.storage.from(bucket).getPublicUrl(path)
|
|
153
|
+
})
|