@lucashw68/nsdb 1.0.0-rc.2
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/CHANGELOG.md +34 -0
- package/GET_STARTED.md +709 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/cli/index.js +83 -0
- package/helpers/args.js +22 -0
- package/helpers/config.js +142 -0
- package/helpers/generated.js +48 -0
- package/helpers/io.js +39 -0
- package/helpers/metadata.js +19 -0
- package/helpers/names.js +16 -0
- package/helpers/relations.js +101 -0
- package/helpers/shell.js +15 -0
- package/helpers/tables.js +79 -0
- package/helpers/ts.js +37 -0
- package/module.ts +151 -0
- package/nsdb.config.example.mjs +39 -0
- package/nsdb.config.example.ts +42 -0
- package/package.json +114 -0
- package/runtime/components/Form/NsdbRelationSelect.vue +258 -0
- package/runtime/components/NsdbForm.vue +865 -0
- package/runtime/components/NsdbList.vue +961 -0
- package/runtime/composables/useNsdbProfile.ts +119 -0
- package/runtime/composables/useNsdbSchemas.ts +176 -0
- package/runtime/composables/useSupabaseApi.ts +177 -0
- package/runtime/composables/useSupabaseApiStorage.ts +337 -0
- package/runtime/composables/useSupabaseModels.ts +412 -0
- package/runtime/query.ts +126 -0
- package/runtime/stores/createDbStore.ts +439 -0
- package/runtime/stores/createSingletonDbStore.ts +67 -0
- package/runtime/utils/dataFreshness.ts +47 -0
- package/runtime/utils/storage.ts +41 -0
- package/scripts/clear.js +64 -0
- package/scripts/generate-composables.js +100 -0
- package/scripts/generate-enums.js +106 -0
- package/scripts/generate-metadata.js +165 -0
- package/scripts/generate-models.js +164 -0
- package/scripts/generate-schemas.js +443 -0
- package/scripts/generate-stores.js +90 -0
- package/scripts/generate-types.js +196 -0
- package/scripts/init.js +225 -0
- package/templates/model.template.ts +48 -0
- package/templates/schema.template.ts +13 -0
- package/templates/useNsdbModel.template.ts +9 -0
- package/types/config.ts +50 -0
- package/types/entities.ts +66 -0
- package/types/index.ts +14 -0
- package/types/list.ts +78 -0
- package/types/model.ts +57 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { computed, ref, watch } from 'vue'
|
|
2
|
+
import { useSupabaseUser } from '#imports'
|
|
3
|
+
import { useSupabaseApi } from './useSupabaseApi'
|
|
4
|
+
|
|
5
|
+
export type NsdbProfileDefaults<TProfile extends Record<string, any>> = (
|
|
6
|
+
user: any
|
|
7
|
+
) => Partial<TProfile>
|
|
8
|
+
|
|
9
|
+
export interface UseNsdbProfileOptions<TProfile extends Record<string, any>> {
|
|
10
|
+
table?: string
|
|
11
|
+
select?: string
|
|
12
|
+
userColumn?: string
|
|
13
|
+
idColumn?: keyof TProfile | string
|
|
14
|
+
createIfMissing?: boolean
|
|
15
|
+
defaults?: NsdbProfileDefaults<TProfile>
|
|
16
|
+
immediate?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useNsdbProfile<TProfile extends Record<string, any> = Record<string, any>>(
|
|
20
|
+
options: UseNsdbProfileOptions<TProfile> = {}
|
|
21
|
+
) {
|
|
22
|
+
const {
|
|
23
|
+
table = 'profiles',
|
|
24
|
+
select = '*',
|
|
25
|
+
userColumn = 'user_id',
|
|
26
|
+
idColumn = 'id',
|
|
27
|
+
createIfMissing = false,
|
|
28
|
+
defaults = (user: any) => ({ [userColumn]: user.id, email: user.email } as unknown as Partial<TProfile>),
|
|
29
|
+
immediate = true,
|
|
30
|
+
} = options
|
|
31
|
+
|
|
32
|
+
const user = useSupabaseUser()
|
|
33
|
+
const api = useSupabaseApi()
|
|
34
|
+
const profile = ref<TProfile | null>(null)
|
|
35
|
+
const loading = ref(false)
|
|
36
|
+
const error = ref<unknown>(null)
|
|
37
|
+
|
|
38
|
+
const profileId = computed(() => {
|
|
39
|
+
const currentProfile = profile.value as Record<string, any> | null
|
|
40
|
+
return currentProfile?.[idColumn as string] ?? null
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
async function createProfile() {
|
|
44
|
+
if (!user.value?.id) return null
|
|
45
|
+
|
|
46
|
+
const payload = defaults(user.value)
|
|
47
|
+
const response = await api.create<TProfile>(table, payload)
|
|
48
|
+
|
|
49
|
+
if (!response.success) {
|
|
50
|
+
throw response.error
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
profile.value = response.data ?? null
|
|
54
|
+
return profile.value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function refresh() {
|
|
58
|
+
if (!user.value?.id) {
|
|
59
|
+
profile.value = null
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
loading.value = true
|
|
64
|
+
error.value = null
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await api.all<TProfile>(table, {
|
|
68
|
+
select,
|
|
69
|
+
where: {
|
|
70
|
+
[userColumn]: user.value.id,
|
|
71
|
+
},
|
|
72
|
+
limit: 1,
|
|
73
|
+
offset: 0,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (!response.success) {
|
|
77
|
+
throw response.error
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
profile.value = response.data?.[0] ?? null
|
|
81
|
+
|
|
82
|
+
if (!profile.value && createIfMissing) {
|
|
83
|
+
return await createProfile()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return profile.value
|
|
87
|
+
} catch (profileError) {
|
|
88
|
+
error.value = profileError
|
|
89
|
+
throw profileError
|
|
90
|
+
} finally {
|
|
91
|
+
loading.value = false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function ensureProfile() {
|
|
96
|
+
if (profile.value) return profile.value
|
|
97
|
+
return await refresh()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (immediate) {
|
|
101
|
+
watch(
|
|
102
|
+
() => user.value?.id ?? null,
|
|
103
|
+
() => {
|
|
104
|
+
refresh().catch(() => {})
|
|
105
|
+
},
|
|
106
|
+
{ immediate: true }
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
user,
|
|
112
|
+
profile,
|
|
113
|
+
profileId,
|
|
114
|
+
loading,
|
|
115
|
+
error,
|
|
116
|
+
refresh,
|
|
117
|
+
ensureProfile,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// @lucashw68/nsdb/runtime/composables/useNsdbSchemas.ts
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { EntityField, EntityRelation } from '@lucashw68/nsdb/types/entities'
|
|
4
|
+
import type { ModelQuery } from '@lucashw68/nsdb/types/model'
|
|
5
|
+
|
|
6
|
+
type Schema = Record<string, EntityField>
|
|
7
|
+
|
|
8
|
+
export function useNsdbSchema(
|
|
9
|
+
schema: Schema | null | undefined,
|
|
10
|
+
relations: EntityRelation[] = [],
|
|
11
|
+
) {
|
|
12
|
+
// Sécurise le schema pour éviter les erreurs si null / undefined
|
|
13
|
+
const safeSchema: Schema = schema && typeof schema === 'object'
|
|
14
|
+
? schema
|
|
15
|
+
: ({} as Schema)
|
|
16
|
+
|
|
17
|
+
// ------------------------------------------------------------
|
|
18
|
+
// Champs & editableKeys
|
|
19
|
+
// ------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const fields = Object.keys(safeSchema)
|
|
22
|
+
|
|
23
|
+
const editableKeys = computed(() =>
|
|
24
|
+
Object.entries(safeSchema)
|
|
25
|
+
.filter(([, field]) => !field.readonly && field.editable !== false && !field.serverOnly)
|
|
26
|
+
.map(([key]) => key)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// ------------------------------------------------------------
|
|
30
|
+
// Fabrique d'objet vide basé sur le schema
|
|
31
|
+
// ------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function createDraftFromSchema(): Record<string, unknown> {
|
|
34
|
+
const result: Record<string, any> = {}
|
|
35
|
+
|
|
36
|
+
for (const [key, definition] of Object.entries(safeSchema)) {
|
|
37
|
+
if (definition.readonly || definition.editable === false || definition.serverOnly) continue
|
|
38
|
+
|
|
39
|
+
if ('default' in definition) {
|
|
40
|
+
result[key] = definition.default
|
|
41
|
+
} else if (definition.hasDefault) {
|
|
42
|
+
// Keep the key renderable while omitting it from create payloads.
|
|
43
|
+
result[key] = undefined
|
|
44
|
+
} else if (definition.nullable) {
|
|
45
|
+
result[key] = null
|
|
46
|
+
} else if (definition.type === 'checkbox') {
|
|
47
|
+
result[key] = false
|
|
48
|
+
} else {
|
|
49
|
+
result[key] = null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @deprecated Use `createDraftFromSchema()`. Scheduled for removal before 1.0. */
|
|
57
|
+
const emptyFromSchema = createDraftFromSchema
|
|
58
|
+
|
|
59
|
+
// ------------------------------------------------------------
|
|
60
|
+
// Helpers pour les relations → select Supabase
|
|
61
|
+
// ------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
function aliasFromColumn(column: string, relation: EntityRelation) {
|
|
64
|
+
if (typeof column === 'string' && column.endsWith('_id')) {
|
|
65
|
+
const base = column.slice(0, -3)
|
|
66
|
+
return base || relation.referencedTable
|
|
67
|
+
}
|
|
68
|
+
return relation.referencedTable
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Construit une chaîne `select` pour Supabase à partir d'un schema.
|
|
73
|
+
* Exemple : "*, playlist:playlists(*), profile:profiles(*)"
|
|
74
|
+
*
|
|
75
|
+
* - Si aucun schema n'est fourni, on utilise celui passé au hook.
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
function buildSelectFromSchema(
|
|
79
|
+
schema: Schema | null | undefined = safeSchema,
|
|
80
|
+
baseSelect?: string,
|
|
81
|
+
include?: readonly string[],
|
|
82
|
+
): string {
|
|
83
|
+
if (!schema || typeof schema !== 'object') {
|
|
84
|
+
console.warn('[buildSelectFromSchema] invalid schema, returning baseSelect only')
|
|
85
|
+
return String(baseSelect ?? '*')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const selectedBase = baseSelect ?? Object.entries(schema)
|
|
89
|
+
.filter(([, field]) => field.selectable !== false && !field.serverOnly)
|
|
90
|
+
.map(([column]) => column)
|
|
91
|
+
.join(', ')
|
|
92
|
+
|
|
93
|
+
const relationParts: string[] = []
|
|
94
|
+
if (include) {
|
|
95
|
+
for (const alias of include) {
|
|
96
|
+
const relation = relations.find(candidate => candidate.alias === alias)
|
|
97
|
+
if (!relation) {
|
|
98
|
+
throw new Error(`[nsdb] Unknown relation include "${alias}".`)
|
|
99
|
+
}
|
|
100
|
+
const resource = relation.embedResource ?? relation.referencedTable
|
|
101
|
+
const needsConstraintHint = relation.direction !== 'through' && resource === relation.referencedTable
|
|
102
|
+
const fkSuffix = needsConstraintHint && relation.foreignKeyName ? `!${relation.foreignKeyName}` : ''
|
|
103
|
+
relationParts.push(`${alias}:${resource}${fkSuffix}(*)`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const [column, field] of include ? [] : Object.entries(schema)) {
|
|
108
|
+
if (!field || typeof field !== 'object') continue
|
|
109
|
+
if (!field.relation) continue
|
|
110
|
+
|
|
111
|
+
const rel = field.relation
|
|
112
|
+
|
|
113
|
+
// Pour l'instant, tous les belongsTo et hasOne sont inclus.
|
|
114
|
+
// Les hasMany sont exclus pour éviter les charges trop lourdes.
|
|
115
|
+
// À affiner, par exemple, filtrer certains hasOne.
|
|
116
|
+
if (rel.kind !== 'belongsTo' && rel.kind !== 'hasOne') continue
|
|
117
|
+
|
|
118
|
+
const alias = rel.alias ?? aliasFromColumn(column, rel)
|
|
119
|
+
const resource = rel.embedResource ?? rel.referencedTable
|
|
120
|
+
const needsConstraintHint = resource === rel.referencedTable
|
|
121
|
+
const fkSuffix = needsConstraintHint && rel.foreignKeyName ? `!${rel.foreignKeyName}` : ''
|
|
122
|
+
|
|
123
|
+
const part = `${alias}:${resource}${fkSuffix}(*)`
|
|
124
|
+
relationParts.push(part)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!relationParts.length) {
|
|
128
|
+
return String(selectedBase || '*')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return [String(selectedBase || '*'), ...relationParts].join(', ')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ------------------------------------------------------------
|
|
135
|
+
// bindModel : plugge un useSupabaseModel sur le schema
|
|
136
|
+
// ------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
function bindModel<TRow, TRelations extends Record<string, unknown> = Record<never, never>>(model: {
|
|
139
|
+
fetch: (query?: ModelQuery<string, Extract<keyof TRow, string>>) => Promise<TRow[]>
|
|
140
|
+
refresh: (query?: ModelQuery<string, Extract<keyof TRow, string>>) => Promise<TRow[]>
|
|
141
|
+
}) {
|
|
142
|
+
/**
|
|
143
|
+
* Récupère une liste d'éléments avec :
|
|
144
|
+
* - select auto (relations) basé sur le schema
|
|
145
|
+
* - support de where, orderBy, limit, offset via model.fetch(query)
|
|
146
|
+
*/
|
|
147
|
+
const fetch = async <TInclude extends keyof TRelations & string = never>(
|
|
148
|
+
query: ModelQuery<TInclude, Extract<keyof TRow, string>> = {},
|
|
149
|
+
): Promise<Array<TRow & Pick<TRelations, TInclude>>> => {
|
|
150
|
+
const select = query.select ?? buildSelectFromSchema(safeSchema, undefined, query.include)
|
|
151
|
+
const finalQuery = { ...query, select }
|
|
152
|
+
|
|
153
|
+
const rows = await model.fetch(finalQuery)
|
|
154
|
+
return Array.isArray(rows) ? (rows as Array<TRow & Pick<TRelations, TInclude>>) : []
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const refresh = async <TInclude extends keyof TRelations & string = never>(
|
|
158
|
+
query: ModelQuery<TInclude, Extract<keyof TRow, string>> = {},
|
|
159
|
+
): Promise<Array<TRow & Pick<TRelations, TInclude>>> => {
|
|
160
|
+
const select = query.select ?? buildSelectFromSchema(safeSchema, undefined, query.include)
|
|
161
|
+
const rows = await model.refresh({ ...query, select })
|
|
162
|
+
return Array.isArray(rows) ? (rows as Array<TRow & Pick<TRelations, TInclude>>) : []
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { fetch, refresh }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
fields,
|
|
170
|
+
editableKeys,
|
|
171
|
+
buildSelectFromSchema,
|
|
172
|
+
createDraftFromSchema,
|
|
173
|
+
emptyFromSchema,
|
|
174
|
+
bindModel,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { useSupabaseClient } from '#imports'
|
|
2
|
+
import type {
|
|
3
|
+
ListOptions,
|
|
4
|
+
} from '@lucashw68/nsdb/types/list'
|
|
5
|
+
import { applyListOptions, applySearch, applyWhereFilters } from '../query'
|
|
6
|
+
|
|
7
|
+
type QueryBuilder = any
|
|
8
|
+
type MutationPayload = Record<string, unknown> | Record<string, unknown>[]
|
|
9
|
+
|
|
10
|
+
export interface SupabaseApiSuccess<T> {
|
|
11
|
+
success: true
|
|
12
|
+
error: undefined
|
|
13
|
+
data: T | null
|
|
14
|
+
count: number | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SupabaseApiFailure<T> {
|
|
18
|
+
success: false
|
|
19
|
+
error: unknown
|
|
20
|
+
data: T
|
|
21
|
+
count: number | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SupabaseApiResponse<T> = SupabaseApiSuccess<T> | SupabaseApiFailure<T>
|
|
25
|
+
export type SupabaseApiListResponse<T> = SupabaseApiResponse<T[]>
|
|
26
|
+
|
|
27
|
+
/** #########################################################
|
|
28
|
+
* Handle Responses
|
|
29
|
+
* ##########################################################
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
function handleResponse<T>(
|
|
33
|
+
payload: { data: T | null; error: unknown; count?: number | null },
|
|
34
|
+
context: string
|
|
35
|
+
): SupabaseApiResponse<T> {
|
|
36
|
+
const { data, error, count } = payload
|
|
37
|
+
|
|
38
|
+
if (error) {
|
|
39
|
+
console.error(`❌ [${context}]`, error)
|
|
40
|
+
return {
|
|
41
|
+
success: false as const,
|
|
42
|
+
error,
|
|
43
|
+
data: undefined as unknown as T,
|
|
44
|
+
count: count ?? null,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
success: true as const,
|
|
50
|
+
error: undefined,
|
|
51
|
+
data,
|
|
52
|
+
count: count ?? null,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleListResponse<T>(
|
|
57
|
+
data: T[] | null,
|
|
58
|
+
count: number | null,
|
|
59
|
+
error: unknown,
|
|
60
|
+
context: string
|
|
61
|
+
) {
|
|
62
|
+
if (error) {
|
|
63
|
+
console.error(`❌ [${context}]`, error)
|
|
64
|
+
return {
|
|
65
|
+
success: false as const,
|
|
66
|
+
error,
|
|
67
|
+
data: [] as T[],
|
|
68
|
+
count: null as number | null,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
success: true as const,
|
|
74
|
+
error: undefined,
|
|
75
|
+
data: (data ?? []) as T[],
|
|
76
|
+
count: typeof count === 'number' ? count : null,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Canonical low-level response-object API. */
|
|
81
|
+
|
|
82
|
+
export const useSupabaseApi = () => {
|
|
83
|
+
const supabaseClient = useSupabaseClient?.()
|
|
84
|
+
if (!supabaseClient) {
|
|
85
|
+
throw new Error('[nsdb] Supabase client not found. Install @nuxtjs/supabase.')
|
|
86
|
+
}
|
|
87
|
+
// This advanced API deliberately accepts runtime table names. Recent typed
|
|
88
|
+
// Supabase clients narrow `from()` to generated relations, so keep the
|
|
89
|
+
// unavoidable dynamic cast at this single low-level escape-hatch boundary.
|
|
90
|
+
const from = (resource: string): QueryBuilder => (
|
|
91
|
+
supabaseClient.from as unknown as (relation: string) => QueryBuilder
|
|
92
|
+
)(resource)
|
|
93
|
+
|
|
94
|
+
async function all<T = any>(
|
|
95
|
+
resource: string,
|
|
96
|
+
options: ListOptions = {},
|
|
97
|
+
): Promise<SupabaseApiListResponse<T>> {
|
|
98
|
+
const selectClause = options.select ?? '*'
|
|
99
|
+
|
|
100
|
+
let q: QueryBuilder = from(resource).select(selectClause, { count: 'exact' })
|
|
101
|
+
q = applyWhereFilters(q, options.where)
|
|
102
|
+
q = applySearch(q, options)
|
|
103
|
+
q = applyListOptions(q, options)
|
|
104
|
+
|
|
105
|
+
const { data, error, count } = await q
|
|
106
|
+
return handleListResponse<T>(data, count ?? null, error, `ALL ${resource}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function getById<T = any>(
|
|
110
|
+
resource: string,
|
|
111
|
+
id: string | number,
|
|
112
|
+
options: { key?: string; select?: string } = {},
|
|
113
|
+
) {
|
|
114
|
+
const { data, error } = await from(resource)
|
|
115
|
+
.select(options.select ?? '*')
|
|
116
|
+
.eq(options.key ?? 'id', id)
|
|
117
|
+
.limit(1)
|
|
118
|
+
.single()
|
|
119
|
+
|
|
120
|
+
return handleResponse<T>({ data, error }, `GET ${resource}/${id}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function create<T = any>(resource: string, payload: MutationPayload) {
|
|
124
|
+
const { data, error } = await from(resource).insert(payload).select().single()
|
|
125
|
+
return handleResponse<T>({ data, error }, `CREATE ${resource}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function update<T = any>(
|
|
129
|
+
resource: string,
|
|
130
|
+
id: string | number,
|
|
131
|
+
payload: MutationPayload,
|
|
132
|
+
options: { key?: string } = {},
|
|
133
|
+
) {
|
|
134
|
+
const { data, error } = await from(resource).update(payload).eq(options.key ?? 'id', id).select()
|
|
135
|
+
return handleResponse<T | T[]>({ data, error }, `UPDATE ${resource}/${id}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function remove(resource: string, id: string | number, options: { key?: string } = {}) {
|
|
139
|
+
const { data, error } = await from(resource).delete().eq(options.key ?? 'id', id)
|
|
140
|
+
return handleResponse({ data, error }, `DELETE ${resource}/${id}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function upsert<T = any>(resource: string, payload: MutationPayload, options: { onConflict?: string } = {}) {
|
|
144
|
+
const upsertOptions = options.onConflict ? { onConflict: options.onConflict } : undefined
|
|
145
|
+
const q: QueryBuilder = from(resource)
|
|
146
|
+
.upsert(payload, upsertOptions)
|
|
147
|
+
.select()
|
|
148
|
+
const { data, error } = await q
|
|
149
|
+
return handleResponse<T | T[]>({ data, error }, `UPSERT ${resource}`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function count(resource: string, where?: { property: string; value: string | number }) {
|
|
153
|
+
let q: QueryBuilder = from(resource).select('*', { count: 'exact', head: true })
|
|
154
|
+
if (where) q = q.eq(where.property, where.value)
|
|
155
|
+
const { count, error } = await q
|
|
156
|
+
return handleResponse<number | null>({ data: count ?? null, error }, `COUNT ${resource}`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function findOne<T = any>(resource: string, options: ListOptions) {
|
|
160
|
+
const { select = '*', where } = options
|
|
161
|
+
let q: QueryBuilder = from(resource).select(select)
|
|
162
|
+
q = applyWhereFilters(q, where)
|
|
163
|
+
const { data, error } = await q.limit(1).single()
|
|
164
|
+
return handleResponse<T>({ data, error }, `FIND ONE ${resource}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
all,
|
|
169
|
+
getById,
|
|
170
|
+
create,
|
|
171
|
+
update,
|
|
172
|
+
remove,
|
|
173
|
+
upsert,
|
|
174
|
+
count,
|
|
175
|
+
findOne,
|
|
176
|
+
}
|
|
177
|
+
}
|