@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,258 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted, watch } from 'vue'
|
|
3
|
+
import { useSupabaseUser } from '#imports'
|
|
4
|
+
import { useNsdbModel } from '#build/nsdb/registry'
|
|
5
|
+
import type { EntityRelation } from '@lucashw68/nsdb/types/entities'
|
|
6
|
+
|
|
7
|
+
type Primitive = string | number | null
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
relation: EntityRelation
|
|
11
|
+
value: Primitive | Record<string, any> // peut être un payload d'inline-create
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
placeholder?: string
|
|
14
|
+
inputId?: string
|
|
15
|
+
name?: string
|
|
16
|
+
required?: boolean
|
|
17
|
+
ariaInvalid?: boolean | 'true' | 'false' | 'grammar' | 'spelling'
|
|
18
|
+
ariaDescribedby?: string
|
|
19
|
+
store?: boolean
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{
|
|
23
|
+
(e: 'update:value', value: Primitive | Record<string, any>): void
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
const loading = ref(false)
|
|
27
|
+
const error = ref<string | null>(null)
|
|
28
|
+
|
|
29
|
+
const allowInlineCreate = computed(
|
|
30
|
+
() => !!props.relation.allowInlineCreate
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
// Mode : sélection existante / création inline
|
|
34
|
+
const mode = ref<'select' | 'create'>('select')
|
|
35
|
+
const newLabel = ref('')
|
|
36
|
+
|
|
37
|
+
// DX handle sur le modèle référencé (ex: playlists, profiles…)
|
|
38
|
+
const relatedModel = computed(() =>
|
|
39
|
+
useNsdbModel(props.relation.referencedTable, { store: props.store ?? false }) as unknown as {
|
|
40
|
+
items: { value?: any[] } | any[]
|
|
41
|
+
fetch: (query?: Record<string, any>) => Promise<any[]>
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
const supabaseUser = useSupabaseUser()
|
|
45
|
+
let loadSequence = 0
|
|
46
|
+
|
|
47
|
+
function isItemsRef(value: { value?: any[] } | any[]): value is { value?: any[] } {
|
|
48
|
+
return !Array.isArray(value) && typeof value === 'object' && value !== null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// items : Ref<any[]>
|
|
52
|
+
const rows = computed<any[]>(() => {
|
|
53
|
+
const raw = relatedModel.value.items
|
|
54
|
+
if (Array.isArray(raw)) return raw
|
|
55
|
+
if (isItemsRef(raw) && Array.isArray(raw.value)) return raw.value
|
|
56
|
+
return []
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// valueField : clé utilisée comme valeur (id par défaut)
|
|
60
|
+
const valueField = computed<string>(() => {
|
|
61
|
+
if (props.relation.referencedColumns?.length) {
|
|
62
|
+
return props.relation.referencedColumns[0] ?? 'id'
|
|
63
|
+
}
|
|
64
|
+
return 'id'
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// labelField : champ utilisé pour afficher l’option & créer inline
|
|
68
|
+
const labelField = computed<string>(() => {
|
|
69
|
+
if (props.relation.displayField) return props.relation.displayField
|
|
70
|
+
return valueField.value
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const options = computed(() => {
|
|
74
|
+
return rows.value.map((row: any) => ({
|
|
75
|
+
value: row?.[valueField.value] ?? null,
|
|
76
|
+
label: String(row?.[labelField.value] ?? row?.[valueField.value] ?? 'Inconnu'),
|
|
77
|
+
}))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
async function load() {
|
|
81
|
+
const requestId = ++loadSequence
|
|
82
|
+
loading.value = true
|
|
83
|
+
error.value = null
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await relatedModel.value.fetch({
|
|
87
|
+
limit: 200,
|
|
88
|
+
orderBy: valueField.value,
|
|
89
|
+
orderDirection: 'asc',
|
|
90
|
+
})
|
|
91
|
+
} catch (e: any) {
|
|
92
|
+
if (requestId !== loadSequence) return
|
|
93
|
+
console.error('[NsdbRelationSelect] load error:', e)
|
|
94
|
+
error.value = e?.message ?? 'Erreur de chargement des options'
|
|
95
|
+
} finally {
|
|
96
|
+
if (requestId === loadSequence) loading.value = false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function onSelectChange(event: Event) {
|
|
101
|
+
const target = event.target as HTMLSelectElement
|
|
102
|
+
const raw = target.value
|
|
103
|
+
const nextValue: Primitive = raw === '' ? null : raw
|
|
104
|
+
emit('update:value', nextValue)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Payload spécial pour l’inline-create
|
|
108
|
+
const inlineCreatePayload = computed<Record<string, any> | null>(() => {
|
|
109
|
+
if (!allowInlineCreate.value) return null
|
|
110
|
+
if (!props.relation.displayField) {
|
|
111
|
+
console.warn(
|
|
112
|
+
'[NsdbRelationSelect] inline create requires relation.displayField'
|
|
113
|
+
)
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
const label = newLabel.value?.trim()
|
|
117
|
+
if (!label) return null
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
__nsdbInlineCreate: true,
|
|
121
|
+
table: props.relation.referencedTable,
|
|
122
|
+
displayField: props.relation.displayField,
|
|
123
|
+
data: {
|
|
124
|
+
[props.relation.displayField]: label,
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
function onCreateInput(event: Event) {
|
|
130
|
+
const target = event.target as HTMLInputElement
|
|
131
|
+
newLabel.value = target.value
|
|
132
|
+
|
|
133
|
+
const payload = inlineCreatePayload.value
|
|
134
|
+
if (!payload) {
|
|
135
|
+
emit('update:value', null)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
emit('update:value', payload)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function switchToSelect() {
|
|
143
|
+
mode.value = 'select'
|
|
144
|
+
// si on repasse en select, on remet juste la valeur primitive
|
|
145
|
+
if (typeof props.value === 'object' && props.value !== null) {
|
|
146
|
+
emit('update:value', null)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function switchToCreate() {
|
|
151
|
+
if (!allowInlineCreate.value) return
|
|
152
|
+
mode.value = 'create'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
onMounted(load)
|
|
156
|
+
|
|
157
|
+
watch(
|
|
158
|
+
() => props.relation.referencedTable,
|
|
159
|
+
() => {
|
|
160
|
+
mode.value = 'select'
|
|
161
|
+
newLabel.value = ''
|
|
162
|
+
void load()
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
watch(
|
|
167
|
+
() => supabaseUser.value?.id ?? null,
|
|
168
|
+
() => {
|
|
169
|
+
const raw = relatedModel.value.items
|
|
170
|
+
if (Array.isArray(raw)) raw.splice(0)
|
|
171
|
+
else if (isItemsRef(raw)) raw.value = []
|
|
172
|
+
void load()
|
|
173
|
+
},
|
|
174
|
+
{ flush: 'sync' },
|
|
175
|
+
)
|
|
176
|
+
</script>
|
|
177
|
+
|
|
178
|
+
<template>
|
|
179
|
+
<div class="space-y-1">
|
|
180
|
+
<!-- Toggle Select / Create inline -->
|
|
181
|
+
<div
|
|
182
|
+
v-if="allowInlineCreate"
|
|
183
|
+
class="flex items-center justify-end gap-2 text-xs text-gray-500 mb-1"
|
|
184
|
+
>
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
class="px-2 py-1 rounded border text-gray-700 hover:bg-gray-50"
|
|
188
|
+
:class="{ 'bg-gray-100 font-semibold': mode === 'select' }"
|
|
189
|
+
@click="switchToSelect"
|
|
190
|
+
>
|
|
191
|
+
Sélectionner
|
|
192
|
+
</button>
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
class="px-2 py-1 rounded border text-gray-700 hover:bg-gray-50"
|
|
196
|
+
:class="{ 'bg-gray-100 font-semibold': mode === 'create' }"
|
|
197
|
+
@click="switchToCreate"
|
|
198
|
+
>
|
|
199
|
+
Créer
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<!-- MODE SELECT -->
|
|
204
|
+
<select
|
|
205
|
+
v-if="mode === 'select' || !allowInlineCreate"
|
|
206
|
+
class="border text-black rounded px-3 py-2 w-full text-sm"
|
|
207
|
+
:id="inputId"
|
|
208
|
+
:name="name"
|
|
209
|
+
:disabled="disabled || loading"
|
|
210
|
+
:required="required"
|
|
211
|
+
:aria-invalid="ariaInvalid"
|
|
212
|
+
:aria-describedby="ariaDescribedby"
|
|
213
|
+
:value="typeof value === 'object' && value !== null ? '' : (value ?? '')"
|
|
214
|
+
@change="onSelectChange"
|
|
215
|
+
>
|
|
216
|
+
<option value="" :disabled="required">
|
|
217
|
+
{{ placeholder || (loading ? 'Chargement…' : 'Sélectionner…') }}
|
|
218
|
+
</option>
|
|
219
|
+
|
|
220
|
+
<option
|
|
221
|
+
v-for="opt in options"
|
|
222
|
+
:key="opt.value ?? 'null'"
|
|
223
|
+
:value="opt.value ?? ''"
|
|
224
|
+
>
|
|
225
|
+
{{ opt.label }}
|
|
226
|
+
</option>
|
|
227
|
+
</select>
|
|
228
|
+
|
|
229
|
+
<!-- MODE CREATE INLINE -->
|
|
230
|
+
<div v-else class="space-y-1">
|
|
231
|
+
<input
|
|
232
|
+
type="text"
|
|
233
|
+
:id="inputId"
|
|
234
|
+
:name="name"
|
|
235
|
+
class="border text-black rounded px-3 py-2 w-full text-sm"
|
|
236
|
+
:disabled="disabled || loading"
|
|
237
|
+
:required="required"
|
|
238
|
+
:aria-invalid="ariaInvalid"
|
|
239
|
+
:aria-describedby="ariaDescribedby"
|
|
240
|
+
:placeholder="placeholder || 'Créer un nouvel élément…'"
|
|
241
|
+
:value="newLabel"
|
|
242
|
+
@input="onCreateInput"
|
|
243
|
+
/>
|
|
244
|
+
<p class="text-[10px] text-gray-500">
|
|
245
|
+
Un nouvel élément sera créé dans « {{ relation.referencedTable }} » et lié
|
|
246
|
+
automatiquement.
|
|
247
|
+
</p>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<p
|
|
251
|
+
v-if="error"
|
|
252
|
+
class="text-xs text-red-500"
|
|
253
|
+
role="alert"
|
|
254
|
+
>
|
|
255
|
+
{{ error }}
|
|
256
|
+
</p>
|
|
257
|
+
</div>
|
|
258
|
+
</template>
|