@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,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
+ }