@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/GET_STARTED.md +709 -0
  3. package/LICENSE +21 -0
  4. package/README.md +159 -0
  5. package/cli/index.js +83 -0
  6. package/helpers/args.js +22 -0
  7. package/helpers/config.js +142 -0
  8. package/helpers/generated.js +48 -0
  9. package/helpers/io.js +39 -0
  10. package/helpers/metadata.js +19 -0
  11. package/helpers/names.js +16 -0
  12. package/helpers/relations.js +101 -0
  13. package/helpers/shell.js +15 -0
  14. package/helpers/tables.js +79 -0
  15. package/helpers/ts.js +37 -0
  16. package/module.ts +151 -0
  17. package/nsdb.config.example.mjs +39 -0
  18. package/nsdb.config.example.ts +42 -0
  19. package/package.json +114 -0
  20. package/runtime/components/Form/NsdbRelationSelect.vue +258 -0
  21. package/runtime/components/NsdbForm.vue +865 -0
  22. package/runtime/components/NsdbList.vue +961 -0
  23. package/runtime/composables/useNsdbProfile.ts +119 -0
  24. package/runtime/composables/useNsdbSchemas.ts +176 -0
  25. package/runtime/composables/useSupabaseApi.ts +177 -0
  26. package/runtime/composables/useSupabaseApiStorage.ts +337 -0
  27. package/runtime/composables/useSupabaseModels.ts +412 -0
  28. package/runtime/query.ts +126 -0
  29. package/runtime/stores/createDbStore.ts +439 -0
  30. package/runtime/stores/createSingletonDbStore.ts +67 -0
  31. package/runtime/utils/dataFreshness.ts +47 -0
  32. package/runtime/utils/storage.ts +41 -0
  33. package/scripts/clear.js +64 -0
  34. package/scripts/generate-composables.js +100 -0
  35. package/scripts/generate-enums.js +106 -0
  36. package/scripts/generate-metadata.js +165 -0
  37. package/scripts/generate-models.js +164 -0
  38. package/scripts/generate-schemas.js +443 -0
  39. package/scripts/generate-stores.js +90 -0
  40. package/scripts/generate-types.js +196 -0
  41. package/scripts/init.js +225 -0
  42. package/templates/model.template.ts +48 -0
  43. package/templates/schema.template.ts +13 -0
  44. package/templates/useNsdbModel.template.ts +9 -0
  45. package/types/config.ts +50 -0
  46. package/types/entities.ts +66 -0
  47. package/types/index.ts +14 -0
  48. package/types/list.ts +78 -0
  49. 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>