@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,439 @@
1
+ import { computed, shallowRef, ref, watch } from 'vue'
2
+ import { defineStore, useSupabaseClient, useSupabaseUser } from '#imports'
3
+ import { useSupabaseApi } from '../composables/useSupabaseApi'
4
+ import { isComplexCollectionQuery, sortCollection, stableQueryKey } from '../utils/dataFreshness'
5
+ import type { ListOptions } from '@lucashw68/nsdb/types/list'
6
+
7
+ export type DbStoreFetchOptions = ListOptions & {
8
+ merge?: boolean
9
+ staleTimeMs?: number
10
+ }
11
+
12
+ export function createDbStore<T extends Record<string, any>>(resource: string, options: {
13
+ key?: keyof T
14
+ orderBy?: keyof T
15
+ defaultSort?: 'asc' | 'desc'
16
+ staleTimeMs?: number
17
+ persist?: boolean
18
+ scopeToUser?: boolean
19
+ }) {
20
+ const key = options.key || 'id'
21
+ const orderBy = options.orderBy || key
22
+ const sortDir = options.defaultSort || 'desc'
23
+ const defaultStaleTimeMs = options.staleTimeMs ?? 30_000
24
+ const shouldPersist = options.persist ?? false
25
+ const shouldScopeToUser = options.scopeToUser ?? true
26
+
27
+ return defineStore(`db_${resource}`, () => {
28
+ const supabase = useSupabaseClient()
29
+ const supabaseUser = useSupabaseUser()
30
+ const api = useSupabaseApi()
31
+ const items = shallowRef<T[]>([])
32
+ const totalCount = ref<number | null>(null)
33
+ const loading = ref(false)
34
+ const error = ref<any>(null)
35
+ const lastFetchedAt = ref<number | null>(null)
36
+ const lastQueryKey = ref<string | null>(null)
37
+ const scopeOwnerId = ref<string | null>(null)
38
+ const hydrationReady = ref(!shouldPersist || !shouldScopeToUser)
39
+ const stale = ref(true)
40
+ const cachedQueries = new Map<string, { rows: T[]; count: number | null; fetchedAt: number }>()
41
+ const inFlightQueries = new Map<string, Promise<T[]>>()
42
+ const maxCachedQueries = 20
43
+ let hydrationValidation: Promise<void> | null = null
44
+ let currentQuery: ListOptions = getDefaultQuery()
45
+ let collectionRevision = 0
46
+ let fetchSequence = 0
47
+ let subscription: ReturnType<typeof supabase.channel> | null = null
48
+ let subscriptionOwnerId: string | null = null
49
+ let realtimeRequested = false
50
+ let realtimeDisconnected = false
51
+
52
+ function resetData() {
53
+ items.value = []
54
+ totalCount.value = null
55
+ loading.value = false
56
+ error.value = null
57
+ lastFetchedAt.value = null
58
+ lastQueryKey.value = null
59
+ stale.value = true
60
+ cachedQueries.clear()
61
+ inFlightQueries.clear()
62
+ collectionRevision++
63
+ }
64
+
65
+ function reconcileUserScope(userId: string | null) {
66
+ if (!shouldScopeToUser) return
67
+ if (scopeOwnerId.value !== userId) {
68
+ void unsubscribe(false)
69
+ resetData()
70
+ }
71
+ scopeOwnerId.value = userId
72
+ if (realtimeRequested && userId) queueMicrotask(() => subscribe())
73
+ }
74
+
75
+ function reset() {
76
+ realtimeRequested = false
77
+ void unsubscribe(false)
78
+ resetData()
79
+ if (shouldScopeToUser) scopeOwnerId.value = supabaseUser.value?.id ?? null
80
+ }
81
+
82
+ function quarantineHydratedState() {
83
+ if (!shouldPersist || !shouldScopeToUser) {
84
+ hydrationReady.value = true
85
+ return Promise.resolve()
86
+ }
87
+
88
+ const hydrated = {
89
+ items: [...items.value],
90
+ totalCount: totalCount.value,
91
+ lastFetchedAt: lastFetchedAt.value,
92
+ lastQueryKey: lastQueryKey.value,
93
+ ownerId: scopeOwnerId.value,
94
+ }
95
+
96
+ // Persisted rows must disappear synchronously, before Vue can render them.
97
+ resetData()
98
+ scopeOwnerId.value = null
99
+ hydrationReady.value = false
100
+
101
+ hydrationValidation = (async () => {
102
+ try {
103
+ const { data, error: userError } = await supabase.auth.getUser()
104
+ const resolvedUserId = userError ? null : data?.user?.id ?? null
105
+
106
+ if (resolvedUserId && hydrated.ownerId === resolvedUserId) {
107
+ items.value = hydrated.items
108
+ totalCount.value = hydrated.totalCount
109
+ lastFetchedAt.value = hydrated.lastFetchedAt
110
+ lastQueryKey.value = hydrated.lastQueryKey
111
+ }
112
+
113
+ scopeOwnerId.value = resolvedUserId
114
+ } finally {
115
+ hydrationReady.value = true
116
+ hydrationValidation = null
117
+ }
118
+ })()
119
+
120
+ return hydrationValidation
121
+ }
122
+
123
+ async function waitForHydrationValidation() {
124
+ if (hydrationValidation) await hydrationValidation
125
+ }
126
+
127
+ if (shouldScopeToUser) {
128
+ watch(
129
+ () => supabaseUser.value?.id ?? null,
130
+ (newUserId) => reconcileUserScope(newUserId),
131
+ { immediate: true },
132
+ )
133
+ }
134
+
135
+ function getItemKey(item: Partial<T> | Record<string, any>) {
136
+ return item?.[key as string] as string | number | undefined
137
+ }
138
+
139
+ function getDefaultQuery(): ListOptions {
140
+ return {
141
+ orderBy: String(orderBy),
142
+ orderDirection: sortDir,
143
+ limit: 100,
144
+ offset: 0,
145
+ }
146
+ }
147
+
148
+ function getQuerySignature(query: ListOptions) {
149
+ return stableQueryKey({
150
+ select: query.select ?? '*',
151
+ where: query.where ?? null,
152
+ orderBy: query.orderBy ?? null,
153
+ orderDirection: query.orderDirection ?? null,
154
+ orderForeignTable: query.orderForeignTable ?? null,
155
+ limit: query.limit ?? null,
156
+ offset: query.offset ?? null,
157
+ search: query.search ?? null,
158
+ searchColumns: query.searchColumns ?? [],
159
+ })
160
+ }
161
+
162
+ const cachedCount = computed(() => items.value.length)
163
+
164
+ const mergeItems = (newItems: T[]) => {
165
+ const map = new Map<string | number | undefined, T>(items.value.map(item => [getItemKey(item), item]))
166
+ for (const newItem of newItems) {
167
+ map.set(getItemKey(newItem), newItem)
168
+ }
169
+ items.value = Array.from(map.values()).filter(Boolean) as T[]
170
+ }
171
+
172
+ const replaceItems = (newItems: T[]) => {
173
+ items.value = [...newItems]
174
+ }
175
+
176
+ const addOrUpdate = (item: T) => {
177
+ const itemKey = getItemKey(item)
178
+ const index = items.value.findIndex(candidate => getItemKey(candidate) === itemKey)
179
+ if (index !== -1) {
180
+ const nextItems = [...items.value]
181
+ nextItems[index] = item
182
+ items.value = nextItems
183
+ }
184
+ else items.value = [item, ...items.value]
185
+ }
186
+
187
+ const removeLocal = (id: string | number) => {
188
+ items.value = items.value.filter(item => getItemKey(item) !== id)
189
+ }
190
+
191
+ function rememberQuery(signature: string, entry: { rows: T[]; count: number | null; fetchedAt: number }) {
192
+ cachedQueries.delete(signature)
193
+ cachedQueries.set(signature, entry)
194
+ while (cachedQueries.size > maxCachedQueries) {
195
+ const oldest = cachedQueries.keys().next().value
196
+ if (oldest === undefined) break
197
+ cachedQueries.delete(oldest)
198
+ }
199
+ }
200
+
201
+ function invalidate() {
202
+ collectionRevision++
203
+ cachedQueries.clear()
204
+ lastQueryKey.value = null
205
+ stale.value = true
206
+ }
207
+
208
+ function markMutation() {
209
+ invalidate()
210
+ if (isComplexCollectionQuery(currentQuery)) totalCount.value = null
211
+ }
212
+
213
+ const getById = (id: string | number) => {
214
+ return items.value.find(item => getItemKey(item) === id) || null
215
+ }
216
+
217
+ const fetchFromSupabaseInternal = async (query: DbStoreFetchOptions = {}, bypassCache = false) => {
218
+ await waitForHydrationValidation()
219
+ reconcileUserScope(supabaseUser.value?.id ?? null)
220
+ const {
221
+ merge = false,
222
+ staleTimeMs = defaultStaleTimeMs,
223
+ ...queryOptions
224
+ } = query
225
+ const finalQuery: ListOptions = {
226
+ ...getDefaultQuery(),
227
+ ...queryOptions,
228
+ }
229
+ const querySignature = getQuerySignature(finalQuery)
230
+ currentQuery = finalQuery
231
+ let cachedQuery = cachedQueries.get(querySignature)
232
+ const now = Date.now()
233
+ if (!cachedQuery && lastQueryKey.value === querySignature && lastFetchedAt.value != null) {
234
+ cachedQuery = { rows: [...items.value], count: totalCount.value, fetchedAt: lastFetchedAt.value }
235
+ }
236
+
237
+ if (!bypassCache && cachedQuery && now - cachedQuery.fetchedAt < staleTimeMs) {
238
+ if (merge) mergeItems(cachedQuery.rows)
239
+ else replaceItems(cachedQuery.rows)
240
+ totalCount.value = cachedQuery.count
241
+ lastFetchedAt.value = cachedQuery.fetchedAt
242
+ lastQueryKey.value = querySignature
243
+ stale.value = false
244
+ error.value = null
245
+ rememberQuery(querySignature, cachedQuery)
246
+ return cachedQuery.rows
247
+ }
248
+ const inFlightKey = `${querySignature}:${merge ? 'merge' : 'replace'}`
249
+ const pending = inFlightQueries.get(inFlightKey)
250
+ if (!bypassCache && pending) return pending
251
+
252
+ const requestId = ++fetchSequence
253
+ const startingRevision = collectionRevision
254
+ loading.value = true
255
+ error.value = null
256
+
257
+ const request = (async () => {
258
+ try {
259
+ const response = await api.all<T>(resource, finalQuery)
260
+
261
+ if (!response.success) {
262
+ error.value = response.error
263
+ throw response.error
264
+ }
265
+
266
+ const rows = Array.isArray(response.data) ? response.data : []
267
+
268
+ if (requestId === fetchSequence && startingRevision === collectionRevision) {
269
+ if (merge) mergeItems(rows)
270
+ else replaceItems(rows)
271
+ totalCount.value = response.count ?? null
272
+ lastFetchedAt.value = Date.now()
273
+ lastQueryKey.value = querySignature
274
+ stale.value = false
275
+ rememberQuery(querySignature, { rows, count: response.count ?? null, fetchedAt: Date.now() })
276
+ }
277
+
278
+ return rows
279
+ } catch (fetchError) {
280
+ console.error(`[${resource}] fetch error`, fetchError)
281
+ error.value = fetchError
282
+ stale.value = true
283
+ throw fetchError
284
+ } finally {
285
+ if (requestId === fetchSequence) loading.value = false
286
+ inFlightQueries.delete(inFlightKey)
287
+ }
288
+ })()
289
+ inFlightQueries.set(inFlightKey, request)
290
+ return request
291
+ }
292
+ const fetchFromSupabase = (query: DbStoreFetchOptions = {}) => fetchFromSupabaseInternal(query)
293
+
294
+ const refresh = (query: DbStoreFetchOptions = {}) => fetchFromSupabaseInternal({ ...currentQuery, ...query }, true)
295
+
296
+ const create = async (payload: Partial<T>): Promise<T | null> => {
297
+ await waitForHydrationValidation()
298
+ reconcileUserScope(supabaseUser.value?.id ?? null)
299
+ const response = await api.create<T>(resource, payload)
300
+
301
+ if (!response.success) {
302
+ console.error(`[${resource}] create error`, response.error)
303
+ throw response.error
304
+ }
305
+
306
+ const data = response.data as T
307
+ markMutation()
308
+ if (!isComplexCollectionQuery(currentQuery)) {
309
+ addOrUpdate(data)
310
+ items.value = sortCollection(items.value, currentQuery).slice(0, currentQuery.limit ?? 100)
311
+ if (totalCount.value != null) totalCount.value++
312
+ }
313
+ return data
314
+ }
315
+
316
+ const update = async (id: string | number, payload: Partial<T>): Promise<T | null> => {
317
+ await waitForHydrationValidation()
318
+ reconcileUserScope(supabaseUser.value?.id ?? null)
319
+ const response = await api.update<T>(resource, id, payload, { key: String(key) })
320
+
321
+ if (!response.success) {
322
+ console.error(`[${resource}] update error`, response.error)
323
+ throw response.error
324
+ }
325
+
326
+ const data = Array.isArray(response.data) ? response.data[0] : response.data
327
+ if (data) {
328
+ markMutation()
329
+ if (!isComplexCollectionQuery(currentQuery)) {
330
+ addOrUpdate(data as T)
331
+ items.value = sortCollection(items.value, currentQuery)
332
+ }
333
+ }
334
+ return (data as T | undefined) ?? null
335
+ }
336
+
337
+ const remove = async (id: string | number) => {
338
+ await waitForHydrationValidation()
339
+ reconcileUserScope(supabaseUser.value?.id ?? null)
340
+ const response = await api.remove(resource, id, { key: String(key) })
341
+
342
+ if (!response.success) {
343
+ console.error(`[${resource}] delete error`, response.error)
344
+ throw response.error
345
+ }
346
+
347
+ const existed = !!getById(id)
348
+ markMutation()
349
+ removeLocal(id)
350
+ if (!isComplexCollectionQuery(currentQuery) && existed && totalCount.value != null) totalCount.value--
351
+ }
352
+
353
+ const subscribe = () => {
354
+ realtimeRequested = true
355
+ if (typeof window === 'undefined' || subscription) return
356
+ subscriptionOwnerId = supabaseUser.value?.id ?? null
357
+
358
+ subscription = supabase
359
+ .channel(`public:${resource}`)
360
+ .on('postgres_changes', {
361
+ event: '*',
362
+ schema: 'public',
363
+ table: resource
364
+ }, (payload: any) => {
365
+ if (shouldScopeToUser && subscriptionOwnerId !== (supabaseUser.value?.id ?? null)) return
366
+ const { eventType, new: newItem, old } = payload
367
+ invalidate()
368
+ if (isComplexCollectionQuery(currentQuery)) return
369
+ if (eventType === 'INSERT' || eventType === 'UPDATE') {
370
+ addOrUpdate(newItem as T)
371
+ items.value = sortCollection(items.value, currentQuery)
372
+ } else if (eventType === 'DELETE') {
373
+ removeLocal(old?.[key as string])
374
+ }
375
+ })
376
+ .subscribe((status: string, statusError?: unknown) => {
377
+ if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT' || status === 'CLOSED') {
378
+ realtimeDisconnected = true
379
+ stale.value = true
380
+ if (statusError) error.value = statusError
381
+ }
382
+ else if (status === 'SUBSCRIBED' && realtimeDisconnected) {
383
+ realtimeDisconnected = false
384
+ invalidate()
385
+ void refresh().catch(() => {})
386
+ }
387
+ })
388
+
389
+ console.info(`[${resource}] realtime subscription initialized`)
390
+ }
391
+
392
+ async function unsubscribe(clearIntent = true) {
393
+ if (clearIntent) realtimeRequested = false
394
+ const active = subscription
395
+ subscription = null
396
+ subscriptionOwnerId = null
397
+ realtimeDisconnected = false
398
+ if (!active) return
399
+ if (typeof (supabase as any).removeChannel === 'function') await (supabase as any).removeChannel(active)
400
+ else await active.unsubscribe()
401
+ }
402
+
403
+ return {
404
+ items,
405
+ totalCount,
406
+ cachedCount,
407
+ loading,
408
+ error,
409
+ stale,
410
+ lastFetchedAt,
411
+ lastQueryKey,
412
+ scopeOwnerId,
413
+ hydrationReady,
414
+ fetchFromSupabase,
415
+ refresh,
416
+ invalidate,
417
+ addOrUpdate,
418
+ removeLocal,
419
+ reset,
420
+ remove,
421
+ getById,
422
+ create,
423
+ update,
424
+ subscribe,
425
+ unsubscribe,
426
+ reconcileUserScope,
427
+ quarantineHydratedState,
428
+ }
429
+ }, {
430
+ persist: shouldPersist
431
+ ? {
432
+ pick: ['items', 'totalCount', 'lastFetchedAt', 'lastQueryKey', 'scopeOwnerId'],
433
+ afterHydrate: ({ store }: { store: { quarantineHydratedState?: () => Promise<void> } }) => {
434
+ void store.quarantineHydratedState?.()
435
+ },
436
+ }
437
+ : false,
438
+ } as any)
439
+ }
@@ -0,0 +1,67 @@
1
+ import { ref } from 'vue'
2
+ import { defineStore } from '#imports'
3
+
4
+ export interface SingletonStoreOptions<T> {
5
+ /** Fonction de chargement de la ressource depuis Supabase ou autre */
6
+ fetch: () => Promise<{ success: boolean; data?: T; error?: any }>
7
+
8
+ /** Fonction de mise à jour de la ressource dans la base */
9
+ update?: (partialData: Partial<T>) => Promise<{ success: boolean; data?: T; error?: any }>
10
+
11
+ /** Clé d'identification unique du store */
12
+ id: string
13
+
14
+ /** Activer la persistance locale */
15
+ persist?: boolean
16
+ }
17
+
18
+ export function createSingletonStore<T>(options: SingletonStoreOptions<T>) {
19
+ const {
20
+ id,
21
+ fetch,
22
+ update = async () => ({ success: false, error: 'Update not implemented' }),
23
+ persist = false
24
+ } = options
25
+
26
+ return defineStore(id, () => {
27
+ const data = ref<T | null>(null)
28
+ const loading = ref(false)
29
+ const error = ref<any>(null)
30
+
31
+ const fetchData = async () => {
32
+ loading.value = true
33
+ const result = await fetch()
34
+ loading.value = false
35
+
36
+ if (result.success && result.data) {
37
+ data.value = result.data
38
+ error.value = null
39
+ } else {
40
+ error.value = result.error
41
+ }
42
+
43
+ return result
44
+ }
45
+
46
+ const updateData = async (partialData: Partial<T>) => {
47
+ const result = await update(partialData)
48
+
49
+ if (result.success && result.data) {
50
+ data.value = result.data
51
+ error.value = null
52
+ } else {
53
+ error.value = result.error
54
+ }
55
+
56
+ return result
57
+ }
58
+
59
+ return {
60
+ data,
61
+ loading,
62
+ error,
63
+ fetch: fetchData,
64
+ update: updateData
65
+ }
66
+ }, { persist } as any)
67
+ }
@@ -0,0 +1,47 @@
1
+ import type { ListOptions } from '@lucashw68/nsdb/types/list'
2
+
3
+ function normalize(value: unknown): unknown {
4
+ if (Array.isArray(value)) return value.map(normalize)
5
+ if (value && typeof value === 'object') {
6
+ return Object.fromEntries(
7
+ Object.entries(value as Record<string, unknown>)
8
+ .filter(([, entry]) => entry !== undefined)
9
+ .sort(([left], [right]) => left.localeCompare(right))
10
+ .map(([key, entry]) => [key, normalize(entry)]),
11
+ )
12
+ }
13
+ return value
14
+ }
15
+
16
+ export function stableQueryKey(value: unknown): string {
17
+ return JSON.stringify(normalize(value))
18
+ }
19
+
20
+ export function isComplexCollectionQuery(query: ListOptions): boolean {
21
+ return !!(
22
+ (query.where && Object.keys(query.where).length > 0)
23
+ || query.search
24
+ || (query.offset ?? 0) > 0
25
+ || query.orderForeignTable
26
+ // Selecting a subset of scalar columns does not change membership. An
27
+ // embedded PostgREST relation does, and cannot be maintained locally.
28
+ || (query.select?.includes('(') ?? false)
29
+ )
30
+ }
31
+
32
+ export function sortCollection<T extends Record<string, any>>(
33
+ rows: T[],
34
+ query: ListOptions,
35
+ ): T[] {
36
+ if (!query.orderBy || query.orderForeignTable) return [...rows]
37
+ const direction = query.orderDirection === 'desc' ? -1 : 1
38
+ const column = query.orderBy
39
+ return [...rows].sort((left, right) => {
40
+ const a = left[column]
41
+ const b = right[column]
42
+ if (a == null && b == null) return 0
43
+ if (a == null) return 1
44
+ if (b == null) return -1
45
+ return String(a).localeCompare(String(b), undefined, { numeric: true }) * direction
46
+ })
47
+ }
@@ -0,0 +1,41 @@
1
+ export function normalizePath(path = '') {
2
+ const segments = path
3
+ .split('/')
4
+ .map(part => part.trim())
5
+ .filter(Boolean)
6
+
7
+ if (segments.some(part => part === '.' || part === '..')) {
8
+ throw new Error('[nsdb:storage] Relative path segments are not allowed.')
9
+ }
10
+
11
+ return segments.join('/')
12
+ }
13
+
14
+ export function normalizeDirectoryPath(path = '') {
15
+ return normalizePath(path)
16
+ }
17
+
18
+ export function normalizeFilePath(path: string) {
19
+ const normalizedPath = normalizePath(path)
20
+ if (!normalizedPath) throw new Error('[nsdb:storage] A file path is required.')
21
+ return normalizedPath
22
+ }
23
+
24
+ export function joinPath(...parts: Array<string | number | null | undefined>) {
25
+ return parts
26
+ .map(part => normalizePath(String(part ?? '')))
27
+ .filter(Boolean)
28
+ .join('/')
29
+ }
30
+
31
+ export function applySearchFilter<T extends { name?: string }>(items: T[], search?: string) {
32
+ const normalizedSearch = search?.trim().toLowerCase()
33
+ if (!normalizedSearch) return items
34
+ return items.filter(item => item.name?.toLowerCase().includes(normalizedSearch))
35
+ }
36
+
37
+ export function normalizeBucketName(bucketName: string) {
38
+ const normalizedName = bucketName.trim()
39
+ if (!normalizedName) throw new Error('[nsdb:storage] A bucket name is required.')
40
+ return normalizedName
41
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path'
3
+ import { parseArgs } from '../helpers/args.js'
4
+ import { loadNsdbConfig } from '../helpers/config.js'
5
+ import {
6
+ isNsdbGeneratedFile,
7
+ listGeneratedFiles,
8
+ removeGeneratedFile,
9
+ } from '../helpers/generated.js'
10
+
11
+ function configuredTargets(currentWorkingDirectory, config, shouldDeleteStores) {
12
+ const fileTargets = [config.paths.enums]
13
+ const directoryTargets = [
14
+ config.paths.schemas,
15
+ config.paths.models,
16
+ config.paths.composables,
17
+ ]
18
+
19
+ if (shouldDeleteStores) directoryTargets.push(config.paths.stores)
20
+
21
+ return {
22
+ fileTargets: fileTargets.map(target => path.resolve(currentWorkingDirectory, target)),
23
+ directoryTargets: [...new Set(directoryTargets.map(target => path.resolve(currentWorkingDirectory, target)))],
24
+ }
25
+ }
26
+
27
+ export async function clearGeneratedFiles({
28
+ currentWorkingDirectory = process.cwd(),
29
+ parsedArguments = parseArgs(),
30
+ } = {}) {
31
+ const verbose = parsedArguments.getBool('verbose', false)
32
+ const dryRun = parsedArguments.getBool('dry-run', false)
33
+ const shouldDeleteStores = !parsedArguments.getBool('no-stores', false)
34
+ const { config } = await loadNsdbConfig(
35
+ currentWorkingDirectory,
36
+ parsedArguments.get('config', ''),
37
+ )
38
+ const targets = configuredTargets(currentWorkingDirectory, config, shouldDeleteStores)
39
+ const candidates = [
40
+ ...targets.fileTargets.filter(isNsdbGeneratedFile),
41
+ ...targets.directoryTargets.flatMap(listGeneratedFiles),
42
+ ]
43
+ const uniqueCandidates = [...new Set(candidates)]
44
+
45
+ for (const filePath of uniqueCandidates) {
46
+ removeGeneratedFile(filePath, { dryRun, verbose })
47
+ }
48
+
49
+ if (dryRun) {
50
+ console.log(`Cleanup preview: ${uniqueCandidates.length} generated file(s) would be removed.`)
51
+ } else {
52
+ console.log(`Cleanup completed: ${uniqueCandidates.length} generated file(s) removed.`)
53
+ }
54
+
55
+ return uniqueCandidates
56
+ }
57
+
58
+ if (import.meta.url === `file://${process.argv[1]}`) {
59
+ clearGeneratedFiles().catch((error) => {
60
+ console.error('Failed to clear NSDB generated files.')
61
+ console.error(error)
62
+ process.exit(1)
63
+ })
64
+ }