@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,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { parseArgs } from '../helpers/args.js'
|
|
4
|
+
import { ensureDir, readText, writeText } from '../helpers/io.js'
|
|
5
|
+
import { run, isAvailable } from '../helpers/shell.js'
|
|
6
|
+
import { getBoolOption, getOption, loadNsdbConfig } from '../helpers/config.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build the Supabase CLI command that dumps the database types to disk.
|
|
10
|
+
*/
|
|
11
|
+
function quoteShellArgument(value) {
|
|
12
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function quoteRemoteShellArgument(value) {
|
|
16
|
+
return `"${String(value)
|
|
17
|
+
.replaceAll('\\', '\\\\')
|
|
18
|
+
.replaceAll('"', '\\"')
|
|
19
|
+
.replaceAll('`', '\\`')}"`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildCommand({
|
|
23
|
+
projectId,
|
|
24
|
+
dbUrl,
|
|
25
|
+
outputPath,
|
|
26
|
+
schemaName,
|
|
27
|
+
useLinkedProject,
|
|
28
|
+
supabaseCommand = 'npx supabase',
|
|
29
|
+
argumentQuoter = quoteShellArgument,
|
|
30
|
+
}) {
|
|
31
|
+
const parts = [`${supabaseCommand} gen types typescript`]
|
|
32
|
+
if (schemaName) parts.push(`--schema ${schemaName}`)
|
|
33
|
+
if (dbUrl) {
|
|
34
|
+
parts.push(`--db-url ${argumentQuoter(dbUrl)}`)
|
|
35
|
+
} else {
|
|
36
|
+
parts.push(useLinkedProject ? '--linked' : `--project-id ${projectId}`)
|
|
37
|
+
}
|
|
38
|
+
parts.push(`> ${argumentQuoter(outputPath)}`)
|
|
39
|
+
return parts.join(' ')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildLocalTypesEnvironment({ dbUrl, environment = process.env }) {
|
|
43
|
+
const commandEnvironment = { ...environment }
|
|
44
|
+
if (dbUrl) delete commandEnvironment.SUPABASE_PROJECT_ID
|
|
45
|
+
return commandEnvironment
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildRemoteTypesCommands({
|
|
49
|
+
sshHost,
|
|
50
|
+
projectPath,
|
|
51
|
+
dbUrl,
|
|
52
|
+
remoteOutput = '/tmp/database.types.ts',
|
|
53
|
+
localOutputPath,
|
|
54
|
+
schemaName = 'public',
|
|
55
|
+
beforeCommand = '',
|
|
56
|
+
supabaseCommand = 'npx supabase',
|
|
57
|
+
}) {
|
|
58
|
+
const remoteGenerateCommand = [
|
|
59
|
+
projectPath ? `cd ${quoteShellArgument(projectPath)}` : '',
|
|
60
|
+
beforeCommand,
|
|
61
|
+
buildCommand({
|
|
62
|
+
dbUrl,
|
|
63
|
+
outputPath: remoteOutput,
|
|
64
|
+
schemaName,
|
|
65
|
+
useLinkedProject: false,
|
|
66
|
+
supabaseCommand,
|
|
67
|
+
argumentQuoter: quoteRemoteShellArgument,
|
|
68
|
+
}),
|
|
69
|
+
]
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join(' && ')
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
generateCommand: `ssh ${quoteShellArgument(sshHost)} ${quoteShellArgument(remoteGenerateCommand)}`,
|
|
75
|
+
copyCommand: `scp ${quoteShellArgument(`${sshHost}:${remoteOutput}`)} ${quoteShellArgument(localOutputPath)}`,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function loadDotenvIfAvailable(dotenvFilePath) {
|
|
80
|
+
try {
|
|
81
|
+
const dotenvModule = await import('dotenv')
|
|
82
|
+
dotenvModule.config({ path: dotenvFilePath })
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const errorCode = error?.code || error?.cause?.code
|
|
85
|
+
if (errorCode !== 'ERR_MODULE_NOT_FOUND') {
|
|
86
|
+
console.warn(`⚠️ Unable to load dotenv file at ${dotenvFilePath}:`, error?.message || error)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeGeneratedTypes(filePath) {
|
|
92
|
+
writeText(filePath, `${readText(filePath).trimEnd()}\n`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function main() {
|
|
96
|
+
const parsedArguments = parseArgs()
|
|
97
|
+
const currentWorkingDirectory = process.cwd()
|
|
98
|
+
const { config } = await loadNsdbConfig(currentWorkingDirectory, parsedArguments.get('config', ''))
|
|
99
|
+
const dotenvFilePath = path.resolve(currentWorkingDirectory, parsedArguments.get('dotenv', '.env'))
|
|
100
|
+
await loadDotenvIfAvailable(dotenvFilePath)
|
|
101
|
+
|
|
102
|
+
const outputFilePath = path.resolve(currentWorkingDirectory, getOption(parsedArguments, config, 'out', 'paths.types'))
|
|
103
|
+
const projectId = getOption(parsedArguments, config, 'project-id', 'supabase.projectId', process.env.SUPABASE_PROJECT_ID || '')
|
|
104
|
+
const dbUrl = getOption(parsedArguments, config, 'db-url', 'supabase.dbUrl', process.env.SUPABASE_DB_URL || '')
|
|
105
|
+
const schemaName = getOption(parsedArguments, config, 'schema', 'supabase.schema', 'public')
|
|
106
|
+
const useLinkedProject = getBoolOption(parsedArguments, config, 'linked', 'supabase.linked', false)
|
|
107
|
+
const remoteSshHost = getOption(parsedArguments, config, 'remote-ssh-host', 'supabase.remoteTypes.sshHost', process.env.SUPABASE_REMOTE_SSH_HOST || '')
|
|
108
|
+
const remoteProjectPath = getOption(parsedArguments, config, 'remote-project-path', 'supabase.remoteTypes.projectPath', process.env.SUPABASE_REMOTE_PROJECT_PATH || '')
|
|
109
|
+
const remoteDbUrl = getOption(parsedArguments, config, 'remote-db-url', 'supabase.remoteTypes.dbUrl', process.env.SUPABASE_REMOTE_DB_URL || '')
|
|
110
|
+
const remoteOutputPath = getOption(parsedArguments, config, 'remote-output', 'supabase.remoteTypes.remoteOutput', '/tmp/database.types.ts')
|
|
111
|
+
const remoteBeforeCommand = getOption(parsedArguments, config, 'remote-before-command', 'supabase.remoteTypes.beforeCommand', process.env.SUPABASE_REMOTE_BEFORE_COMMAND || '')
|
|
112
|
+
const remoteSupabaseCommand = getOption(parsedArguments, config, 'remote-supabase-command', 'supabase.remoteTypes.supabaseCommand', process.env.SUPABASE_REMOTE_SUPABASE_COMMAND || 'npx supabase')
|
|
113
|
+
const useRemoteTypes = Boolean(remoteSshHost)
|
|
114
|
+
|
|
115
|
+
if (useRemoteTypes && !remoteDbUrl) {
|
|
116
|
+
console.error('❌ Missing remote DB URL (pass --remote-db-url or set supabase.remoteTypes.dbUrl).')
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!useRemoteTypes && !dbUrl && !useLinkedProject && !projectId) {
|
|
121
|
+
console.error('❌ Missing Supabase source (pass --remote-ssh-host, --db-url, --project-id or --linked).')
|
|
122
|
+
process.exit(1)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!useRemoteTypes && !isAvailable('npx supabase --version')) {
|
|
126
|
+
console.error('❌ Supabase CLI not available. Install it with: npm i -D supabase')
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ensureDir(path.dirname(outputFilePath))
|
|
131
|
+
if (useRemoteTypes) {
|
|
132
|
+
const { generateCommand, copyCommand } = buildRemoteTypesCommands({
|
|
133
|
+
sshHost: remoteSshHost,
|
|
134
|
+
projectPath: remoteProjectPath,
|
|
135
|
+
dbUrl: remoteDbUrl,
|
|
136
|
+
remoteOutput: remoteOutputPath,
|
|
137
|
+
localOutputPath: outputFilePath,
|
|
138
|
+
schemaName,
|
|
139
|
+
beforeCommand: remoteBeforeCommand,
|
|
140
|
+
supabaseCommand: remoteSupabaseCommand,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
console.log(`📦 Project: ${remoteSshHost} (remote ssh)`)
|
|
144
|
+
console.log(`📁 Output: ${path.relative(currentWorkingDirectory, outputFilePath)}`)
|
|
145
|
+
console.log(`📚 Schema: ${schemaName}`)
|
|
146
|
+
console.log(`🗄️ Remote output: ${remoteOutputPath}`)
|
|
147
|
+
console.log('🔄 Generating Supabase types remotely...')
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
run(generateCommand, { inherit: true })
|
|
151
|
+
run(copyCommand, { inherit: true })
|
|
152
|
+
normalizeGeneratedTypes(outputFilePath)
|
|
153
|
+
console.log('✅ Types generated and copied successfully.')
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('❌ Failed to generate Supabase types remotely.')
|
|
156
|
+
process.exit(1)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const commandLine = buildCommand({
|
|
163
|
+
projectId,
|
|
164
|
+
dbUrl,
|
|
165
|
+
outputPath: outputFilePath,
|
|
166
|
+
schemaName,
|
|
167
|
+
useLinkedProject
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
console.log(`📦 Project: ${dbUrl ? '(db-url)' : useLinkedProject ? '(linked)' : projectId}`)
|
|
171
|
+
console.log(`📁 Output: ${path.relative(currentWorkingDirectory, outputFilePath)}`)
|
|
172
|
+
console.log(`📚 Schema: ${schemaName}`)
|
|
173
|
+
if (dbUrl) console.log('🗄️ DB URL mode enabled')
|
|
174
|
+
if (useLinkedProject) console.log('🔗 Linked project mode enabled')
|
|
175
|
+
console.log('🔄 Generating Supabase types...')
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
run(commandLine, {
|
|
179
|
+
inherit: true,
|
|
180
|
+
env: buildLocalTypesEnvironment({ dbUrl }),
|
|
181
|
+
})
|
|
182
|
+
normalizeGeneratedTypes(outputFilePath)
|
|
183
|
+
console.log('✅ Types generated successfully.')
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('❌ Failed to generate Supabase types.')
|
|
186
|
+
process.exit(1)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
191
|
+
main().catch((error) => {
|
|
192
|
+
console.error('❌ Unexpected error while generating types.')
|
|
193
|
+
console.error(error)
|
|
194
|
+
process.exit(1)
|
|
195
|
+
})
|
|
196
|
+
}
|
package/scripts/init.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { parseArgs } from '../helpers/args.js'
|
|
4
|
+
import { ensureDir, exists, readText, writeText } from '../helpers/io.js'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PATHS = {
|
|
7
|
+
types: 'types/database.types.ts',
|
|
8
|
+
metadata: 'nsdb/database.metadata.json',
|
|
9
|
+
enums: 'nsdb/enums.ts',
|
|
10
|
+
schemas: 'nsdb/schemas',
|
|
11
|
+
models: 'nsdb/models',
|
|
12
|
+
composables: 'nsdb/composables',
|
|
13
|
+
stores: 'stores',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const NSDB_SCRIPTS = {
|
|
17
|
+
'nsdb:init': 'nsdb init',
|
|
18
|
+
'nsdb:types': 'nsdb generate:types',
|
|
19
|
+
'nsdb:metadata': 'nsdb generate:metadata',
|
|
20
|
+
'nsdb:enums': 'nsdb generate:enums',
|
|
21
|
+
'nsdb:schemas': 'nsdb generate:schemas',
|
|
22
|
+
'nsdb:models': 'nsdb generate:models',
|
|
23
|
+
'nsdb:composables': 'nsdb generate:composables',
|
|
24
|
+
'nsdb:stores': 'nsdb generate:stores',
|
|
25
|
+
'nsdb:all': 'nsdb generate:all',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function quoteString(value) {
|
|
29
|
+
return `'${String(value).replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildNsdbConfigTemplate({
|
|
33
|
+
schemaName = 'public',
|
|
34
|
+
projectIdExpression = 'process.env.SUPABASE_PROJECT_ID',
|
|
35
|
+
dbUrlExpression = '',
|
|
36
|
+
remoteTypes = null,
|
|
37
|
+
linked = false,
|
|
38
|
+
paths = DEFAULT_PATHS,
|
|
39
|
+
} = {}) {
|
|
40
|
+
const usesDefaultPaths = Object.entries(DEFAULT_PATHS).every(([key, value]) => paths[key] === value)
|
|
41
|
+
const projectIdLine = linked || dbUrlExpression || remoteTypes
|
|
42
|
+
? ''
|
|
43
|
+
: `\n\t\tprojectId: ${projectIdExpression},`
|
|
44
|
+
const dbUrlLine = dbUrlExpression
|
|
45
|
+
? `\n\t\tdbUrl: ${dbUrlExpression},`
|
|
46
|
+
: ''
|
|
47
|
+
const remoteTypesLine = remoteTypes
|
|
48
|
+
? `\n\t\tremoteTypes: {
|
|
49
|
+
\t\t\tsshHost: ${remoteTypes.sshHost},
|
|
50
|
+
\t\t\tprojectPath: ${remoteTypes.projectPath},
|
|
51
|
+
\t\t\tdbUrl: ${remoteTypes.dbUrl},
|
|
52
|
+
\t\t\tremoteOutput: ${remoteTypes.remoteOutput},
|
|
53
|
+
\t\t\tbeforeCommand: ${remoteTypes.beforeCommand},
|
|
54
|
+
\t\t\tsupabaseCommand: ${remoteTypes.supabaseCommand},
|
|
55
|
+
\t\t},`
|
|
56
|
+
: ''
|
|
57
|
+
const pathsBlock = usesDefaultPaths
|
|
58
|
+
? ''
|
|
59
|
+
: `
|
|
60
|
+
paths: {
|
|
61
|
+
${Object.entries(paths).map(([key, value]) => `\t\t${key}: ${quoteString(value)},`).join('\n')}
|
|
62
|
+
},
|
|
63
|
+
imports: {
|
|
64
|
+
databaseTypes: '~~/types/database.types',
|
|
65
|
+
},`
|
|
66
|
+
|
|
67
|
+
return `import type { NsdbConfig } from '@lucashw68/nsdb/types/config'
|
|
68
|
+
|
|
69
|
+
export default {
|
|
70
|
+
\tsupabase: {
|
|
71
|
+
\t\tschema: ${quoteString(schemaName)},${projectIdLine}${dbUrlLine}${remoteTypesLine}
|
|
72
|
+
\t\tlinked: ${linked ? 'true' : 'false'},
|
|
73
|
+
\t},${pathsBlock}
|
|
74
|
+
} satisfies NsdbConfig
|
|
75
|
+
`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function mergePackageScripts(packageJson, scriptsToAdd = NSDB_SCRIPTS) {
|
|
79
|
+
const nextPackageJson = {
|
|
80
|
+
...packageJson,
|
|
81
|
+
scripts: {
|
|
82
|
+
...(packageJson.scripts ?? {}),
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const [scriptName, scriptCommand] of Object.entries(scriptsToAdd)) {
|
|
87
|
+
if (!nextPackageJson.scripts[scriptName]) {
|
|
88
|
+
nextPackageJson.scripts[scriptName] = scriptCommand
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return nextPackageJson
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildEnvExample() {
|
|
96
|
+
return `SUPABASE_URL=https://your-project.supabase.co
|
|
97
|
+
SUPABASE_KEY=your-anon-key
|
|
98
|
+
SUPABASE_PROJECT_ID=your-project-id
|
|
99
|
+
SUPABASE_DB_URL=postgresql://postgres:password@localhost:5432/postgres
|
|
100
|
+
SUPABASE_REMOTE_SSH_HOST=vps
|
|
101
|
+
SUPABASE_REMOTE_PROJECT_PATH=/opt/supabase-projects/example
|
|
102
|
+
SUPABASE_REMOTE_DB_URL=postgresql://postgres:$POSTGRES_PASSWORD@db:5432/postgres
|
|
103
|
+
SUPABASE_REMOTE_BEFORE_COMMAND=source ~/.nvm/nvm.sh
|
|
104
|
+
SUPABASE_REMOTE_SUPABASE_COMMAND=npx supabase
|
|
105
|
+
`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writeFileIfAllowed(filePath, content, { force = false, label }) {
|
|
109
|
+
if (exists(filePath) && !force) {
|
|
110
|
+
console.log(`↷ ${label} already exists, skipped: ${path.relative(process.cwd(), filePath)}`)
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
writeText(filePath, content)
|
|
115
|
+
console.log(`✓ ${label}: ${path.relative(process.cwd(), filePath)}`)
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ensureConfiguredDirectories(currentWorkingDirectory, paths = DEFAULT_PATHS) {
|
|
120
|
+
const directories = [
|
|
121
|
+
path.dirname(paths.types),
|
|
122
|
+
path.dirname(paths.metadata),
|
|
123
|
+
path.dirname(paths.enums),
|
|
124
|
+
paths.schemas,
|
|
125
|
+
paths.models,
|
|
126
|
+
paths.composables,
|
|
127
|
+
paths.stores,
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
for (const directoryPath of directories) {
|
|
131
|
+
const absoluteDirectoryPath = path.resolve(currentWorkingDirectory, directoryPath)
|
|
132
|
+
ensureDir(absoluteDirectoryPath)
|
|
133
|
+
console.log(`✓ Directory: ${path.relative(currentWorkingDirectory, absoluteDirectoryPath)}`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function updatePackageJson(currentWorkingDirectory) {
|
|
138
|
+
const packageJsonPath = path.resolve(currentWorkingDirectory, 'package.json')
|
|
139
|
+
|
|
140
|
+
if (!exists(packageJsonPath)) {
|
|
141
|
+
console.log('↷ package.json not found, skipped scripts setup')
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const packageJson = JSON.parse(readText(packageJsonPath))
|
|
146
|
+
const nextPackageJson = mergePackageScripts(packageJson)
|
|
147
|
+
writeText(packageJsonPath, `${JSON.stringify(nextPackageJson, null, 2)}\n`)
|
|
148
|
+
console.log('✓ package.json scripts')
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function printNextSteps({ linked, usesDbUrl }) {
|
|
153
|
+
console.log('')
|
|
154
|
+
console.log('Next steps:')
|
|
155
|
+
console.log('1. Add @lucashw68/nsdb to modules in nuxt.config.ts.')
|
|
156
|
+
console.log('2. Configure @nuxtjs/supabase with SUPABASE_URL and SUPABASE_KEY.')
|
|
157
|
+
if (!linked && !usesDbUrl) console.log('3. Set SUPABASE_PROJECT_ID in .env.')
|
|
158
|
+
if (usesDbUrl) console.log('3. Set SUPABASE_DB_URL or SUPABASE_REMOTE_* variables in .env.')
|
|
159
|
+
console.log(`${linked && !usesDbUrl ? '3' : '4'}. Run: npm run nsdb:all`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function initNsdb({
|
|
163
|
+
currentWorkingDirectory = process.cwd(),
|
|
164
|
+
parsedArguments = parseArgs(),
|
|
165
|
+
} = {}) {
|
|
166
|
+
const force = parsedArguments.getBool('force', false)
|
|
167
|
+
const linked = parsedArguments.getBool('linked', false)
|
|
168
|
+
const schemaName = parsedArguments.get('schema', 'public')
|
|
169
|
+
const projectId = parsedArguments.get('project-id', '')
|
|
170
|
+
const dbUrl = parsedArguments.get('db-url', '')
|
|
171
|
+
const remoteSshHost = parsedArguments.get('remote-ssh-host', '')
|
|
172
|
+
const remoteProjectPath = parsedArguments.get('remote-project-path', '')
|
|
173
|
+
const remoteDbUrl = parsedArguments.get('remote-db-url', '')
|
|
174
|
+
const remoteOutput = parsedArguments.get('remote-output', '/tmp/database.types.ts')
|
|
175
|
+
const remoteBeforeCommand = parsedArguments.get('remote-before-command', '')
|
|
176
|
+
const remoteSupabaseCommand = parsedArguments.get('remote-supabase-command', '')
|
|
177
|
+
const projectIdExpression = projectId ? quoteString(projectId) : 'process.env.SUPABASE_PROJECT_ID'
|
|
178
|
+
const dbUrlExpression = dbUrl ? quoteString(dbUrl) : parsedArguments.getBool('self-hosted', false) ? 'process.env.SUPABASE_DB_URL' : ''
|
|
179
|
+
const useRemoteTypes = Boolean(remoteSshHost) || parsedArguments.getBool('remote-types', false)
|
|
180
|
+
const remoteTypes = useRemoteTypes
|
|
181
|
+
? {
|
|
182
|
+
sshHost: remoteSshHost ? quoteString(remoteSshHost) : 'process.env.SUPABASE_REMOTE_SSH_HOST',
|
|
183
|
+
projectPath: remoteProjectPath ? quoteString(remoteProjectPath) : 'process.env.SUPABASE_REMOTE_PROJECT_PATH',
|
|
184
|
+
dbUrl: remoteDbUrl ? quoteString(remoteDbUrl) : 'process.env.SUPABASE_REMOTE_DB_URL',
|
|
185
|
+
remoteOutput: quoteString(remoteOutput),
|
|
186
|
+
beforeCommand: remoteBeforeCommand ? quoteString(remoteBeforeCommand) : 'process.env.SUPABASE_REMOTE_BEFORE_COMMAND',
|
|
187
|
+
supabaseCommand: remoteSupabaseCommand ? quoteString(remoteSupabaseCommand) : 'process.env.SUPABASE_REMOTE_SUPABASE_COMMAND',
|
|
188
|
+
}
|
|
189
|
+
: null
|
|
190
|
+
const configFileName = parsedArguments.get('config', 'nsdb.config.ts')
|
|
191
|
+
const configFilePath = path.resolve(currentWorkingDirectory, configFileName)
|
|
192
|
+
const envExampleFilePath = path.resolve(currentWorkingDirectory, '.env.example')
|
|
193
|
+
|
|
194
|
+
console.log('🧬 Initializing NSDB...')
|
|
195
|
+
|
|
196
|
+
writeFileIfAllowed(
|
|
197
|
+
configFilePath,
|
|
198
|
+
buildNsdbConfigTemplate({
|
|
199
|
+
schemaName,
|
|
200
|
+
projectIdExpression,
|
|
201
|
+
dbUrlExpression,
|
|
202
|
+
remoteTypes,
|
|
203
|
+
linked,
|
|
204
|
+
paths: DEFAULT_PATHS,
|
|
205
|
+
}),
|
|
206
|
+
{ force, label: 'Config' }
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
writeFileIfAllowed(envExampleFilePath, buildEnvExample(), {
|
|
210
|
+
force: false,
|
|
211
|
+
label: 'Env example',
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
ensureConfiguredDirectories(currentWorkingDirectory, DEFAULT_PATHS)
|
|
215
|
+
updatePackageJson(currentWorkingDirectory)
|
|
216
|
+
printNextSteps({ linked, usesDbUrl: Boolean(dbUrlExpression || remoteTypes) })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
220
|
+
initNsdb().catch((error) => {
|
|
221
|
+
console.error('❌ Failed to initialize NSDB.')
|
|
222
|
+
console.error(error)
|
|
223
|
+
process.exit(1)
|
|
224
|
+
})
|
|
225
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// __IMPORTS__
|
|
2
|
+
import type { Tables, TablesInsert, TablesUpdate } from '~~/types/database.types'
|
|
3
|
+
import { __PASCAL__Relations, __PASCAL__Schema } from '~~/nsdb/schemas/__TABLE__'
|
|
4
|
+
import { useNsdbSchema } from '@lucashw68/nsdb/useNsdbSchema'
|
|
5
|
+
__STORE_IMPORT__
|
|
6
|
+
|
|
7
|
+
export type __ROW__ = Omit<Tables<'__TABLE__'>, __ROW_OMIT__>
|
|
8
|
+
export type __PASCAL__Insert = Omit<TablesInsert<'__TABLE__'>, __INSERT_OMIT__>
|
|
9
|
+
export type __PASCAL__Update = Omit<TablesUpdate<'__TABLE__'>, __UPDATE_OMIT__>
|
|
10
|
+
export type __PASCAL__RelationRows = __RELATION_ROWS__
|
|
11
|
+
|
|
12
|
+
export function __HOOK__(opts: { store?: boolean } = {}) {
|
|
13
|
+
const model = useSupabaseModel<__ROW__, __PASCAL__Insert, __PASCAL__Update, '__PRIMARY_KEY__'>(
|
|
14
|
+
'__TABLE__',
|
|
15
|
+
{ store: !!opts.store, storeCreator: __STORE_CREATOR__, primaryKey: '__PRIMARY_KEY__' }
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
fields,
|
|
20
|
+
editableKeys,
|
|
21
|
+
createDraftFromSchema,
|
|
22
|
+
bindModel,
|
|
23
|
+
} = useNsdbSchema(__PASCAL__Schema, __PASCAL__Relations);
|
|
24
|
+
|
|
25
|
+
const { fetch, refresh } = bindModel<__ROW__, __PASCAL__RelationRows>(model)
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
primaryKey: '__PRIMARY_KEY__' as const,
|
|
29
|
+
items: model.items,
|
|
30
|
+
totalCount: model.totalCount,
|
|
31
|
+
loading: model.loading,
|
|
32
|
+
error: model.error,
|
|
33
|
+
stale: model.stale,
|
|
34
|
+
schema: __PASCAL__Schema,
|
|
35
|
+
fields,
|
|
36
|
+
editableKeys,
|
|
37
|
+
createDraft: () => createDraftFromSchema(),
|
|
38
|
+
fetch,
|
|
39
|
+
refresh,
|
|
40
|
+
invalidate: model.invalidate,
|
|
41
|
+
getById: model.getById,
|
|
42
|
+
create: model.create,
|
|
43
|
+
update: model.update,
|
|
44
|
+
remove: model.remove,
|
|
45
|
+
subscribe: model.subscribe,
|
|
46
|
+
unsubscribe: model.unsubscribe,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Tables } from '~~/types/database.types'
|
|
2
|
+
import type { EntityField, EntityRelation } from '@lucashw68/nsdb/types/entities'
|
|
3
|
+
import * as Enums from '~~/nsdb/enums'
|
|
4
|
+
|
|
5
|
+
// Convenience row type for this table (optional)
|
|
6
|
+
export type __ROW__ = Tables<'__TABLE__'>
|
|
7
|
+
|
|
8
|
+
/** ----- Schema (entities) for __TABLE__ ----- */
|
|
9
|
+
export const __PASCAL__Schema = {
|
|
10
|
+
// __FIELDS__
|
|
11
|
+
} as const satisfies Partial<Record<keyof __ROW__, EntityField>>
|
|
12
|
+
|
|
13
|
+
export const __PASCAL__Relations: EntityRelation[] = __RELATIONS__
|
package/types/config.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface NsdbConfig {
|
|
2
|
+
supabase?: {
|
|
3
|
+
schema?: string
|
|
4
|
+
projectId?: string
|
|
5
|
+
dbUrl?: string
|
|
6
|
+
linked?: boolean
|
|
7
|
+
remoteTypes?: {
|
|
8
|
+
sshHost?: string
|
|
9
|
+
projectPath?: string
|
|
10
|
+
dbUrl?: string
|
|
11
|
+
remoteOutput?: string
|
|
12
|
+
beforeCommand?: string
|
|
13
|
+
supabaseCommand?: string
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
paths?: {
|
|
17
|
+
types?: string
|
|
18
|
+
metadata?: string
|
|
19
|
+
enums?: string
|
|
20
|
+
schemas?: string
|
|
21
|
+
models?: string
|
|
22
|
+
composables?: string
|
|
23
|
+
stores?: string
|
|
24
|
+
}
|
|
25
|
+
imports?: {
|
|
26
|
+
databaseTypes?: string
|
|
27
|
+
}
|
|
28
|
+
tables?: {
|
|
29
|
+
/** Allowlist of tables exposed through generated NSDB artifacts. */
|
|
30
|
+
include?: string[]
|
|
31
|
+
/** Tables omitted from generated NSDB artifacts. Mutually exclusive with include. */
|
|
32
|
+
exclude?: string[]
|
|
33
|
+
/** Per-table client exposure rules. Unspecified columns keep inferred defaults. */
|
|
34
|
+
columns?: Record<string, Record<string, {
|
|
35
|
+
selectable?: boolean
|
|
36
|
+
editable?: boolean
|
|
37
|
+
hidden?: boolean
|
|
38
|
+
serverOnly?: boolean
|
|
39
|
+
}>>
|
|
40
|
+
}
|
|
41
|
+
templates?: {
|
|
42
|
+
model?: string
|
|
43
|
+
schema?: string
|
|
44
|
+
useNsdbModel?: string
|
|
45
|
+
store?: string
|
|
46
|
+
}
|
|
47
|
+
generators?: {
|
|
48
|
+
force?: boolean
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type FieldType =
|
|
2
|
+
| 'text'
|
|
3
|
+
| 'textarea'
|
|
4
|
+
| 'select'
|
|
5
|
+
| 'checkbox'
|
|
6
|
+
| 'number'
|
|
7
|
+
| 'date'
|
|
8
|
+
| 'datetime'
|
|
9
|
+
| 'json'
|
|
10
|
+
| 'array'
|
|
11
|
+
| 'file'
|
|
12
|
+
| 'relation'
|
|
13
|
+
|
|
14
|
+
export type EntityField = {
|
|
15
|
+
label: string
|
|
16
|
+
type: FieldType
|
|
17
|
+
required?: boolean
|
|
18
|
+
readonly?: boolean
|
|
19
|
+
selectable?: boolean
|
|
20
|
+
editable?: boolean
|
|
21
|
+
insertable?: boolean
|
|
22
|
+
updatable?: boolean
|
|
23
|
+
hidden?: boolean
|
|
24
|
+
serverOnly?: boolean
|
|
25
|
+
primaryKey?: boolean
|
|
26
|
+
nullable?: boolean
|
|
27
|
+
hasDefault?: boolean
|
|
28
|
+
databaseType?: string
|
|
29
|
+
defaultExpression?: string | null
|
|
30
|
+
default?: any
|
|
31
|
+
options?: Array<{ label: string; value: any }>
|
|
32
|
+
relation?: EntityRelation
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type RelationKind = 'belongsTo' | 'hasOne' | 'hasMany' | 'manyToMany'
|
|
36
|
+
|
|
37
|
+
export interface EntityRelation {
|
|
38
|
+
alias?: string
|
|
39
|
+
kind: RelationKind
|
|
40
|
+
direction?: 'forward' | 'inverse' | 'through'
|
|
41
|
+
nullable?: boolean
|
|
42
|
+
composite?: boolean
|
|
43
|
+
throughTable?: string
|
|
44
|
+
/** Table référencée dans Supabase (ex: "playlists") */
|
|
45
|
+
referencedTable: string
|
|
46
|
+
embedResource?: string
|
|
47
|
+
/** Colonnes locales qui forment la FK (souvent ["playlist_id"]) */
|
|
48
|
+
localColumns: string[]
|
|
49
|
+
/** Colonnes référencées (souvent ["id"]) */
|
|
50
|
+
referencedColumns: string[]
|
|
51
|
+
/** Nom de la FK dans Supabase (facultatif, pour debug) */
|
|
52
|
+
foreignKeyName?: string,
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Champ de la table référencée à utiliser comme label.
|
|
56
|
+
* Exemple: "title", "username", "full_name", etc.
|
|
57
|
+
* Si non fourni, on tombera sur 'id' (sans guess "intelligent").
|
|
58
|
+
*/
|
|
59
|
+
displayField?: string
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Est-ce qu'on autorise la création inline de l'entité liée
|
|
63
|
+
* dans NsdbForm ? (préparation pour la suite)
|
|
64
|
+
*/
|
|
65
|
+
allowInlineCreate?: boolean
|
|
66
|
+
}
|
package/types/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type { NsdbConfig } from './config'
|
|
2
|
+
export type { EntityField, EntityRelation, FieldType, RelationKind } from './entities'
|
|
3
|
+
export type {
|
|
4
|
+
Column,
|
|
5
|
+
ListOptions,
|
|
6
|
+
NsdbTableClasses,
|
|
7
|
+
OrderDirection,
|
|
8
|
+
SortState,
|
|
9
|
+
WhereClause,
|
|
10
|
+
WhereOperator,
|
|
11
|
+
WherePrimitive,
|
|
12
|
+
WhereValue,
|
|
13
|
+
} from './list'
|
|
14
|
+
export type { ModelHandle, ModelMutationTarget, ModelQuery, ModelWhere } from './model'
|