@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,79 @@
1
+ function normalizeTableList(value, optionName) {
2
+ if (value == null) return []
3
+ if (!Array.isArray(value) || value.some(table => typeof table !== 'string' || !table.trim())) {
4
+ throw new TypeError(`[nsdb] tables.${optionName} must be an array of non-empty table names.`)
5
+ }
6
+ return [...new Set(value.map(table => table.trim()))]
7
+ }
8
+
9
+ export function selectTableNames(availableTableNames, tableSelection = {}) {
10
+ const available = [...new Set(availableTableNames)].sort((left, right) => left.localeCompare(right))
11
+ const include = normalizeTableList(tableSelection?.include, 'include')
12
+ const exclude = normalizeTableList(tableSelection?.exclude, 'exclude')
13
+
14
+ if (include.length > 0 && exclude.length > 0) {
15
+ throw new Error('[nsdb] Configure either tables.include or tables.exclude, not both.')
16
+ }
17
+
18
+ const availableSet = new Set(available)
19
+ const configuredNames = include.length > 0 ? include : exclude
20
+ const unknownNames = configuredNames.filter(table => !availableSet.has(table))
21
+ if (unknownNames.length > 0) {
22
+ throw new Error(`[nsdb] Unknown table(s) in exposure config: ${unknownNames.join(', ')}`)
23
+ }
24
+
25
+ if (include.length > 0) {
26
+ const included = new Set(include)
27
+ return available.filter(table => included.has(table))
28
+ }
29
+
30
+ const excluded = new Set(exclude)
31
+ return available.filter(table => !excluded.has(table))
32
+ }
33
+
34
+ export function selectTableProperties(tablesType, tableSelection = {}) {
35
+ const properties = tablesType.getProperties()
36
+ const byName = new Map(properties.map(property => [property.getName(), property]))
37
+ return selectTableNames([...byName.keys()], tableSelection).map(tableName => byName.get(tableName))
38
+ }
39
+
40
+ export function getColumnPolicies(tableSelection = {}, tableName, availableColumnNames) {
41
+ const configuredTables = tableSelection?.columns ?? {}
42
+ if (configuredTables == null || typeof configuredTables !== 'object' || Array.isArray(configuredTables)) {
43
+ throw new TypeError('[nsdb] tables.columns must be an object keyed by table name.')
44
+ }
45
+
46
+ const tableRules = configuredTables[tableName] ?? {}
47
+ if (typeof tableRules !== 'object' || Array.isArray(tableRules)) {
48
+ throw new TypeError(`[nsdb] tables.columns.${tableName} must be an object keyed by column name.`)
49
+ }
50
+
51
+ const available = new Set(availableColumnNames)
52
+ const unknown = Object.keys(tableRules).filter(column => !available.has(column))
53
+ if (unknown.length) {
54
+ throw new Error(`[nsdb] Unknown column(s) for ${tableName}: ${unknown.join(', ')}`)
55
+ }
56
+
57
+ const result = {}
58
+ for (const columnName of availableColumnNames) {
59
+ const rule = tableRules[columnName] ?? {}
60
+ if (typeof rule !== 'object' || Array.isArray(rule)) {
61
+ throw new TypeError(`[nsdb] Column policy ${tableName}.${columnName} must be an object.`)
62
+ }
63
+ for (const key of ['selectable', 'editable', 'hidden', 'serverOnly']) {
64
+ if (rule[key] != null && typeof rule[key] !== 'boolean') {
65
+ throw new TypeError(`[nsdb] ${tableName}.${columnName}.${key} must be boolean.`)
66
+ }
67
+ }
68
+ if (rule.serverOnly && (rule.selectable === true || rule.editable === true)) {
69
+ throw new Error(`[nsdb] ${tableName}.${columnName} cannot be serverOnly and selectable/editable.`)
70
+ }
71
+ result[columnName] = {
72
+ selectable: rule.serverOnly ? false : rule.selectable ?? true,
73
+ editable: rule.serverOnly ? false : rule.editable,
74
+ hidden: rule.serverOnly ? true : rule.hidden ?? false,
75
+ serverOnly: rule.serverOnly ?? false,
76
+ }
77
+ }
78
+ return result
79
+ }
package/helpers/ts.js ADDED
@@ -0,0 +1,37 @@
1
+ // helpers/ts.js
2
+ import { Project } from 'ts-morph'
3
+
4
+ export function createTsProject() {
5
+ // Supabase's Row/Insert distinction depends on preserving `null` in unions.
6
+ // The in-memory generator project must not inherit TypeScript's loose default.
7
+ return new Project({
8
+ skipAddingFilesFromTsConfig: true,
9
+ compilerOptions: { strictNullChecks: true },
10
+ })
11
+ }
12
+
13
+ export function addSourceFile(project, absPath) {
14
+ return project.addSourceFileAtPath(absPath)
15
+ }
16
+
17
+ export function loadDatabaseAlias(sourceFile) {
18
+ try {
19
+ return sourceFile.getTypeAliasOrThrow('Database')
20
+ } catch {
21
+ return null
22
+ }
23
+ }
24
+
25
+ export function getPublicEnumsType(databaseAlias) {
26
+ const databaseType = databaseAlias.getType()
27
+ const publicType = databaseType.getProperty('public')?.getTypeAtLocation(databaseAlias)
28
+ if (!publicType) return null
29
+ return publicType.getProperty('Enums')?.getTypeAtLocation(databaseAlias) ?? null
30
+ }
31
+
32
+ export function getPublicTablesType(databaseAlias) {
33
+ const databaseType = databaseAlias.getType()
34
+ const publicType = databaseType.getProperty('public')?.getTypeAtLocation(databaseAlias)
35
+ if (!publicType) return null
36
+ return publicType.getProperty('Tables')?.getTypeAtLocation(databaseAlias) ?? null
37
+ }
package/module.ts ADDED
@@ -0,0 +1,151 @@
1
+ import {
2
+ defineNuxtModule,
3
+ addComponentsDir,
4
+ addImports,
5
+ addImportsDir,
6
+ addTemplate,
7
+ createResolver,
8
+ logger
9
+ } from '@nuxt/kit'
10
+ import { existsSync } from 'node:fs'
11
+
12
+ export interface NsdbOptions {
13
+ withComponents?: boolean
14
+ componentsPrefix?: string
15
+ withStores?: boolean
16
+ autoImportModels?: boolean
17
+ }
18
+
19
+ export default defineNuxtModule<NsdbOptions>({
20
+ meta: { name: '@lucashw68/nsdb', configKey: 'nsdb' },
21
+ defaults: { withComponents: true, componentsPrefix: 'Nsdb', withStores: true, autoImportModels: true },
22
+
23
+ setup(options: NsdbOptions, nuxt: any) {
24
+ const { resolve } = createResolver(import.meta.url)
25
+ const rMod = createResolver(import.meta.url)
26
+ const rApp = createResolver(nuxt.options.rootDir)
27
+
28
+ const runtimeDir = rMod.resolve('./runtime')
29
+ // 1) Alias interne vers le runtime du module
30
+ nuxt.options.alias['#nsdb'] = runtimeDir
31
+
32
+ if (options.withComponents) {
33
+ addComponentsDir({
34
+ path: resolve(runtimeDir, 'components'),
35
+ prefix: options.componentsPrefix ?? 'Nsdb',
36
+ pathPrefix: false,
37
+ ignore: ['Form/**'],
38
+ transpile: true,
39
+ })
40
+ }
41
+
42
+ // 2) Proxies de build pour les barrels d'app:
43
+ // -> import { ... } from '#build/nsdb/models'
44
+ // -> import { ... } from '#build/nsdb/schemas'
45
+ const appModelsAbs = rApp.resolve('nsdb/models/index.ts')
46
+ const appSchemasAbs = rApp.resolve('nsdb/schemas/index.ts')
47
+ const appRegistryAbs = rApp.resolve('nsdb/composables/useNsdbModels.ts')
48
+ const appModelsExists = existsSync(appModelsAbs)
49
+ const appSchemasExists = existsSync(appSchemasAbs)
50
+ const appRegistryExists = existsSync(appRegistryAbs)
51
+
52
+ addTemplate({
53
+ filename: 'nsdb/models.ts',
54
+ write: true,
55
+ getContents: () =>
56
+ appModelsExists
57
+ ? `export * from '~~/nsdb/models'`
58
+ : `// Fallback neutre: générez vos modules avec nsdb:models
59
+ export {}`
60
+ })
61
+
62
+ addTemplate({
63
+ filename: 'nsdb/schemas.ts',
64
+ write: true,
65
+ getContents: () =>
66
+ appSchemasExists
67
+ ? `export * from '~~/nsdb/schemas'`
68
+ : `// Fallback neutre: générez vos schémas avec nsdb:models (qui émet aussi nsdb/schemas)
69
+ export {}`
70
+ })
71
+
72
+ addTemplate({
73
+ filename: 'nsdb/registry.ts',
74
+ write: true,
75
+ getContents: () =>
76
+ appRegistryExists
77
+ ? `export { useNsdbModel } from '~~/nsdb/composables/useNsdbModels'`
78
+ : `export function useNsdbModel(model: string): never {
79
+ throw new Error('[nsdb] No generated model registry found for "' + model + '". Run nsdb generate:all.')
80
+ }`
81
+ })
82
+
83
+ // Regénère les proxies si ~/nsdb/models.ts ou ~/nsdb/schemas.ts apparaissent en dev
84
+ nuxt.hook('builder:watch', (_event: string, relPath: string) => {
85
+ if (!relPath) return
86
+ const normalized = relPath.replace(/^[./]+/, '')
87
+ if (normalized === 'nsdb/models/index.ts') {
88
+ addTemplate({
89
+ filename: 'nsdb/models.ts',
90
+ write: true,
91
+ getContents: () => `export * from '~~/nsdb/models'`
92
+ })
93
+ logger.success('[nsdb] Proxy #build/nsdb/models mis à jour (~/nsdb/models.ts détecté).')
94
+ }
95
+ if (normalized === 'nsdb/schemas/index.ts') {
96
+ addTemplate({
97
+ filename: 'nsdb/schemas.ts',
98
+ write: true,
99
+ getContents: () => `export * from '~~/nsdb/schemas'`
100
+ })
101
+ logger.success('[nsdb] Proxy #build/nsdb/schemas mis à jour (~/nsdb/schemas.ts détecté).')
102
+ }
103
+ if (normalized === 'nsdb/composables/useNsdbModels.ts') {
104
+ addTemplate({
105
+ filename: 'nsdb/registry.ts',
106
+ write: true,
107
+ getContents: () => `export { useNsdbModel } from '~~/nsdb/composables/useNsdbModels'`
108
+ })
109
+ logger.success('[nsdb] Proxy #build/nsdb/registry mis à jour.')
110
+ }
111
+ })
112
+
113
+ // 3) Public runtime auto-imports. Keep this list explicit: exporting a
114
+ // helper from a runtime file must not accidentally make it app-global.
115
+ addImports([
116
+ { name: 'useSupabaseApi', from: rMod.resolve(runtimeDir, 'composables/useSupabaseApi') },
117
+ { name: 'useSupabaseApiStorage', from: rMod.resolve(runtimeDir, 'composables/useSupabaseApiStorage') },
118
+ { name: 'useSupabaseModel', from: rMod.resolve(runtimeDir, 'composables/useSupabaseModels') },
119
+ { name: 'useNsdbSchema', from: rMod.resolve(runtimeDir, 'composables/useNsdbSchemas') },
120
+ { name: 'useNsdbProfile', from: rMod.resolve(runtimeDir, 'composables/useNsdbProfile') },
121
+ ])
122
+ if (options.withStores) {
123
+ addImports([
124
+ { name: 'createDbStore', from: rMod.resolve(runtimeDir, 'stores/createDbStore') },
125
+ { name: 'createSingletonStore', from: rMod.resolve(runtimeDir, 'stores/createSingletonDbStore') },
126
+ ])
127
+ }
128
+ if (appModelsExists && options.autoImportModels) {
129
+ const generatedModelsDir = rApp.resolve('nsdb/models')
130
+ addImportsDir(generatedModelsDir)
131
+ nuxt.hook('imports:extend', (imports: Array<{ name?: string; as?: string; from?: string }>) => {
132
+ const generatedImports = imports.filter(entry => entry.from?.startsWith(generatedModelsDir))
133
+ for (const generatedImport of generatedImports) {
134
+ const publicName = generatedImport.as ?? generatedImport.name
135
+ if (!publicName?.startsWith('use')) continue
136
+ const collision = imports.find(entry =>
137
+ entry !== generatedImport &&
138
+ (entry.as ?? entry.name) === publicName &&
139
+ !entry.from?.startsWith(generatedModelsDir),
140
+ )
141
+ if (collision) {
142
+ throw new Error(
143
+ `[nsdb] Auto-import collision for "${publicName}" between generated models and ${collision.from}. ` +
144
+ 'Set nsdb.autoImportModels=false and import the generated model explicitly.',
145
+ )
146
+ }
147
+ }
148
+ })
149
+ }
150
+ }
151
+ })
@@ -0,0 +1,39 @@
1
+ export default {
2
+ supabase: {
3
+ schema: 'public',
4
+ projectId: process.env.SUPABASE_PROJECT_ID,
5
+ // For self-hosted Supabase, use dbUrl instead of projectId:
6
+ // dbUrl: process.env.SUPABASE_DB_URL,
7
+ // If Postgres is only reachable from a VPS, generate types remotely:
8
+ // remoteTypes: {
9
+ // sshHost: process.env.SUPABASE_REMOTE_SSH_HOST,
10
+ // projectPath: process.env.SUPABASE_REMOTE_PROJECT_PATH,
11
+ // dbUrl: process.env.SUPABASE_REMOTE_DB_URL,
12
+ // remoteOutput: '/tmp/database.types.ts',
13
+ // beforeCommand: process.env.SUPABASE_REMOTE_BEFORE_COMMAND,
14
+ // supabaseCommand: process.env.SUPABASE_REMOTE_SUPABASE_COMMAND,
15
+ // },
16
+ linked: false,
17
+ },
18
+ paths: {
19
+ types: 'types/database.types.ts',
20
+ metadata: 'nsdb/database.metadata.json',
21
+ enums: 'nsdb/enums.ts',
22
+ schemas: 'nsdb/schemas',
23
+ models: 'nsdb/models',
24
+ composables: 'nsdb/composables',
25
+ stores: 'stores',
26
+ },
27
+ imports: {
28
+ databaseTypes: '~~/types/database.types',
29
+ },
30
+ tables: {
31
+ include: ['playlists'],
32
+ columns: {
33
+ playlists: {
34
+ internal_note: { serverOnly: true },
35
+ created_at: { editable: false },
36
+ },
37
+ },
38
+ },
39
+ }
@@ -0,0 +1,42 @@
1
+ import type { NsdbConfig } from '@lucashw68/nsdb/types/config'
2
+
3
+ export default {
4
+ supabase: {
5
+ schema: 'public',
6
+ projectId: process.env.SUPABASE_PROJECT_ID,
7
+ // For self-hosted Supabase, use dbUrl instead of projectId:
8
+ // dbUrl: process.env.SUPABASE_DB_URL,
9
+ // If Postgres is only reachable from a VPS, generate types remotely:
10
+ // remoteTypes: {
11
+ // sshHost: process.env.SUPABASE_REMOTE_SSH_HOST,
12
+ // projectPath: process.env.SUPABASE_REMOTE_PROJECT_PATH,
13
+ // dbUrl: process.env.SUPABASE_REMOTE_DB_URL,
14
+ // remoteOutput: '/tmp/database.types.ts',
15
+ // beforeCommand: process.env.SUPABASE_REMOTE_BEFORE_COMMAND,
16
+ // supabaseCommand: process.env.SUPABASE_REMOTE_SUPABASE_COMMAND,
17
+ // },
18
+ linked: false,
19
+ },
20
+ paths: {
21
+ types: 'types/database.types.ts',
22
+ metadata: 'nsdb/database.metadata.json',
23
+ enums: 'nsdb/enums.ts',
24
+ schemas: 'nsdb/schemas',
25
+ models: 'nsdb/models',
26
+ composables: 'nsdb/composables',
27
+ stores: 'stores',
28
+ },
29
+ imports: {
30
+ databaseTypes: '~~/types/database.types',
31
+ },
32
+ tables: {
33
+ // Prefer an allowlist for browser-facing artifacts.
34
+ include: ['playlists'],
35
+ columns: {
36
+ playlists: {
37
+ internal_note: { serverOnly: true },
38
+ created_at: { editable: false },
39
+ },
40
+ },
41
+ },
42
+ } satisfies NsdbConfig
package/package.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "name": "@lucashw68/nsdb",
3
+ "version": "1.0.0-rc.2",
4
+ "type": "module",
5
+ "description": "A typed Nuxt 4 bridge for Supabase models, optional Pinia caching and generic CRUD components.",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "nuxt",
9
+ "supabase",
10
+ "pinia",
11
+ "store",
12
+ "module",
13
+ "database",
14
+ "layer"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22.14.0"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "registry": "https://registry.npmjs.org/",
22
+ "tag": "next"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/Lucashw68/nsdb.git",
27
+ "directory": "Nsdb"
28
+ },
29
+ "homepage": "https://github.com/Lucashw68/nsdb#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/Lucashw68/nsdb/issues"
32
+ },
33
+ "exports": {
34
+ ".": "./module.ts",
35
+ "./useSupabaseApi": "./runtime/composables/useSupabaseApi.ts",
36
+ "./useSupabaseApiStorage": "./runtime/composables/useSupabaseApiStorage.ts",
37
+ "./useSupabaseModel": "./runtime/composables/useSupabaseModels.ts",
38
+ "./useNsdbProfile": "./runtime/composables/useNsdbProfile.ts",
39
+ "./useNsdbSchema": "./runtime/composables/useNsdbSchemas.ts",
40
+ "./types": "./types/index.ts",
41
+ "./types/config": "./types/config.ts",
42
+ "./types/entities": "./types/entities.ts",
43
+ "./types/list": "./types/list.ts",
44
+ "./types/model": "./types/model.ts",
45
+ "./createDbStore": "./runtime/stores/createDbStore.ts",
46
+ "./createSingletonStore": "./runtime/stores/createSingletonDbStore.ts"
47
+ },
48
+ "dependencies": {
49
+ "@nuxt/kit": "^4.0.1",
50
+ "@supabase/supabase-js": "^2.56.0",
51
+ "@types/node": "^22.0.0",
52
+ "postgres": "3.4.7",
53
+ "ts-morph": "^20.0.0",
54
+ "tsx": "^4.20.5"
55
+ },
56
+ "devDependencies": {
57
+ "@nuxt/kit": "^4.0.1",
58
+ "@vitejs/plugin-vue": "6.0.1",
59
+ "@vue/test-utils": "2.4.6",
60
+ "happy-dom": "18.0.1",
61
+ "pinia": "^3.0.3",
62
+ "typescript": "^5.0.0",
63
+ "vitest": "3.2.4",
64
+ "vue": "3.5.24"
65
+ },
66
+ "peerDependencies": {
67
+ "@nuxtjs/supabase": "^1.6.1 || ^2.0.0",
68
+ "@pinia/nuxt": "^0.11.2",
69
+ "nuxt": "^4.2.1",
70
+ "pinia": "^3.0.3",
71
+ "pinia-plugin-persistedstate": "^4.5.0",
72
+ "vue": "^3.5.24"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "pinia-plugin-persistedstate": {
76
+ "optional": true
77
+ }
78
+ },
79
+ "bin": {
80
+ "nsdb": "./cli/index.js"
81
+ },
82
+ "scripts": {
83
+ "generate:types": "node ./scripts/generate-types.js",
84
+ "generate:metadata": "node ./scripts/generate-metadata.js",
85
+ "generate:enums": "node ./scripts/generate-enums.js",
86
+ "generate:schemas": "node ./scripts/generate-schemas.js",
87
+ "generate:models": "node ./scripts/generate-models.js",
88
+ "generate:composables": "node ./scripts/generate-composables.js",
89
+ "generate:stores": "node ./scripts/generate-stores.js",
90
+ "generate:all": "npm run generate:types && npm run generate:metadata && npm run generate:enums && npm run generate:schemas && npm run generate:models && npm run generate:stores && npm run generate:models && npm run generate:composables",
91
+ "typecheck": "tsc --noEmit",
92
+ "check": "tsc --noEmit",
93
+ "test": "npm run test:node && npm run test:runtime",
94
+ "test:node": "node --test \"tests/**/*.test.mjs\"",
95
+ "test:runtime": "vitest run",
96
+ "test:consumer": "node tests/fresh-consumer.mjs",
97
+ "prepublishOnly": "npm run check && npm test",
98
+ "clear": "node scripts/clear.js"
99
+ },
100
+ "files": [
101
+ "CHANGELOG.md",
102
+ "GET_STARTED.md",
103
+ "module.ts",
104
+ "runtime",
105
+ "scripts",
106
+ "templates",
107
+ "helpers",
108
+ "cli",
109
+ "types",
110
+ "nsdb.config.example.ts",
111
+ "nsdb.config.example.mjs"
112
+ ],
113
+ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
114
+ }